How do I add custom claims to a ClaimsPrincipal after authentication in a .net 6 blazor application?

3.6k Views Asked by At

https://learn.microsoft.com/en-us/aspnet/core/security/authorization/claims?source=recommendations&view=aspnetcore-7.0

The above article Gives a great explanation of how to leverage the claims by registering policies that map to claims, and using these policies to control access to various endpoints.

However, when it comes to defining the claims, or adding claims to the identity, all the article offers is:

When an identity is created it may be assigned one or more claims issued by a trusted party.

Our claims are accessible via a sql query, and I would like to embed a users claims into their ClaimsPrincipal immediately after authentication.

I've seen examples of modifying the Claims on the identity object through middleware. However, Middleware would be executed on every request and I don't want to have to fetch from the database and modify the claims on every single request.. I'd rather do it once at the outset, and be able to leverage the native authorize attributes for api endpoints and blazor components.

The other examples I've seen involve implementing a custom AccountClaimsPrincipalFactory on the blazor client project. This does provide a way for me to attach my own custom claims once in the CreateUserAsync that would be called once upon authenticating. However, when it comes to injecting the dependencies that would allow me to make the necessary api call to fetch the claims, I can't implement my own constructor without breaking the application. An from other posts I've seen, there seem to be challenges making http calls from within this method beyond dependency injection.

Can anyone point me in the direction of any implementations to add custom claims once and only once in a .net core blazor application?

2

There are 2 best solutions below

5
MrC aka Shaun Curtis On

You can create a custom AuthenticationStateProvider like this:

The User is a ClaimsPrincipal which can have as many ClaimsIdentity objects as you wish.

public class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider
{
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var authState = await base.GetAuthenticationStateAsync();
        var user = authState.User;
        
        // Get your custom data - in this case some roles

        // add some new identities to the Claims Principal
        user.AddIdentity(new ClaimsIdentity(new List<Claim>() { new Claim(ClaimTypes.Role, "Admin") }));
        user.AddIdentity(new ClaimsIdentity(new List<Claim>() { new Claim(ClaimTypes.Role, "User") }));

        // return the modified principal
        return await Task.FromResult(new AuthenticationState(user));
    }
}

And register last in Program.

0
Todd On

The various types involved did make this a little tricky in a non-hosted standalone Blazor WebAssembly app. Here is my entire AuthenticationStateProvider subclass:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

namespace BlazorWasmOIDC {

    public class CustomAuthenticationStateProvider : RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions> {

        [Obsolete]
        public CustomAuthenticationStateProvider(IJSRuntime jsRuntime, IOptionsSnapshot<RemoteAuthenticationOptions<OidcProviderOptions>> options, NavigationManager navigation, AccountClaimsPrincipalFactory<RemoteUserAccount> accountClaimsPrincipalFactory) : 
            base(jsRuntime, options, navigation, accountClaimsPrincipalFactory) {}

        public CustomAuthenticationStateProvider(IJSRuntime jsRuntime, IOptionsSnapshot<RemoteAuthenticationOptions<OidcProviderOptions>> options, NavigationManager navigation, AccountClaimsPrincipalFactory<RemoteUserAccount> accountClaimsPrincipalFactory, ILogger<RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>> logger) : 
            base(jsRuntime, options, navigation, accountClaimsPrincipalFactory, logger) {}

        public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
            var authState = await base.GetAuthenticationStateAsync();
            var user = authState.User;

            AccessTokenResult tokenResult = await base.RequestAccessToken();

            if (tokenResult.TryGetToken(out AccessToken token)) {
                JwtSecurityToken jwt = new JwtSecurityToken(jwtEncodedString: token.Value);
                foreach (Claim claim in jwt.Claims) {
                    if (claim.Type == "custom_user") { user.AddIdentity(new ClaimsIdentity(new List<Claim>() { claim })); }
                    if (claim.Type == "custom_admin") { user.AddIdentity(new ClaimsIdentity(new List<Claim>() { claim })); }
                }
            }
            return (await Task.FromResult(new AuthenticationState(user)));
        }

    }
}

I then register it like this in Program.cs:

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();

For the curious (going beyond the answer), in my case, I am doing this in order to parse the raw, underlying JWT and forward on custom claims from it. I can then use Blazor's built in "Policy" and "Authorize attribute" mechanisms with my custom claims to control authorization (access to various resources in my app). For this I add the following to Program.cs:

builder.Services.AddAuthorizationCore(options => {
    options.AddPolicy("IsCustomUser", policy => policy.RequireClaim("custom_user", "true"));
    options.AddPolicy("IsCustomAdmin", policy => policy.RequireClaim("custom_admin", "true"));
});

And then I can restrict access like this (using the standard "Counter" demo razor as an example):

@page "/counter"
@using Microsoft.AspNetCore.Authorization;
@attribute [Authorize(Policy = "IsCustomUser")]
. . .