1

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.

Junaid
  • 941
  • 2
  • 14
  • 38
  • Please add your `App.razor` to your question. – MrC aka Shaun Curtis Jan 03 '22 at 09:46
  • @MrCakaShaunCurtis `App.razor` is added the question. – Junaid Jan 03 '22 at 10:11
  • Thanks. Are you hitting in `App` after you return to your app from the log in process? Or what page are you landing on? – MrC aka Shaun Curtis Jan 03 '22 at 10:33
  • Thanks, @MrCakaShaunCurtis. It lands on `Index.razor` after logging in with admin user. Screenshot: https://ibb.co/Wthd42b When I attempt to access "/users" which contains Authorize and AuthorizeView tags, it gives "You are not authorized to access this resource." Screenshot: https://ibb.co/SnVnqB9 – Junaid Jan 03 '22 at 10:47
  • See answer for more testing. It's not an answer, but you can't put code in a comment! – MrC aka Shaun Curtis Jan 03 '22 at 11:09
  • Do you have `ApplicationUser` defined in your app ? If yes, it should be used instead of IdentityUser. Do you configure Identity Server middleware (`app.UseIdentityServer();`). Do you happen to implement AccountClaimsPrincipalFactory ? If not, you may need to do so. – enet Jan 04 '22 at 04:15
  • @enet I do not have `ApplicationUser`. Already have `app.UseIdentityServer();` configured. Are you referring to `UserClaimsPrincipalFactory` ? – Junaid Jan 04 '22 at 07:00
  • `AccountClaimsPrincipalFactory` See: https://www.youtube.com/watch?v=kpC4SfOzDbw&ab_channel=Blazor%2C.NET%2CC%23%2CAzure%26Unity3d – enet Jan 04 '22 at 07:28
  • oh.. I already have `public class CustomUserFactory : AccountClaimsPrincipalFactory` added in my project. This is inside client project but my problem lies on server end. – Junaid Jan 04 '22 at 07:56
  • Remove ` builder.Services.AddHttpClient();` and `builder.Services.AddApiAuthorization();` You already have them, and test. – enet Jan 04 '22 at 08:10
  • `builder.Services.AddApiAuthorization().AddAccountClaimsPrincipalFactory();` what should I test after removing this? Lol – Junaid Jan 04 '22 at 08:14

1 Answers1

3

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");
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Thanks. I have tried it and got this: https://ibb.co/YjLLPJC The admin role checking call returns false. – Junaid Jan 03 '22 at 13:12
  • 1
    See updated answer – MrC aka Shaun Curtis Jan 03 '22 at 13:28
  • Just removed the said line and tried again. Unfortunately, it had no effect. I got the same result as in my previous comment. The "role" is still not a part of the claims list. – Junaid Jan 03 '22 at 13:34
  • So the problem is with Authentication, not Authorization. Your JWT authentication Issuer is not adding the role data to the JWT token. Likely to be the Cosmos DB interface. At this point I bow out as I have no experience with it! – MrC aka Shaun Curtis Jan 03 '22 at 16:37
  • I understand. Basically, the "role" claim is not a part of the jwt token being returned by the Server app. Can you please let me know which interface from asp.net identity is responsible for adding the "role" claim? – Junaid Jan 03 '22 at 19:04
  • @MrC aka Shaun Curtis: `user is not null` . I think user is always not null – enet Jan 04 '22 at 04:17
  • My implementation of the custom user store on the server-side was wrong. Thanks, @MrCakaShaunCurtis for letting me know where to look for the issue. – Junaid Jan 04 '22 at 10:47