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

2

6

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?

Elmer answered 21/4, 2023 at 17:51 Comment(1)
There is also a IClaimsTransformation learn.microsoft.com/en-us/aspnet/core/security/authentication/…, but this will be run on each AuthenticateAsync call.Genteelism
P
11

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.

Pullet answered 21/4, 2023 at 22:5 Comment(5)
Thanks for the response. When you say: "And register last in Program", do you have any sample code. I tried to register in Startup, alongside other service registers, like this: services.AddScoped<ServerAuthenticationStateProvider, CustomAuthenticationStateProvider>(); However, I havent been able to hit any breakpoints within the GetAuthenticationStateAsync() method.Elmer
Also, I'm now noticing that the ServerAuthenticationStateProvider has the description: "An AuthenticationStateProvider intended for use in server-side Blazor." My application is a Blazor WASM app, so it could be the case that registering an implementation of this class is being ignored anyway..Elmer
For WASM, I think you need to inherit from the RemoteAuthenticationStateProvider I'm travelling at the moment do don't have access to test. By register last, I mean register it as the last service so it overloads the standard one registered.Pullet
Is there any limit to the number of claims for Blazor server? I know for some ASP.NET systems the claims are being passed to the client and if you have too many, you hit limits in the size of the request headers (or somewhere). ??? - TAIGastrotomy
Don't know, but as you say there must be limits due to header sizes. I've never added that many!Pullet
R
5

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")]
. . .
Reft answered 25/7, 2023 at 22:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.