Asp.Net Core 3 Identity - Custom Claims not present in JWT from browser

Viewed 3670

Asp.Net Core 3.0
I am using the ASP.NET Core web application with Angular and Authentication (Individual User Accounts) template (from Visual Studio 2019).
My intention is to add some Custom Claims in the generated JWT and use them in browser.
In order to do that, I have extended the UserClaimsPrincipalFactory

public class MyCustomClaimsInjector : UserClaimsPrincipalFactory<ApplicationUser>
{
    public MyCustomClaimsFactory(UserManager<ApplicationUser> userManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, optionsAccessor)
    {
    }

    protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
    {
        var id = await base.GenerateClaimsAsync(user);
        id.AddClaim(new Claim("my_claim1", "AdditionalClaim1"));
        id.AddClaim(new Claim("my_claim2", "AdditionalClaim2"));
        return id;
    }
}

As well, I have registered the extension in the Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        services.AddDefaultIdentity<ApplicationUser>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddClaimsPrincipalFactory<MyCustomClaimsFactory>();


        services.AddIdentityServer()
            .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

        services.AddAuthentication()
            .AddIdentityServerJwt();
        services.AddControllersWithViews();
        services.AddRazorPages();
        // In production, the Angular files will be served from this directory
        services.AddSpaStaticFiles(configuration =>
        {
            configuration.RootPath = "ClientApp/dist";
        });
    }

During Sign In phase, started from the SPA client, the debugger passes through MyCustomClaimsFactory and adds the claims to the ClaimsIdentity in the GenerateClaimsAsync method.

But, I find strange why the JWT received in browser does not contain the Claims added by the MyCustomClaimsFactory.

Is my expectation to see the Custom Claim in the JWT in browser OK ?

Can anyone suggest the direction to dig in... Why the claims isn't present in the JWT ?

Decoded JWT is: enter image description here

The SPA app: enter image description here

2 Answers

Will share my results. Hope that will help anyone else.

I have implemented IProfileService and piped the .AddProfileService<ProfileService>()implementation in ConfigureServices.

public class ProfileService : IProfileService
{
    protected UserManager<ApplicationUser> _userManager;

    public ProfileService(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {         
        var user = await _userManager.GetUserAsync(context.Subject);

        var claims = new List<Claim>
        {
            new Claim("my_FirstName", "user_FirstName"),
            new Claim("my_LastName", "user_LastName")
        };

        context.IssuedClaims.AddRange(claims);
    }

    public async Task IsActiveAsync(IsActiveContext context)
    {            
        var user = await _userManager.GetUserAsync(context.Subject);

        context.IsActive = (user != null);
    }
}

the Startup.cs file

services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
        .AddProfileService<ProfileService>();

With that, now the JWT contains my custom claims.
I am not sure why the override for UserClaimsPrincipalFactory was not able to solve that.
Will try to study deeper those areas.

So today I encountered the same problem. I haven't used IdentityServer before so I'm not totally sure if the following is the correct way of doing it, but it is certainly easier than creating your own IProfileService implementation.

The default profile service calls

context.AddRequestedClaims(principal.Claims);

The requested claims are defined on the IdentityServer ApiResource that you are accessing. This ApiResource configuration is created by calling

services.AddAuthentication()
        .AddIdentityServerJwt();

This creates a default ApiResource configuration with the name "{Environment.ApplicationName}API". This object also holds a collection of claim types to include when generating the JWT.

I couldn't find any documentation on whether this could also be set in the appsettings.json, but you can access it in the startup code.

TLDR; Change your startup code to something similar to this:

Startup.cs:

public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment environment)
    {
        Configuration = configuration;
        Environment = environment;
    }

    public IConfiguration Configuration { get; }
    public IWebHostEnvironment Environment { get; }

    // ...

    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddIdentityServer()
            .AddApiAuthorization<User, ApplicationDbContext>(options =>
            {
                // The options.ApiResources collection is automatically populated
                // by services.AddAuthentication().AddIdentityServerJwt();
                var apiResource = options.ApiResources[$"{Environment.ApplicationName}API"];
                // Example: add the user's roles to the token
                apiResource.UserClaims.Add(JwtClaimTypes.Role);
                // Example: add another custom claim type
                apiResource.UserClaims.Add("CustomClaimName");
            });

        services.AddAuthentication()
            .AddIdentityServerJwt();

        // ...
    }
}
Related