Search code examples
asp.net-mvcauthenticationasp.net-identityasp.net-identity-2claims

Why after adding a claim in login action, it can’t be accessed in other controllers?


I'm working on a system that uses ASP.NET MVC 5 and Identity 2 with Entity Framework 6. When a user logs in, I add some claims to that login session. I don’t want to use the claims table.

For one of my claims, I did like this:

public class User : IdentityUser<int, UserLogin, UserRole, UserClaim>
{
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<User, int> manager)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);

        //We add the display name so that the _LoginPartial can pick it up;
        userIdentity.AddClaim(new Claim("DisplayName", FirstName + " " + LastName));

        // Add custom user claims here
        return userIdentity;
    }
    public virtual ICollection<UserInsurance> UserInsurances { get; set; }
    public User()
    {
        UserInsurances = new List<UserInsurance>();
    }
}

And for accessing the claim:

var claimsIdentity = User.Identity as System.Security.Claims.ClaimsIdentity;
var displayNameClaim = claimsIdentity != null
    ? claimsIdentity.Claims.SingleOrDefault(x => x.Type == "DisplayName")
    : null;
var nameToDisplay = displayNameClaim == null ? User.Identity.Name : displayNameClaim.Value;

This works well. But the problem is when I need a field that is not in the User table. In fact, it is one record in the user’s navigation property (UserInsurances) and I need a linq query for access that.

var lastUserInsurance = UserInsurances.OrderByDescending(x => x.CompanyInsuranceId).First();
userIdentity.AddClaim(new Claim("CompanyInsuranceId", lastUserInsurance.CompanyInsuranceId.ToString()));

If I put this code in GenerateUserIdentityAsync method like “DisplayName”, UserInsurances is null. So I should add this code to login action and after the user logs in successfully. But I tried that and it doesn’t work. I don’t know why but when I want to access that claim, it does not exist.

public virtual async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }
        var result = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false);

        switch (result)
        {
            case SignInStatus.Success:
                var user = _user.Include(x => x.UserInsurances).FirstOrDefault(x => x.NationalCode == model.UserName);
                var identity = await SignInManager.CreateUserIdentityAsync(user);
                var lastUserInsurance = user.UserInsurances.OrderByDescending(x => x.CompanyInsuranceId).FirstOrDefault();
                identity.AddClaim(new Claim("CompanyInsuranceId", lastUserInsurance.CompanyInsuranceId.ToString()));
                return RedirectToLocal(returnUrl);
            case SignInStatus.LockedOut:
                return View("Lockout");
            case SignInStatus.RequiresVerification:
                return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
            case SignInStatus.Failure:
            default:
                return View(model);
        }
    }

Can anyone tell me why I can’t access this claim and it does not exist? I don’t know how I implement this scenario and access “CompanyInsuranceId” claim in all parts of my application.


Solution

  • You must add your claims before sign the user in. So if for any reason you can not fill your claims in GenerateUserIdentityAsync method. Simply generate Identity object in the log in action method then sign in it. Consider this example:

    public async Task<ActionResult> Login(LoginViewModel model,string returnUrl)
    {
        var user = UserManager.Find(model.Email, model.Password);
        // now you have the user object do what you to gather claims
    
        if(user!=null)
        {
            var ident = UserManager.CreateIdentity(user, 
                DefaultAuthenticationTypes.ApplicationCookie);
                ident.AddClaims(new[] {
                    new Claim("MyClaimName","MyClaimValue"),
                    new Claim("YetAnotherClaim","YetAnotherValue"),
            });
            AuthenticationManager.SignIn(
                new AuthenticationProperties() { IsPersistent = true }, 
                ident);
            return RedirectToLocal(returnUrl);
        }
        ModelState.AddModelError("", "Invalid login attempt.");
        return View(model);
    } 
    

    As you can see you could do anything you want to gather claims and fill the identity then sign in the user.

    But if you want use SignInManager.PasswordSignInAsync() method simply override SignInManager.CreateUserIdentityAsync() method in a way so you could generate desired claims. For example if you need DbContext to fetch extra information for feeding your claims simply you could inject DbContext in SignInManager and use it in CreateUserIdentityAsync() method like this:

    public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
    {
        private readonly ApplicationDbContext _context;
    
        public ApplicationSignInManager(
            ApplicationUserManager userManager, 
            IAuthenticationManager authenticationManager,
            ApplicationDbContext context)
            : base(userManager, authenticationManager)
        {
            _context=context;
        }
    
        public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
        {
            var companyInsuranceId=_context.Users
                .Where(u=>u.NationalCode == user.UserName)
                .Select(u=>u.UserInsurances
                    .OrderByDescending(x => x.CompanyInsuranceId)
                    .Select(x=>x.CompanyInsuranceId)
                    .FirstOrDefault())
                .FirstOrDefault();
    
            var ident=user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
            ident.AddClaim(new Claim("CompanyInsuranceId",
                companyInsuranceId.ToString()));
            return ident;
        }
    
        public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
        {
            return new ApplicationSignInManager(
                context.GetUserManager<ApplicationUserManager>(),
                context.Authentication,
                context.Get<ApplicationDbContext>());
        }
    }
    

    Now simply just by writing

    var result = await SignInManager.PasswordSignInAsync(
        model.UserName, model.Password, 
        model.RememberMe, shouldLockout: false);
    

    you could sign the user in and inject additional claims.