Search code examples
authorizationblazor.net-5blazor-webassemblyasp.net-authorization

Authorize with roles is not working in .NET 5.0 Blazor Client app


I have a .NET 5.0 Blazor client app and I am unable to get the [Authorize(Roles="Admin")] and AuthorizeView tag to work.

enter image description here

I have scaffolded identity pages as well:

enter image description here

I am using a custom identity implementation that uses Cosmos Db: https://github.com/pierodetomi/efcore-identity-cosmos

I know that Authorization with roles in the Blazor client project template is an issue: https://github.com/dotnet/AspNetCore.Docs/issues/17649#issuecomment-612442543

I tried workarounds as mentioned in the above Github issue thread and the following SO answer: https://stackoverflow.com/a/64798061/6181928

...still, I am unable to get it to work.

Ironically, the IsInRoleAsync method is not even called after logging in to the application. I have applied a breakpoint on its implementation in the custom CosmosUserStore class and it doesn't get hit.

The browser console shows this after logging in to the application with the admin user:

enter image description here

Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDatabaseDeveloperPageExceptionFilter();

        services.AddCosmosIdentity<MyDbContext, IdentityUser, IdentityRole>(
          // Auth provider standard configuration (e.g.: account confirmation, password requirements, etc.)
          options => options.SignIn.RequireConfirmedAccount = true,
          options => options.UseCosmos(
              "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
              "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
              databaseName: "xxxxxxxxxxxxxxxxxxxxxxxxx"
          ),
          addDefaultTokenProviders: true
        ).AddDefaultUI().AddRoles<IdentityRole>();

        services.AddScoped<IUsersRepository, UsersRepository>();

        services.AddIdentityServer().AddApiAuthorization<IdentityUser, MyDbContext>(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");

        services.AddAuthentication()
            .AddIdentityServerJwt();

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

Program.cs

    public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("#app");

        builder.Services.AddHttpClient("IdentityDocApp.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
            .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

        // Supply HttpClient instances that include access tokens when making requests to the server project
        builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("IdentityDocApp.ServerAPI"));

        builder.Services.AddHttpClient();
        builder.Services.AddScoped<IManageUsersService, ManageUsersService>();
        builder.Services.AddBlazorTable();

        builder.Services.AddApiAuthorization();
        builder.Services.AddApiAuthorization(options =>
        {
            options.UserOptions.RoleClaim = "role";
        });

        await builder.Build().RunAsync();
    }
}

App.razor enter image description here

NavMenu.razor

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
    <li class="nav-item px-3">
        <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
            <span class="oi oi-home" aria-hidden="true"></span> Home
        </NavLink>
    </li>
    <AuthorizeView Roles="Admin">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="users">
                <span class="oi oi-person" aria-hidden="true"></span> Users
            </NavLink>
        </li>
    </AuthorizeView>
</ul>

ManageUsers.razor

enter image description here

ManageUsersController enter image description here

The database has the right data in the UserRoles collection. No issues there.

So, what could be the issue? What am I doing wrong?

Update:

It is embarrassing but my IsInRoleAsync implementation in the custom user store was not correct. As soon as I fixed it the issue was gone.

I am only using the following code in the Startup.cs of the server side:

    services.AddIdentityServer()
        .AddApiAuthorization<IdentityUser, MyDbContext>(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");

In the Program.cs of client-side I am only using builder.Services.AddApiAuthorization();

Thanks to @MrC aka Shaun Curtis for letting me know that the issue lied on the server-side.


Solution

  • Paste this into your Index page so you can see the information for your user:

    @if (user is not null)
    {
        <h3>@user.Identity.Name</h3>
        <div class="m-2 p-2">
            Is Authenticated: @user.Identity.IsAuthenticated
        </div>
        <div class="m-2 p-2">
            Authentication Type: @user.Identity.AuthenticationType
        </div>
        <div class="m-2 p-2">
            Admin Role: @user.IsInRole("Admin")
        </div>
        <div class="m-2 p-2">
            <h5>Claims</h5>
            @foreach (var claim in user.Claims)
            {
                <span>
                    @claim.Type
                </span>
                <span>:</span>
                <span>
                    @claim.Value
                </span>
                <br />
            }
        </div>
    }
    else
    {
        <div class="m-2 p-2">
            No User Exists
        </div>
    }
    
    @code {
        [CascadingParameter] public Task<AuthenticationState> AuthTask { get; set; }
    
        private System.Security.Claims.ClaimsPrincipal user;
    
        protected async override Task OnInitializedAsync()
        {
            var authState = await AuthTask;
            this.user = authState.User;
        }
    }
    

    You should get something like this:

    enter image description here

    This shows which roles have been passed in the authentication data in the header from the authentication provider. This should include role.

    Update

    Remove:

    // Need to do this as it maps "role" to ClaimTypes.Role and causes issues
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");