Search code examples
authenticationblazorroles

Blazor authentication role based


I'm working on a client-side blazor application with the last webassembly version (3.2.0).

I started the project from the visual tool with enabling local authentications and I tried to add roles.

First, I added the roles in the ApplicationDbContext :

public class ApplicationDbContext : ApiAuthorizationDbContext<ApplicationUser>
{
    public ApplicationDbContext(
        DbContextOptions options,
        IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity<IdentityRole>()
               .HasData(new IdentityRole { Name = "User", NormalizedName = "USER", Id = Guid.NewGuid().ToString(), ConcurrencyStamp = Guid.NewGuid().ToString() });
        builder.Entity<IdentityRole>()
               .HasData(new IdentityRole { Name = "Admin", NormalizedName = "ADMIN", Id = Guid.NewGuid().ToString(), ConcurrencyStamp = Guid.NewGuid().ToString() });
    }
}

Then I added Roles to the IdentityBuilder in the startup class :

public void ConfigureServices(IServiceCollection services)
{
   services.AddDbContext<ApplicationDbContext>(options =>
       options.UseSqlServer(
           Configuration.GetConnectionString("DefaultConnection")));

   services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
       .AddRoles<IdentityRole>()
       .AddEntityFrameworkStores<ApplicationDbContext>();

   services.AddIdentityServer()
       .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

   services.AddAuthentication()
       .AddIdentityServerJwt();

   services.AddControllersWithViews();
   services.AddRazorPages();
}

And then in my DbInitializer I created an Admin account with both roles :

    private async Task SeedASPIdentityCoreAsync()
    {
        if (!await context.Users.AnyAsync())
        {
            var admin = new ApplicationUser()
            {
                UserName = "[email protected]",
                Email = "[email protected]",
                EmailConfirmed = true,
            };
            var result = await userManager.CreateAsync(admin, "aA&123");
            if (!result.Succeeded)
            {
                throw new Exception(result.Errors.First().Description);
            }

            result = await userManager.AddClaimsAsync(admin, new Claim[]
                {
                    new Claim(JwtClaimTypes.Email, "[email protected]"),
                    new Claim(JwtClaimTypes.Name, "[email protected]")
                });


            ApplicationUser user = await userManager.FindByNameAsync("[email protected]");

            try
            {
                result = await userManager.AddToRoleAsync(user, "User");
                result = await userManager.AddToRoleAsync(user, "Admin");
            }
            catch
            {
                await userManager.DeleteAsync(user);
                throw;
            }

            if (!result.Succeeded)
            {
                await userManager.DeleteAsync(user);
                throw new Exception(result.Errors.First().Description);
            }
        }
    }

But the roles doen't appear in the JWT, and the client-side has no idea about the roles.

How can I add the roles in the JWT, as with the new version of blazor, there is no need of the LoginController ? (If i well understood the changes)


Solution

  • Ok I found what I needed :

    1) Create a CustomUserFactory in your client App

    using System.Linq;
    using System.Security.Claims;
    using System.Text.Json;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
    
    public class CustomUserFactory
        : AccountClaimsPrincipalFactory<RemoteUserAccount>
    {
        public CustomUserFactory(IAccessTokenProviderAccessor accessor)
            : base(accessor)
        {
        }
    
        public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
            RemoteUserAccount account,
            RemoteAuthenticationUserOptions options)
        {
            var user = await base.CreateUserAsync(account, options);
    
            if (user.Identity.IsAuthenticated)
            {
                var identity = (ClaimsIdentity)user.Identity;
                var roleClaims = identity.FindAll(identity.RoleClaimType);
    
                if (roleClaims != null && roleClaims.Any())
                {
                    foreach (var existingClaim in roleClaims)
                    {
                        identity.RemoveClaim(existingClaim);
                    }
    
                    var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
    
                    if (rolesElem is JsonElement roles)
                    {
                        if (roles.ValueKind == JsonValueKind.Array)
                        {
                            foreach (var role in roles.EnumerateArray())
                            {
                                identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
                            }
                        }
                        else
                        {
                            identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
                        }
                    }
                }
            }
    
            return user;
        }
    }
    

    2) Register the client factory

    builder.Services.AddApiAuthorization()
    .AddAccountClaimsPrincipalFactory<CustomUserFactory>();
    

    3) In the Server App, call IdentityBuilder.AddRoles

    services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
    

    4) Configure Identity Server

    services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
        options.IdentityResources["openid"].UserClaims.Add("name");
        options.ApiResources.Single().UserClaims.Add("name");
        options.IdentityResources["openid"].UserClaims.Add("role");
        options.ApiResources.Single().UserClaims.Add("role");
    });
    
    
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
    

    There is an other way, by creating a ProfileService

    5) User Authorization mechanisms :

    <AuthorizeView Roles="admin">
    

    Source : https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/security/blazor/webassembly/hosted-with-identity-server.md#Name-and-role-claim-with-API-authorization