Starts in:
1 DAY
5 HRS
22 MIN
30 SEC
Starts in:
1 D
5 H
22 M
30 S
Open Closed

How to assign roles to Azure AD B2C users #5928


User avatar
0
jpatron created
  • ABP Framework version: v6.0.3
  • UI Type: MVC
  • Database System: EF Core (SQL Server)
  • Tiered (for MVC) or Auth Server Separated (for Angular): no
  • Exception message and full stack trace: N/A
  • Steps to reproduce the issue: N/A

Hello,

I am in the process of integrating Azure AD B2C with one of our applications using ABP Commercial framework. Azure AD B2C users are able to login to our application successfully after signing up for an account, and I noticed that ABP treats them as external users and I could not find any trace of stored information in our application database when it comes to the user and its roles, permissions, etc. So my question is, how do we assign roles and permissions to users that only exist in Azure AD B2C?

Thanks!


22 Answer(s)
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    You can assign a role to the user on the user edit page.

  • User Avatar
    0
    jpatron created

    Hi,

    Would you mind elaborating on your response a bit more? Are you talking about the user edit page in the ABP application? Or are you referring to edit the user directly on Azure AD B2C?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    I mean the user edit page in the ABP application

  • User Avatar
    0
    jpatron created

    Hi,

    With the current state of the integration with Azure AD B2C, we cannot edit the user and there are no user accounts in the ABP application database, and we cannot access the user edit page. I can share more details about how we integrated our ABP application with Azure AD B2C, if that helps.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    I can share more details about how we integrated our ABP application with Azure AD B2C, if that helps.

    Ok, please.

  • User Avatar
    0
    jpatron created

    So far, we have overwritten the OnGetAsync method in Login.cshtml.cs to redirect directly to Azure AD B2C:

    public virtual async Task<IActionResult> OnGetAsync()
    {
        // Customize the login page to redirect to the Azure AD B2C login endpoint.
        // References:
        // https://docs.abp.io/en/abp/6.0/UI/AspNetCore/Customization-User-Interface
        // https://support.abp.io/QA/Questions/5328/Issue-with-Azure-AD-SSO-using-open-id-connect
        ExternalProviders = await GetExternalProviders();
        return await OnPostExternalLogin(ExternalProviders.First().AuthenticationScheme);
    }
    

    Also, this is our version of the ConfigureAuthentication method in our application's web module class:

    private static void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
    {
        context.Services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
            // Handling SameSite cookie according to https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
            options.HandleSameSiteCookieCompatibility();
        });
    
        // Configuration to sign-in users with Azure AD B2C
        context.Services.AddMicrosoftIdentityWebAppAuthentication(configuration, "AzureAdB2C", OpenIdConnectDefaults.AuthenticationScheme)
            .EnableTokenAcquisitionToCallDownstreamApi()
            .AddInMemoryTokenCaches();
    
        context.Services.AddControllersWithViews()
            .AddMicrosoftIdentityUI();
    
        context.Services.AddRazorPages();
    
        // Configuring appsettings section AzureAdB2C, into IOptions
        context.Services.AddOptions();
        context.Services.Configure<OpenIdConnectOptions>(configuration.GetSection("AzureAdB2C"));
    
        // Set sign in scheme to external scheme
        // This is required to support external login/logout
        // Reference: https://community.abp.io/posts/how-to-use-the-azure-active-directory-authentication-for-mvc-razor-page-applications-4603b9cf?_ga=2.97528351.1400404833.1695766286-1228591420.1688082434
        context.Services.PostConfigure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            options.SignInScheme = IdentityConstants.ExternalScheme;
            options.ClaimActions.Add(new ClaimActions());
        });
    }
    
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    we cannot edit the user and there are no user accounts in the ABP application database, and we cannot access the user edit page.

    When you log in externally, it will create a local user, are there no users in your database?

    Could you create the suite to create a new project to reproduce the problem and share it with me? shiwei.liang@volosoft.com I will check it, thanks.

  • User Avatar
    0
    jpatron created

    Hello,

    I have the project ready for you but I cannot send it to you via email because the attachment size is too big. Is there an alternative way I can share the project with you?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    You can upload to google drive or onedrive and share the link with me. thanks.

  • User Avatar
    0
    jpatron created

    Hi,

    I emailed you the link to the new solution I created with our changes to integrate Azure AD B2C. One thing I noticed, however, is that the application does create a local user, but when the user is redirected to the home page after being authenticated in Azure AD B2C, the application still shows the Login button.

    In our actual application, I made a couple of changes that allow the application to create the local user after authenticating via Azure AD B2C, but the user goes into an infinite loop where the application keeps trying to display the alert to confirm the email address but it never loads.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    I can't download the file.

    In our actual application, I made a couple of changes that allow the application to create the local user after authenticating via Azure AD B2C, but the user goes into an infinite loop where the application keeps trying to display the alert to confirm the email address but it never loads.

    You can try this:

    [ExposeServices(typeof(MyLoginModel))]
    public class MyLoginModel : OpenIddictSupportedLoginModel
    {
       public MyLoginModel(IAuthenticationSchemeProvider schemeProvider, IOptions<AbpAccountOptions> accountOptions, IAbpRecaptchaValidatorFactory recaptchaValidatorFactory, IAccountExternalProviderAppService accountExternalProviderAppService, ICurrentPrincipalAccessor currentPrincipalAccessor, IOptions<IdentityOptions> identityOptions, IOptionsSnapshot<reCAPTCHAOptions> reCaptchaOptions, AbpOpenIddictRequestHelper openIddictRequestHelper) : base(schemeProvider, accountOptions, recaptchaValidatorFactory, accountExternalProviderAppService, currentPrincipalAccessor, identityOptions, reCaptchaOptions, openIddictRequestHelper)
       {
       }
    
       protected override async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
       {
           await IdentityOptions.SetAsync();
    
           var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email) ?? info.Principal.FindFirstValue(ClaimTypes.Email);
           var userName = await GetUserNameFromEmail(emailAddress);
           var user = new IdentityUser(GuidGenerator.Create(), userName, emailAddress, CurrentTenant.Id);
           user.SetEmailConfirmed(true); // set true to skip email confirmation step
    
           (await UserManager.CreateAsync(user)).CheckErrors();
           (await UserManager.SetEmailAsync(user, emailAddress)).CheckErrors();
           (await UserManager.AddLoginAsync(user, info)).CheckErrors();
           (await UserManager.AddDefaultRolesAsync(user)).CheckErrors();
    
           if (emailAddress == "<admin user email>")
           {
               await UserManager.AddToRoleAsync(user, "admin");// add admin role to admin user
           }
    
           user.Name = info.Principal.FindFirstValue(AbpClaimTypes.Name);
           user.Surname = info.Principal.FindFirstValue(AbpClaimTypes.SurName);
    
           var phoneNumber = info.Principal.FindFirstValue(AbpClaimTypes.PhoneNumber);
           if (!phoneNumber.IsNullOrWhiteSpace())
           {
               var phoneNumberConfirmed = string.Equals(info.Principal.FindFirstValue(AbpClaimTypes.PhoneNumberVerified), "true", StringComparison.InvariantCultureIgnoreCase);
               user.SetPhoneNumber(phoneNumber, phoneNumberConfirmed);
           }
    
           await UserManager.UpdateAsync(user);
    
           return user;
       }
       
       protected virtual async Task<string> GetUserNameFromEmail(string email)
       {
           var userName = email.Split('@')[0];
           var existUser = await UserManager.FindByNameAsync(userName);
           while (existUser != null)
           {
               var randomUserName = userName + RandomHelper.GetRandom(1000, 9999);
               existUser = await UserManager.FindByNameAsync(randomUserName);
               if (existUser == null)
               {
                   userName = randomUserName;
                   break;
               }
           }
    
           return userName;
       }
    }
    
    • Set email confirmation when creating external users
    • Assign admin role if email If the email address is the administrator (you can add any roles you want)
  • User Avatar
    0
    jpatron created

    Hi,

    I apologize for the inconvenience regarding to sharing the clean solution. Maybe what I can do is to email you only the files I had to modify after creating the clean solution. Also, how can I overwrite the Register.cshtml.cs file so that users go directly to Azure AD B2C similar to what I did with Login.cshtml.cs?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Also, how can I overwrite the Register.cshtml.cs file so that users go directly to Azure AD B2C similar to what I did with Login.cshtml.cs?

    Hi,

    You can try:

    [ExposeServices(typeof(RegisterModel))]
    public class MyRegisterModel : RegisterModel
    {
        public virtual async Task<IActionResult> OnGetAsync()
        {
            if (IsExternalLogin)
            {
                var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync();
                if (externalLoginInfo == null)
                {
                    Logger.LogWarning("External login info is not available");
                    return RedirectToPage("./Login");
                }
                var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email) ?? info.Principal.FindFirstValue(ClaimTypes.Email);
                user = await RegisterExternalUserAsync(externalLoginInfo, emailAddress);
                
                await SignInManager.SignInAsync(user, false);
    
            }else{
                return await base.OnGetAsync();
            }
        }
        
       public override async Task<IdentityUser> RegisterExternalUserAsync(ExternalLoginInfo externalLoginInfo, string emailAddress)
       {
           await IdentityOptions.SetAsync();
    
            var user = new IdentityUser(GuidGenerator.Create(), emailAddress, emailAddress, CurrentTenant.Id);
    
            (await UserManager.CreateAsync(user)).CheckErrors();
            (await UserManager.AddDefaultRolesAsync(user)).CheckErrors();
            user.SetEmailConfirmed(true); // set true to skip email confirmation ste
            
            if (emailAddress == "<admin user email>")
           {
               await UserManager.AddToRoleAsync(user, "admin");// add admin role to admin user
           }
    
            var userLoginAlreadyExists = user.Logins.Any(x =>
                x.TenantId == user.TenantId &&
                x.LoginProvider == externalLoginInfo.LoginProvider &&
                x.ProviderKey == externalLoginInfo.ProviderKey);
    
            if (!userLoginAlreadyExists)
            {
                user.AddLogin(new UserLoginInfo(
                        externalLoginInfo.LoginProvider,
                        externalLoginInfo.ProviderKey,
                        externalLoginInfo.ProviderDisplayName
                    )
                );
    
                (await UserManager.UpdateAsync(user)).CheckErrors();
            }
    
            return user;
       }
    }
    
  • User Avatar
    0
    jpatron created

    Hi,

    As a quick update, I managed to overwrite the Register.cshtml.cs to navigate to the Azure AD B2C page directly. However, I tried the line below to skip the email confirmation process but it is not working:

    user.SetEmailConfirmed(true); // set true to skip email confirmation step
    

    Do you have any suggestions?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Will it work if try this?

  • User Avatar
    0
    jpatron created

    It didn't work, I can see the EmailConfirmed property set to true after that line, but the flag is still set to false in the database when the user is created. The line to set the flag to true seems to be in the right place, so I am not sure what's going on.

    protected virtual async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
    {
        await IdentityOptions.SetAsync();
    
        var emailAddress = info.Principal.Claims.Where(c => c.Type == "signInName").Select(c => c.Value).FirstOrDefault();
        var userName = await GetUserNameFromEmail(emailAddress);
        var user = new IdentityUser(GuidGenerator.Create(), userName, emailAddress, CurrentTenant.Id);
        user.SetEmailConfirmed(true); // set true to skip email confirmation step
    
        (await UserManager.CreateAsync(user)).CheckErrors();
        (await UserManager.SetEmailAsync(user, emailAddress)).CheckErrors();
        (await UserManager.AddLoginAsync(user, info)).CheckErrors();
        (await UserManager.AddDefaultRolesAsync(user)).CheckErrors();
    
        user.Name = info.Principal.FindFirstValue(AbpClaimTypes.Name);
        user.Surname = info.Principal.FindFirstValue(AbpClaimTypes.SurName);
    
        var phoneNumber = info.Principal.FindFirstValue(AbpClaimTypes.PhoneNumber);
        if (!phoneNumber.IsNullOrWhiteSpace())
        {
            var phoneNumberConfirmed = string.Equals(info.Principal.FindFirstValue(AbpClaimTypes.PhoneNumberVerified), "true", StringComparison.InvariantCultureIgnoreCase);
            user.SetPhoneNumber(phoneNumber, phoneNumberConfirmed);
        }
    
        await UserManager.UpdateAsync(user);
    
        return user;
    }
    
  • User Avatar
    0
    jpatron created

    Another problem I am facing is that the page I am redirecting users after authenticating successfully is checking whether _currentUser.IsAuthenticated is true, but the value is false even if I log in as a user who has previously confirmed the email address:

    public IndexModel(IConfiguration configuration, IUserPropertiesAppService userPropertiesAppService, ICurrentUser currentUser, IUrlHelperFactory urlHelperFactory)
    {
        _configuration = configuration;
        _userPropertiesAppService = userPropertiesAppService;
        _currentUser = currentUser;
        _urlHelperFactory = urlHelperFactory;
    }
    
    public async Task<IActionResult> OnGetAsync()
    {
        if (!_currentUser.IsAuthenticated)
        {
            return RedirectToPage("/Account/Login");
        }
        ...
    }
    

    If I look at the state of _currentUser at that point, I get the following:

    If the user logs in using the Azure AD B2C credentials, shouldn't the application be able to get the details from the local user linked to the Azure AD B2C account and populate the CurrentUser instance with it?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    It didn't work, I can see the EmailConfirmed property set to true after that line, but the flag is still set to false in the database when the user is created. The line to set the flag to true seems to be in the right place, so I am not sure what's going on.

    Hi,

    Sorry, I realized that the SetEmailAsync method resets the mail confirmation to false. please move the SetEmailConfirmed to below SetEmailAsync

    (await UserManager.SetEmailAsync(user, emailAddress)).CheckErrors();
    user.SetEmailConfirmed(true);
    
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    When you call the SignInAsync method, the external user will log in to the system

     await SignInManager.SignInAsync(user, false);
    
  • User Avatar
    0
    jpatron created

    It didn't work, I can see the EmailConfirmed property set to true after that line, but the flag is still set to false in the database when the user is created. The line to set the flag to true seems to be in the right place, so I am not sure what's going on.

    Hi,

    Sorry, I realized that the SetEmailAsync method resets the mail confirmation to false. please move the SetEmailConfirmed to below SetEmailAsync

    (await UserManager.SetEmailAsync(user, emailAddress)).CheckErrors(); 
    user.SetEmailConfirmed(true); 
    

    This suggestion worked, the email is confirmed at the moment the local user is created now. I am still having issues with the instance of CurrentUser being null after the user signs in, though.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Could you share a minimal reproducible example? thanks

  • User Avatar
    0
    jpatron created

    Hi,

    For now, the team has decided to rollback the integration with Azure AD B2C. Therefore, this ticket can be closed.

Made with ❤️ on ABP v9.1.0-preview. Updated on November 20, 2024, 13:06