How to add claims to the HttpContext User on sign in

13,832

Solution 1

To add or transform custom claims, implement and use a custom ClaimsAuthenticationManager. How To: Transform Incoming Claims.

public class ClaimsTransformationModule : ClaimsAuthenticationManager {  
    public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal) {  
        if (incomingPrincipal != null && incomingPrincipal.Identity.IsAuthenticated == true) {  
           var identity = (ClaimsIdentity)incomingPrincipal.Identity;
           var user = GetUserData(identity);

           identity.AddClaim(new Claim("fullname", user.GetFullName(user.UserName)));  
           identity.AddClaim(new Claim("avatarUrl", user.AvatarUrl)); 
        }  

        return incomingPrincipal;  
    }  
} 

Here, GetUserData() retrieves the User entity from the DB, given the user name.

Register this transformer in the web.config:

<system.identityModel>
   <identityConfiguration>
      <claimsAuthenticationManager type="MyProject.ClaimsTransformationModule , MyProject, Version=1.0.0.0, Culture=neutral" />
   </identityConfiguration>
</system.identityModel>

Solution 2

We encountered exactly the same problem some time ago. The solution is rather simple. You just need to create your own implementation of IUserClaimsPrincipalFactory interface and register it in DI container. Of course, it's not necessary to write the implementation of that interface from scratch - you can derive your class from UserClaimsPrincipalFactory and just override one method.

Here is a step-by-step description including the code snippets.

Solution 3

If you have a .NET core Middleware pipeline (or another custom setup) where you handle your authentication/authorization and instantiate the Claim you can directly add it to the HttpContext like so (no need for a ClaimsAuthenticationManager):

HttpContext ctx; // you need to have access to the context
var claim = new Claim(ClaimTypes.Name, user.Name.Value);
var identity = new ClaimsIdentity(new[] { claim }, "BasicAuthentication"); // this uses basic auth
var principal = new ClaimsPrincipal(identity);
ctx.User = principal;

This example sets the ClaimsIdentity, if you need to add a Claim instead you can do that as well.

Share:
13,832
dinotom
Author by

dinotom

Hobbyist electronics and developer, either for electronics applications or web enabled apps to support my main interest, which is commodity trading.

Updated on June 16, 2022

Comments

  • dinotom
    dinotom almost 2 years

    This post may be long, but will have all the pertinent detail required for an answer.

    I have been searching, and found that many others have as well, for the proper methodology to add claims to the HttpContext User so that those claims can be retrieved when needed, using Razor, in a view.

    For example,

    In the default Asp.Net Core 2.0 web application, the _LoginPartial has code that displays the users email. If I wanted to change that to the users full name (this assumes the registration process includes the first and last name entries, with the appropriate changes to the ApplicationUser class)

     // Add profile data for application users by adding properties to the ApplicationUser class
    public class ApplicationUser : IdentityUser
    {
        public string FirstName { get; set; }
    
        public string LastName { get; set; }
    
        public DateTime DateOfBirth { get; set; }
    
        public Gender Gender { get; set; }
        ...balance of code removed for brevity
    }
    

    I would want to add a claim on the User for their full name and gender to use instead of the UserManager method currently employed in the default app. (And others down the road)

    Current default wep app code

    @if (SignInManager.IsSignedIn(User))
    {
        <form asp-area="" asp-controller="Account" asp-action="Logout" method="post" id="logoutForm" class="navbar-right">
            <ul class="nav navbar-nav navbar-right">
                <li>
                    <a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello @UserManager.GetUserName(User)!</a>
                </li>
                <li>
                    <button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button>
                </li>
            </ul>
        </form>
    }
    else
    {
    ...code removed for brevity
    }
    

    What I am hoping to accomplish; Replacing this,

    <a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello @UserManager.GetUserName(User)!</a>
    

    with this

    <a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello @((ClaimsIdentity) User.Identity).GetSpecificClaim("avatarUrl")!</a>    
    

    Note: GetSpecificClaim is an extension method to retrieve the claim.

    I believe the best place to add the claims would be in the login method.

     public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
             if (!ModelState.IsValid) return View(model);
            // Now model is valid, require the user to have a confirmed email before they can log on.
            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user != null)
            {
                if (!await _userManager.IsEmailConfirmedAsync(user))
                {
                    ModelState.AddModelError(string.Empty,
                        "You must have a confirmed email to log in.");
                    return View(model);
                }
            }
            else
            {
                ModelState.AddModelError(string.Empty,
                    "There is no registered account for the email address supplied.");
                return View(model);
            }
    
            var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
            if (result.Succeeded)
            {
                _logger.LogInformation("User logged in.");
    
                // Add claims to signed in user 
                var userClaims = HttpContext.User.Claims.ToList();
                userClaims.Add(new Claim("fullname", user.GetFullName(user.UserName)));
                userClaims.Add(new Claim("avatarUrl", user.AvatarUrl));
    
               // Using ClaimsTransformer
               // Add claims here for the logged in user using AddUserInfoClaimsAsync extension method
                **var ct = new ClaimsHelpers.ClaimsTransformer();
                var identityWithInfoClaims = await ct.AddUserInfoClaimsAsync(User, user);**
    
                return RedirectToLocal(returnUrl);
            }
            if (result.RequiresTwoFactor)
            {
                return RedirectToAction(nameof(LoginWith2Fa), new { returnUrl, model.RememberMe });
            }
            if (result.IsLockedOut)
            {
                _logger.LogWarning("User account locked out.");
                return RedirectToAction(nameof(Lockout));
            }
    
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return View(model);
        }
    

    But the userClaims variable is always empty

    Breakpoint showing empty claim list

    Questions:

    1. Why is the claims list empty when the claims were just set?
    2. Is there a different type of claim for identity?
    3. Is there a better method for doing this?

    UPDATE: I had put a ClaimsTransformer together on some earlier attempts, I can get claims added using that (see the change above in bold in the login controller code) but what do I now do with the ClaimsPrincipal variable identityWithinfoClaims? I can't set User equal to it because User is readonly so how does that object with the claims added get used appropriately?