Search code examples
c#asp.netblazor-server-sideblazor-client-sideblazor-webassembly

How to authorize user's role in client side of blazor wasm?


Environments

  • Asp.Net Core 5.0
  • Blazor WebAssembly App (Asp.Net Core Hosting)
  • Asp.Net Core Identity (with Identity Server 4)

Problem

I want to use Role-based authorization between Server side and Client side.

I can login correctly and UserManager.IsInRoleAsync(user, "admin") returns True in the Server side.

But neither @attribute [Authorize(Roles = "admin")] nor <AuthorizeView Roles="admin"> doesn't work in the Client side. Also User.Identity.IsInRole("admin") returns False in the Client side.

How can I get the user's role in the Client side?

Codes

Server.csproj

// Startup.ConfigureServices()

services.AddDefaultIdentity<ApplicationUser>(options =>
{
    options.SignIn.RequireConfirmedAccount = true;

    options.Password.RequiredLength = 6;
    options.Password.RequiredUniqueChars = 2;
    options.Password.RequireNonAlphanumeric = false;

    options.User.RequireUniqueEmail = true;
})
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

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

services.AddAuthentication()
    .AddIdentityServerJwt();
// Startup.Configure()

app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
// RolesController.Get()

var userid = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
var currentUser = await userManager.FindByIdAsync(userid);
return await userManager.IsInRoleAsync(currentUser, "admin"); // Returns True

Client.csproj

// Program.Main()

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("WebAppIdentity.ServerAPI"));

builder.Services.AddApiAuthorization();
// Test.razor

<AuthorizeView Roles="admin">
    <Authorizing>
        Authorizing...
    </Authorizing>
    <NotAuthorized>
        You are not an admin. // Always here
    </NotAuthorized>
    <Authorized>
        Hello, admin!
    </Authorized>
</AuthorizeView>

<button @onclick="ShowInfo">Show Info</button>
<p>@infoString</p>

@code
{
    [CascadingParameter]
    private Task<AuthenticationState> stateTask { get; set; }
    private string infoString { get; set; }

    private async void ShowInfo()
    {
        var user = (await stateTask).User;

        infoString = $"Is admin: {user.IsInRole("admin")}"; // Always False
    }
}

Solution

  • There are currently two accepted ways of handling this.

    The first

    #1 Configure Identity to use roles by calling AddRoles

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

    #2 Configure identity server to put the role claim into the id token and the access token and prevent the default mapping for roles in the JwtSecurityTokenHandler.

    services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
            options.IdentityResources["openid"].UserClaims.Add("role");
            options.ApiResources.Single().UserClaims.Add("role");
        });
    
    // Need to do this as it maps "role" to ClaimTypes.Role and causes issues
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
    

    #3 On your blazor application use [Authorize(Roles = "admin")] or any other role your app defines.

    #4 On your protected resource APIs use [Authorize(Roles = "admin")] or any other role your app defines.

    The second

    #1 Add Class to configure options.UserOptions.RoleClaim on the Client

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    using Microsoft.Extensions.Options;
    
    namespace App.Client.Services
    {
        public class ApiAuthorizationOptionsConfiguration
            : IPostConfigureOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>
        {
            public void Configure(RemoteAuthenticationOptions<ApiAuthorizationProviderOptions> options)
            {
                options.UserOptions.RoleClaim ??= "role";
            }
    
            public void PostConfigure(string name, RemoteAuthenticationOptions<ApiAuthorizationProviderOptions> options)
            {
                if (string.Equals(name, Options.DefaultName))
                {
                    Configure(options);
                }
            }
        }
    }
    

    #2 Modify the Program.cs file to call ApiAuthorizationOptionsConfiguration and configure the role claim.

    using App.Client.Services;
    ...
    
    namespace App.Client
    {
        public class Program
        {
            public static async Task Main(string[] args)
            {
                ...
    
                builder.Services.AddAuthorizationCore();
                builder.Services.AddApiAuthorization();
                builder.Services.TryAddEnumerable(
                    ServiceDescriptor.Singleton<
                        IPostConfigureOptions<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>,
                        ApiAuthorizationOptionsConfiguration>());
    
                ...
            }
        }
    }