I have a .NET 5.0 Blazor client app and I am unable to get the [Authorize(Roles="Admin")]
and AuthorizeView
tag to work.
I have scaffolded identity pages as well:
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:
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();
}
}
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
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.
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:
This shows which roles have been passed in the authentication data in the header from the authentication provider. This should include role.
Remove:
// Need to do this as it maps "role" to ClaimTypes.Role and causes issues
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");