BnMcG
BnMcG

Reputation: 688

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:

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")

Upvotes: 2

Views: 6529

Answers (2)

Shriram Navaratnalingam
Shriram Navaratnalingam

Reputation: 3187

I also implemented similar functionality to set claims for a user.

Claim claim = new Claim(claimType, claimValue, ClaimValueTypes.String);
IdentityResult result = await userManager.AddClaimAsync(user, claim);

This add the Claims for the particular user in AspNetUserClaims table

enter image description here

Microsoft.AspNetCore.Identity.SignInManager provides the inbuilt feature for Authentication.

This also automatically creates claims for user when logged in. Following is a sample login using SignInManager

[HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Login(Login login)
    {
        if (ModelState.IsValid)
        {
            AppUser appUser = await userManager.FindByEmailAsync(login.Email);
            if (appUser != null)
            {
                await signInManager.SignOutAsync();
                Microsoft.AspNetCore.Identity.SignInResult result = await signInManager.PasswordSignInAsync(appUser, login.Password, false, false);
                if (result.Succeeded)
                    return Redirect(login.ReturnUrl ?? "/");
            }
            ModelState.AddModelError(nameof(login.Email), "Login Failed: Invalid Email or password");
        }
        return View(login);
    }

After user is successfully logged in we can access all the claims in view using Microsoft.AspNetCore.Mvc.Razor.RazorPageBase.User

Following is the sample code to retrieve all the claims:

     <h2>Claim details</h2>
     <ul>
        @foreach (var claim in User.Claims)
        {
           <li><strong>@claim.Type</strong>: @claim.Value</li>
        }
     </ul>

I followed this tutorial for Identity claims, where it includes managing custom claims as well

  1. https://www.yogihosting.com/aspnet-core-identity-claims/
  2. https://code-maze.com/authentication-aspnet-core-identity/

Upvotes: 2

BnMcG
BnMcG

Reputation: 688

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.

Upvotes: 2

Related Questions