Blazor wasm get additional information and add to user claims

Viewed 966

I am using identityserver4 for authentication and it's laid out something like this: identity server4 -> Web Api -> Blazor WASM Client(Standalone). everything is getting authenticated and working great. I get the authenticated user claims all the way to the wasm client. I am now trying to add more claims which come directly from the database. I could have added the claims to the identityserver token but the token gets too big (> 2kb) and then identityserver stops working. apparently this is a known issue.

So iwant to build authorization and trying to keep the jwt token from identityserver small.

in the program.cs file i have a http client like so

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

builder.Services.AddOidcAuthentication(options =>// Configure your authentication provider options here. // For more information, see https://aka.ms/blazor-standalone-auth //builder.Configuration.Bind("Local", options.ProviderOptions); ... provider options

            options.ProviderOptions.ResponseType = "code";
            options.UserOptions.RoleClaim = "role";                
        }).AddAccountClaimsPrincipalFactory<CustomAccountClaimsPrincipalFactory>();

await builder.Build().RunAsync();

in the file CustomAccountClaimsPrincipalFactory i have this

  public class CustomAccountClaimsPrincipalFactory
: AccountClaimsPrincipalFactory<RemoteUserAccount>
{
    private const string Planet = "planet";

    [Inject]
    public HttpClient Http { get; set; }

    public CustomAccountClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor)
        : base(accessor) {            
    }

    public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
        RemoteUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var user = await base.CreateUserAsync(account, options);

        if (user.Identity.IsAuthenticated)
        {
            var identity = (ClaimsIdentity)user.Identity;
            var claims = identity.Claims.Where(a => a.Type == Planet);
            if (!claims.Any())
            {
                identity.AddClaim(new Claim(Planet, "mars"));
            }

            //get user roles
           
            //var url = $"/Identity/users/112b7de8-614f-40dc-a9e2-fa6e9d2bf85a/roles";                
            var dResources = await Http.GetFromJsonAsync<List<somemodel>>("/endpoint");
            foreach (var item in dResources)
            {
                identity.AddClaim(new Claim(item.Name, item.DisplayName));
            }


        }
        return user;
    }
}

this is not working as the httpclient is not biolt when this is called and the http client uses the same builder which is building the base http client.

How do i get this to work?

2 Answers

You can create a IProfileService and customise it however you need:


var builder = services.AddIdentityServer(options =>
...
                .AddProfileService<IdentityProfileService>();
    public class IdentityProfileService : IProfileService
    {

        private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
        private readonly UserManager<ApplicationUser> _userManager;

        public IdentityProfileService(IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory, UserManager<ApplicationUser> userManager)
        {
            _claimsFactory = claimsFactory;
            _userManager = userManager;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            if (user == null)
            {
                throw new ArgumentException("");
            }

            var principal = await _claimsFactory.CreateAsync(user);
            var claims = principal.Claims.ToList();

            //Add more claims like this
            //claims.Add(new System.Security.Claims.Claim("MyProfileID", user.Id));

            context.IssuedClaims = claims;
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            context.IsActive = user != null;
        }
    }

Keep the access token small and only include the necessary claims to get past the JwtBearer authentication step.

Then in the API that receives an access token, you can simply create an authorization policy that do lookup the users additional claims and evaluate if he have access or not.

You can do that in the simple policy definitions or the more advanced authorization handlers like the code below:

 public class CheckIfAccountantHandler : AuthorizationHandler<CanViewReportsRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   CanViewReportsRequirement requirement)
    {
        
      bool result = CallTheCheckIfAccountantService();

      if(result)
                context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

A sample requirement can be defined as:

public class CanViewReportsRequirement : IAuthorizationRequirement
{
    public int StartHour { get; }
    public int EndHour { get; }

    public CanViewReportsRequirement(int startHour, int endHour)
    {
        StartHour = startHour;
        EndHour = endHour;
    }
}

The important thing is to keep the complexity of the application low and not try to make it harder than it has to be. Just to make the system easy to reason about!

Related