Search code examples
c#asp.netidentityserver4

IdentityServer4 and ASP.Net Identity: Adding additional claims


I'm trying to use IdentityServer4 with ASP.Net Identity Core providing a user store. I've loosely followed this guide: https://www.scottbrady91.com/Identity-Server/Getting-Started-with-IdentityServer-4, and have gotten to a point where I can register and authenticate users locally using ASP.Net Identity.

My problem is that I add some claims on registration:

var createUserResult = await _userManager.CreateAsync(user, model.Password);

if (createUserResult.Succeeded)
{
    var claims = new List<Claim>
    {
        new Claim(JwtClaimTypes.Email, user.Email),
        new Claim(JwtClaimTypes.Role, "user"),
        new Claim("test-claim", "loremipsum")
    };

    await _userManager.AddClaimsAsync(user, claims);

    await HttpContext.SignInAsync(user.Id, user.UserName, new AuthenticationProperties());
    return Redirect(model.ReturnUrl ?? "~/");
}

These are saved correctly, and I can see them in the database. However once I login, the user object only has the following claims:

  • sub
  • name
  • auth_time
  • idp
  • amr

Looking online, it seems that I need to override the UserClaimsPrincipalFactory and register this implementation with the DI container:

public class CustomUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
    public CustomUserClaimsPrincipalFactory(
        UserManager<ApplicationUser> userManager,
        RoleManager<IdentityRole> roleManager,
        IOptions<IdentityOptions> options) : base(userManager, roleManager, options)
    {
        Console.WriteLine("Test");
    }

    public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
    {
        var principal = await base.CreateAsync(user);
        ((ClaimsIdentity) principal.Identity).AddClaim(new Claim("yolo", "swag"));

        return principal;
    }

    protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
    {
        var identity = await base.GenerateClaimsAsync(user);
        identity.AddClaim(new Claim("yolo", "swag"));

        return identity;
    }
}

Registered in the DI container as follows:

services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsPrincipalFactory>();

My understanding is that this custom principal factory should add the claims I want to the User object, but this doesn't happen. I can see the custom claims principal factory is instantiated using a breakpoint in the constructor, but the GenerateClaimsAsync function is never called.

IdentityServer and ASP.Net identity are registered as follows:

services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

services.AddIdentityServer(config =>
    {
        config.PublicOrigin = _configuration.PublicOriginUrl;
    })
    .AddOperationalStore(options =>
    {
        options.ConfigureDbContext = builder => builder.UseNpgsql(
            _sqlConnectionString,
            sqlOptions => sqlOptions.MigrationsAssembly(MigrationsAssembly));
    })
    .AddConfigurationStore(options =>
    {
        options.ConfigureDbContext = builder => builder.UseNpgsql(
            _sqlConnectionString,
            sqlOptions => sqlOptions.MigrationsAssembly(MigrationsAssembly));
    })
    .AddAspNetIdentity<ApplicationUser>()
    .AddDeveloperSigningCredential();

My custom user claims principal factory is registered after this section. Looking at the AddAspNetIdentity extension method here, I can see it seems to register some sort of custom user claims principal factory, but I don't understand the implementation code well enough to work out exactly what's happening.

How can I implement a custom user claims principal factory such that I could do something like this in a view?

// "yolo" is a custom claim
@User.FindFirst("yolo")

Solution

  • I worked this out (at least, in a way that satisfies my requirements). The logic that performed the login in the quickstart project looked like this:

    ...
    if (user != null && await _userManager.CheckPasswordAsync(user, model.Password)) {
        await _events.RaiseAsync(
            new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName));
    
    
        AuthenticationProperties props = null;
        if (AccountOptions.AllowRememberLogin && model.RememberLogin) {
            props = new AuthenticationProperties {
                IsPersistent = true,
                ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
            };
        };
    
        await HttpContext.SignInAsync(user.Id, user.UserName, props);
        ...
    }
    

    By looking at the extension methods that IdentityServer4 adds to the HttpContext, I found that there were several that accept an array of claims.

    I modified the login logic in my account controller:

    ...
    var userClaims = await _userManager.GetClaimsAsync(user);
    await HttpContext.SignInAsync(user.Id, user.UserName, props, userClaims.toArray());
    ...
    

    This successfully adds all the user's claims from the database to the current session. From here I was able to select only the claims I wanted, and pass an array of these instead.

    Now that the desired claims are stored as part of the session, I'm able to call @User.FindFirst("yolo") from a view, or other controllers, as I require.