Open Closed

Register user from AzureAD with additional info #5817


User avatar
0
ageiter created
  • ABP Framework version: v7.3.2
  • UI Type: Blazor Server
  • Database System: EF Core (SQL Server)
  • Tiered (for MVC) or Auth Server Separated (for Angular): no

I use the login with the external provider "Microsoft" (Azure AD). If the user does not exist yet, it will be registered with the email address by default. However, I would like to automatically fill the other information as well: firstname, lastname and profile picture...

How can I do this? I have tried with a custom LoginModel but have not figured out how and which method I would need to hook into.


7 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    You should override the OnGetExternalLoginCallbackAsync method.

  • User Avatar
    0
    ageiter created

    I see that I can retrieve the claims here.

            public override async Task<IActionResult> OnGetExternalLoginCallbackAsync(string returnUrl = "", string returnUrlHash = "", string remoteError = null)
            {
                var loginInfo = await SignInManager.GetExternalLoginInfoAsync();
                var claims = loginInfo.Principal.Claims;
    
                return await base.OnGetExternalLoginCallbackAsync(returnUrl, returnUrlHash, remoteError);
            }
    

    But then comes the page for registration, in which only the mail address is added to the new user. How can I add this additional information there? So that at the end the new user is completed with data from the claims?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    You can also override the RegisterModel.

  • User Avatar
    0
    ageiter created

    I have now solved this a little differently, as I do not need the manual registration and prefer to create the user automatically. Therefore I have now copied the OnGetExternalLoginCallbackAsync method and adapted it accordingly (see below).

    loginInfo.Principal.AddClaim(AbpClaimTypes.SurName, loginInfo.Principal.FindFirstValue(ClaimTypes.Surname) ?? string.Empty);

            public override async Task<IActionResult> OnGetExternalLoginCallbackAsync(string returnUrl = "", string returnUrlHash = "", string remoteError = null)
            {
                if (remoteError != null)
                {
                    Logger.LogWarning($"External login callback error: {remoteError}");
                    return RedirectToPage("./Login");
                }
    
                await IdentityOptions.SetAsync();
    
                var loginInfo = await SignInManager.GetExternalLoginInfoAsync();
                if (loginInfo == null)
                {
                    Logger.LogWarning("External login info is not available");
                    return RedirectToPage("./Login");
                }
    
                var result = await SignInManager.ExternalLoginSignInAsync(
                    loginInfo.LoginProvider,
                    loginInfo.ProviderKey,
                    isPersistent: false,
                    bypassTwoFactor: true
                );
    
                if (!result.Succeeded)
                {
                    await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
                    {
                        Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
                        Action = "Login" + result
                    });
                }
    
                if (result.IsLockedOut)
                {
                    Logger.LogWarning($"External login callback error: user is locked out!");
                    throw new UserFriendlyException("Cannot proceed because user is locked out!");
                }
    
                if (result.IsNotAllowed)
                {
                    Logger.LogWarning($"External login callback error: user is not allowed!");
                    throw new UserFriendlyException("Cannot proceed because user is not allowed!");
                }
    
                if (result.Succeeded)
                {
                    return RedirectSafely(returnUrl, returnUrlHash);
                }
    
                //TODO: Handle other cases for result!
    
                var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email);
                if (string.IsNullOrWhiteSpace(email))
                {
                    return RedirectToPage("./Register", new {
                        IsExternalLogin = true,
                        ExternalLoginAuthSchema = loginInfo.LoginProvider,
                        ReturnUrl = returnUrl
                    });
                }
    
                var user = await UserManager.FindByEmailAsync(email);
                if (user == null)
                {
                    // Get additional infos from AzureAD Claims and set them to AbpClaims
                    loginInfo.Principal.AddClaim(AbpClaimTypes.Email, loginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? string.Empty);
                    loginInfo.Principal.AddClaim(AbpClaimTypes.UserName, loginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? string.Empty);
                    loginInfo.Principal.AddClaim(AbpClaimTypes.Name, loginInfo.Principal.FindFirstValue(ClaimTypes.GivenName) ?? string.Empty);
                    loginInfo.Principal.AddClaim(AbpClaimTypes.SurName, loginInfo.Principal.FindFirstValue(ClaimTypes.Surname) ?? string.Empty);
    
                    user = await CreateExternalUserAsync(loginInfo);
                }
                else
                {
                    if (await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey) == null)
                    {
                        CheckIdentityErrors(await UserManager.AddLoginAsync(user, loginInfo));
                    }
                }
    
                await SignInManager.SignInAsync(user, false);
    
                await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
                {
                    Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
                    Action = result.ToIdentitySecurityLogAction(),
                    UserName = user.Name
                });
    
                return RedirectSafely(returnUrl, returnUrlHash);
            }
    

    How do I get the profile picture now? I think the only way is through the Microsoft Graph, right? Can you tell me how to create the GraphServiceClient? I don't know what I need to pass there as parameters and have available in this method.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Sorry, I'm not familiar with Microsoft Graph.

  • User Avatar
    0
    ageiter created

    Ok, I have done it now. I'll post the code in case anyone else can use it.

    I find it a bit uncomfortable that I had to implement an extra class for the AuthenticationProvider with interface IAccessTokenProvider. @maliming, do you know a more elegant solution?

    public class MyAuthenticationProvider : IAccessTokenProvider
    {
        private readonly AuthenticationToken _accessToken;
        
        public AllowedHostsValidator AllowedHostsValidator { get; }
    
        public MyAuthenticationProvider(AuthenticationToken accessToken)
        {
            _accessToken = accessToken;
        }
    
        public Task<string> GetAuthorizationTokenAsync(Uri uri, Dictionary<string, object>? additionalAuthenticationContext = null, CancellationToken cancellationToken = default)
        {
            return Task.FromResult(_accessToken.Value);
        }
    }
    

    In class MyLoginModel.cs:

    public override async Task<IActionResult> OnGetExternalLoginCallbackAsync(string returnUrl = "", string returnUrlHash = "", string remoteError = null)
    {
        ...
        var externalUser = await UserManager.FindByEmailAsync(email);
        if (externalUser == null)
        {
            // Create a new user with data from AzureAD profile
            loginInfo.Principal.AddClaim(AbpClaimTypes.Email, loginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? string.Empty);
            loginInfo.Principal.AddClaim(AbpClaimTypes.UserName, loginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? string.Empty);
            loginInfo.Principal.AddClaim(AbpClaimTypes.Name, loginInfo.Principal.FindFirstValue(ClaimTypes.GivenName) ?? string.Empty);
            loginInfo.Principal.AddClaim(AbpClaimTypes.SurName, loginInfo.Principal.FindFirstValue(ClaimTypes.Surname) ?? string.Empty);
    
            externalUser = await CreateExternalUserAsync(loginInfo);
    
            await SetProfilePictureAsync(loginInfo, externalUser);
        }
        ...
    }
    
    /// < summary>
    /// Save user's profile picture from AzureAD as ABP profile picture.
    /// Note: Code copied and modified from Volo.Abp.Account.AccountAppService
    /// < /summary>
    private async Task SetProfilePictureAsync(ExternalLoginInfo loginInfo, IdentityUser user)
    {
        try
        {
            var accessToken = loginInfo.AuthenticationTokens?.FirstOrDefault(x => x.Name == "access_token");
            if (accessToken == null)
            {
                return;
            }
    
            var authenticationProvider = new BaseBearerTokenAuthenticationProvider(new MyAuthenticationProvider(accessToken));
            var graphClient = new GraphServiceClient(authenticationProvider);
    
            // Get user photo from Azure
            using (var photoStream = await graphClient.Me.Photo.Content.GetAsync())
            {
                if (photoStream == null)
                {
                    return;
                }
    
                var input = new ProfilePictureInput { ImageContent = new RemoteStreamContent(photoStream), Type = ProfilePictureType.Image };
    
                await SettingManager.SetForUserAsync(user.Id, AccountSettingNames.ProfilePictureSource, input.Type.ToString());
    
                var userIdText = user.Id.ToString();
    
                if (input.Type != ProfilePictureType.Image)
                {
                    if (await AccountProfilePictureContainer.ExistsAsync(userIdText))
                    {
                        await AccountProfilePictureContainer.DeleteAsync(userIdText);
                    }
                }
                else
                {
                    if (input.ImageContent == null)
                    {
                        throw new NoImageProvidedException();
                    }
    
                    await AccountProfilePictureContainer.SaveAsync(userIdText, input.ImageContent.GetStream(), true);
                }
            }
        }
        catch (Exception ex)
        {
            Logger.LogWarning($"Exception when retrieving or storing the profile picture: {ex.Message}");
        }
    }
    
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    I think these code are fine. : )

Made with ❤️ on ABP v8.2.0-preview Updated on March 25, 2024, 15:11