3
votes

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

2

2 Answers

3
votes

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.

1
votes

The claim is not available right after the PasswordSignInAsync method. One workaround you can use it to add the UserManager in your Constructor like:

    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly ILogger<LoginModel> _logger;
    private readonly UserManager<ApplicationUser> _userManager; //<----here

    public LoginModel(SignInManager<ApplicationUser> signInManager, 
        UserManager<ApplicationUser> userManager, //<----here
        ILogger<LoginModel> logger)
    {
        _signInManager = signInManager;
        _userManager = userManager;//<----here
        _logger = logger;
    }

and in the Login method, you can check the IsDeleted Property like:

 var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
            if (result.Succeeded)
            {
                var user =  await _userManager.FindByNameAsync(Input.Email); //<----here
                if (user.IsDeleted)  //<----here
                {
                    ModelState.AddModelError(string.Empty, "This user doesn't exist.");

                    await _signInManager.SignOutAsync();

                    return Page();
                }


                _logger.LogInformation("User logged in.");
                return LocalRedirect(returnUrl);
            }

This is my first stack overflow answer :)