0

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?

Mike
  • 59
  • 2
  • There is also a IClaimsTransformation https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims?view=aspnetcore-7.0, but this will be run on each AuthenticateAsync call. – Damian Kurek Jul 13 '23 at 19:02

2 Answers2

4

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.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • 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(); However, I havent been able to hit any breakpoints within the GetAuthenticationStateAsync() method. – Mike Apr 24 '23 at 14:10
  • 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.. – Mike Apr 24 '23 at 14:17
  • 1
    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. – MrC aka Shaun Curtis Apr 24 '23 at 15:15
  • 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). ??? - TAI – David Thielen May 09 '23 at 19:45
  • Don't know, but as you say there must be limits due to header sizes. I've never added that many! – MrC aka Shaun Curtis May 10 '23 at 11:46
1

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")]
. . .
Todd
  • 99
  • 1
  • 4