Search code examples
c#asp.net-core-identityasp.net-core-3.1

Implement get/set methods for Identity User custom property


I am using Identity and have extended the base IdentityUser with three custom properties. Using .netCore 3.1.1 and identity 4

namespace FleetLogix.Intranet.Identity
{
    // Add profile data for application users by adding properties to the ApplicationUser class
    public class ApplicationUser : IdentityUser<int>
    {
        [MaxLength(50)]
        [PersonalData]
        public string FirstName { get; set; }

        [MaxLength(50)]
        [PersonalData]
        public string LastName { get; set; }

        [MaxLength(5)]
        public string OrgCode { get; set; }

        public ApplicationUser() : base()
        {

        }


    }
}

These were happily created in the [AspNetUsers] table. 4 initial users have been created, with all additional properties filled.

I have also created some extensions that let me get the values of these properties. FirstName -> GivenName, LastName -> Surname and OrgCode is a CustomClaimTypes.OrgCode

namespace FleetLogix.Intranet.Identity
{
    /// <summary>
    /// Extends the <see cref="System.Security.Principal.IIdentity" /> object to add accessors for our custom properties.
    /// </summary>
    public static class IdentityExtensions
    {
        /// <summary>
        /// Gets the value of the custom user property FirstName
        /// </summary>
        /// <example>
        /// User.Identity.GetFirstName()
        /// </example>
        /// <param name="identity">Usually the Identity of the current logged in User</param>
        /// <returns><see langword="string"/> containing value of LastName or an empty string</returns>
        public static string GetFirstName(this IIdentity identity)
        {
            ClaimsIdentity claimsIdentity = identity as ClaimsIdentity;
            Claim claim = claimsIdentity?.FindFirst(ClaimTypes.GivenName);

            return claim?.Value ?? string.Empty;
        }

        /// <summary>
        /// Gets the value of the custom user property LastName
        /// </summary>
        /// <example>
        /// User.Identity.GetLastName()
        /// </example>
        /// <param name="identity">Usually the Identity of the current logged in User</param>
        /// <returns><see langword="string"/> containing value of LastName or an empty string</returns>
        public static string GetLastName(this IIdentity identity)
        {
            ClaimsIdentity claimsIdentity = identity as ClaimsIdentity;
            Claim claim = claimsIdentity?.FindFirst(ClaimTypes.Surname);

            return claim?.Value ?? string.Empty;
        }


        /// <summary>
        /// Gets the value of the custom user property OrgCode
        /// </summary>
        /// <example>
        /// User.Identity.GetOrgCode()
        /// </example>
        /// <param name="identity">Usually the Identity of the current logged in User</param>
        /// <returns><see langword="string"/> containing value of OrgCode or an empty string</returns>
        public static string GetOrgCode(this IIdentity identity)
        {
            ClaimsIdentity claimsIdentity = identity as ClaimsIdentity;
            Claim claim = claimsIdentity?.FindFirst(CustomClaimTypes.OrgCode);

            return claim?.Value ?? string.Empty;
        }
    }
}

I am setting up a new site and want to modify _LoginPartial.cshtml. I want to replace the display of the logged in username(an email address) with the logged in FirstName

@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
    <a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @UserManager.GetUserName(User)!</a> 
    </li>
   ...
}

to this

@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
    <a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.GetFirstName()!</a> 
    </li>
   ...
}

however, this results in empty text. Why is this blank?

Clicking through into the Account/Manage/Index page I am presented with a form for modifying user details. I have modified the InputModel to include two of the custom properties (FirstName, LastName). The LoadAsync task has been modified to load values (using the extension methods) and add them to an `InputModel

private async Task LoadAsync(ApplicationUser user)
{

    var userName = await _userManager.GetUserNameAsync(user);
    var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
    var firstName =  user.FirstName;
    var lastName = user.LastName;
    var orgCode = user.OrgCode;


    Username = userName;

    OrgCode = orgCode;

    Input = new InputModel
    {
        PhoneNumber = phoneNumber,
        FirstName = firstName,
        LastName = lastName

    };
}

Screenshot of User Modify page

Why are the custom properties visible on this page, but not on the previous?

Further in Account/Manage/Index is the update method OnPostAsync()

public async Task<IActionResult> OnPostAsync()
{
   var user = await _userManager.GetUserAsync(User);
   if (user == null)
   {
       return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
   }

   if (!ModelState.IsValid)
   {
       await LoadAsync(user);
       return Page();
   }

   var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
   if (Input.PhoneNumber != phoneNumber)
   {
       var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
       if (!setPhoneResult.Succeeded)
       {
           var userId = await _userManager.GetUserIdAsync(user);
           throw new InvalidOperationException($"Unexpected error occurred setting phone number for user with ID '{userId}'.");
       }
   }

   var firstName = user.FirstName; //.GetPhoneNumberAsync(user);
   if (Input.FirstName != firstName)
   {
       //var setFirstNameResult = await _userManager.SetFirstNameAsync(user, Input.FirstName);
       user.FirstName = Input.FirstName;
       //if (!setFirstNameResult.Succeeded)
       //{
       //    var userId = await _userManager.GetUserIdAsync(user);
       //    throw new InvalidOperationException($"Unexpected error occurred setting First Name for user with ID '{userId}'.");
       //}
   }


   var lastName = user.LastName;
   if (Input.LastName != lastName)
   {
       //var setLastNameResult = await _userManager.SetLastNameAsync(user, Input.LastName);
       user.LastName = Input.LastName;
       //if (!setLastNameResult.Succeeded)
       //{
       //    var userId = await _userManager.GetUserIdAsync(user);
       //    throw new InvalidOperationException($"Unexpected error occurred setting Last Name for user with ID '{userId}'.");
       //}
   }

   await _signInManager.RefreshSignInAsync(user);
   StatusMessage = "Your profile has been updated";
   return RedirectToPage();
}

Without Set methods like SetPhoneNumberAsync() I tried using the property setter. This didn't work. How do I update Identity User Custom Property values?

I just want to be able to use custom User properties. I need the FirstName & OrgCode property available as soon as they log in, which isn't currently the case. The current extension methods don't always work.

In addition, I need to be able to edit these properties in case their are errors or changed requirements.


Solution

  • You need to create scope of identity, just create your identity and add scoped

    services.AddScoped<IUserIdentity, UserIdentity>();
    

    You need to implement middleware for map all your properties in identity. Add to startup:

    app.UseMiddleware<UserIdentityAccessor>();
    

    Implementation:

        public class UserIdentityAccessor
        {
            private readonly RequestDelegate _next;
    
            public UserIdentityAccessor(RequestDelegate next)
            {
                _next = next;
            }
    
            public async Task InvokeAsync(HttpContext context, IUserIdentity userIdentity)
            {
                var user = (ClaimsIdentity) context.User.Identity;
    
                if (user.IsAuthenticated)
                {
                    var first = user.FindFirst(ClaimsName.FirstName).Value; \\ get info from claims
                    userIdentity.FirstName = first;                         \\ and add to identity
                }
                else
                {
                    userIdentity.FirstName = null;
                }
    
                await _next(context);
            }
        }
    

    And now you can get identity wherever you want