Search code examples
c#asp.net-coreidentityclaims-based-identity

AspNet Identity Core - Custom Claims on Login


I'm trying to extend my identity user by adding a logical 'deleted' column in the database.

I then want to use this value to add a claim to user using a custom UserClaimsPrincipalFactory.

I want to check the 'Deleted' claim on login and reject the user if their account has been deleted.

The problem: When I try and access the claims through User.Claims the user has no claims.

The only was I can make it work is by overwriting the httpcontext user

public class ApplicationClaimsIdentityFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
    private readonly IHttpContextAccessor _httpContext;

    public ApplicationClaimsIdentityFactory(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> options, IHttpContextAccessor httpContext) : base(userManager, roleManager, options)
    {
        _httpContext = httpContext;
    }

    public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
    {
        ClaimsPrincipal principal = await base.CreateAsync(user);

        ClaimsIdentity claimsIdentity = (ClaimsIdentity) principal.Identity;

        claimsIdentity.AddClaim(new Claim("Deleted", user.Deleted.ToString().ToLower()));

        //I DON'T WANT TO HAVE TO DO THIS
        _httpContext.HttpContext.User = principal;


        return principal;
    }
}

Login Action:

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
    {
        ViewData["ReturnUrl"] = returnUrl;
        if (ModelState.IsValid)
        {

            SignInResult result = await _signInManager.PasswordSignInAsync(model.Email, model.Password,
                model.RememberMe, lockoutOnFailure: false);

            if (result.Succeeded)
            {
                //No claims exists at this point unless I force the HttpContext user (See above)
                if (User.Claims.First(x => x.Type == "Deleted").Value.Equals("true", StringComparison.CurrentCultureIgnoreCase);)
                {
                    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                    await _signInManager.SignOutAsync();
                    return View(model);
                }

          .... Continue login code...

My ApplicationUser class

public class ApplicationUser : IdentityUser
{
    public bool Deleted { get; set; }
}

And finally my startup registration

services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<DbContext>()
            .AddClaimsPrincipalFactory<ApplicationClaimsIdentityFactory>()
            .AddDefaultTokenProviders();

Thanks


Solution

  • I think the problem is you are trying to do it all in one request when the login action is posted.

    The User you have in that method is a claimsprincipal but is not authenticated, it was deserialized from the request by the auth middleware before the code to SignIn was invoked and before your claimsprincipal factory method was called.

    The signin method did create the new authenticated claimsprincipal and should have serialized it into the auth cookie, so on the next request the User would be deserialized from the cookie and would be authenticated, but that deserialization already happened for the current request. So the only way to change it for the current request is to reset the User on the current httpcontext as you have found.

    I think it would be better to reject the user a different way not after login success, it would be better to check that at a lower level and make login fail, rather than succeed and then signout. I did this in my project in a custom userstore.