Reputation: 688
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
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
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
Upvotes: 2
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