ASP.NET Core 2.2 - User Claims

Ken Haggerty
Created 04/22/2019 - Updated 04/28/2019 01:29
Update 04/27/2019

The cut and paste results were confusing. I found and corrected the types in Edit ChangeDisplyedName.cshtml.cs where they were not properly escaped. I also added the SignInManager references to Pages > Index.cshtml.

This article will cover implementing displayed name and create date user claims with ASP.NET Core Identity. I will assume you have created a new ASP.NET Core 2.2 Razor Pages project with Individual User Accounts, updated the database with the CreateIdentitySchema migration, scaffolded the Identity UI and created an ApplicationUser with the CreateDate Property (See User's Last Login Date). Or you can download the free ASP.NET Core 2.2 - Bootstrap Native Project from Manage > Assets. I was able to restore, build and run the project with VS 2017, VS 2019 and VS Code.

Manage Assets

I used string constants to declare the claim type and policy properties in Claims Authorization Policy. Knowing we will have more authorization claims along with user claims, let's create ApplicationClaimPolicy and ApplicationClaimType classes to help manage policy and claim properties.

Add new file named ApplicationClaims in Entities folder:
public class ApplicationClaimPolicy
{
    public const string AdministratorOnly = "AdministratorOnly";
}

public class ApplicationClaimType
{
    // Policy Claims
    public const string Administrator = "http://yourdomain.com/claims/administrator";
    // User Claims
    public const string DisplayedName = "http://yourdomain.com/claims/displayedname";
    public const string UserCreateDate = "http://yourdomain.com/claims/usercreatedate";
}

If you implemented Claims Authorization Policy, you can remove the string constants and update the AddPolicy's parameters.

Edit Stratup.cs > ConfigureServices > AddAuthorization, update parameters:
services.AddAuthorization(options =>
{
    options.AddPolicy(ApplicationClaimPolicy.AdministratorOnly,
        policy => policy.RequireClaim(ApplicationClaimType.Administrator));
});

I often use User.Identity.IsAuthenticated, but SignInManager.IsSignedIn(User) is specific to ASP.NET Core Identity. Inject the SignInManager to _Layout.cshtml.

Edit Pages > Shared > _Layout.cshtml:
@using BootstrapNative.Entities;
@using Microsoft.AspNetCore.Identity
@inject SignInManager<ApplicationUser> SignInManager
Section references:
Edit Pages > Shared > _Layout.cshtml > NavBarCollapse:
@if (SignInManager.IsSignedIn(User) 
    && (await AuthorizationService.AuthorizeAsync(User, ApplicationClaimPolicy.AdministratorOnly)).Succeeded)
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Admin" asp-page="/Index">Admin</a>
    </li>
}

One advantage of user claims is they are automatically loaded to the context with the claims principal. You do not have to call the database to retrieve the claim's value. A user's CreateDate claim can be a string representation of the user's CreateDate property and used to display a Member Since date which should never change. The DisplayedName claim can be used for public comments without compromising the user's login name. Because the DisplayedName may change, I chose to use only a claim for a single source of truth. Let's create these claims for a new registered user.

Edit Register.cshtml > form, add DisplayedName input:
<div class="form-group">
    <label asp-for="Input.DisplayedName"></label>
    <input asp-for="Input.DisplayedName" class="form-control" />
    <span asp-validation-for="Input.DisplayedName" class="text-danger"></span>
</div>
Edit Register.cshtml.cs > InputModel, add DisplayedName property:
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[Display(Name = "Displayed Name")]
public string DisplayedName { get; set; }
Edit Register.cshtml.cs > OnPostAsync > result.Succeeded, add claims:
var displayedNameClaimResult = await _userManager.AddClaimAsync(user,
    new Claim(ApplicationClaimType.DisplayedName,
    Input.DisplayedName));
if (!displayedNameClaimResult.Succeeded)
{
    throw new InvalidOperationException($"Unexpected error occurred setting DisplayedName claim" +
        $" ({displayedNameClaimResult.ToString()}) for user with ID '{user.Id}'.");
}

var userCreateDateClaimResult = await _userManager.AddClaimAsync(user,
    new Claim(ApplicationClaimType.UserCreateDate,
    string.Format("{0:MM/dd/yyyy}", DateTimeOffset.UtcNow)));
if (!userCreateDateClaimResult.Succeeded)
{
    throw new InvalidOperationException($"Unexpected error occurred setting UserCreateDate claim" +
        $" ({userCreateDateClaimResult.ToString()}) for user with ID '{user.Id}'.");
}

The CreateDate should never change, but you should allow the user to edit their DisplayedName. Add a ChangeDisplayedName page to Identity\Pages\Account\Manage.

Edit ChangeDisplayedName.cshtml:
@page
@model ChangeDisplayedNameModel
@{
    ViewData["Title"] = "Change Displayed Name";
    ViewData["ActivePage"] = ManageNavPages.ChangeDisplayedName;
}

<h4>@ViewData["Title"]</h4>
<hr />
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
    <div class="col-md-6">
        <form id="change-name-form" method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="CurrentDisplayedName"></label>
                <input asp-for="CurrentDisplayedName" class="form-control" disabled />
            </div>
            <div class="form-group">
                <label asp-for="Input.DisplayedName"></label>
                <input asp-for="Input.DisplayedName" class="form-control" />
                <span asp-validation-for="Input.DisplayedName" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Update Name</button>
        </form>
    </div>
</div>
Edit ChangeDisplayedName.cshtml.cs:
public class ChangeDisplayedNameModel : PageModel
{
    private readonly ApplicationUserManager _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly ILogger<ChangeDisplayedNameModel> _logger;

    public ChangeDisplayedNameModel(
        ApplicationUserManager userManager,
        SignInManager<ApplicationUser> signInManager,
        ILogger<ChangeDisplayedNameModel> logger)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _logger = logger;
    }

    [BindProperty]
    public InputModel Input { get; set; }

    [Display(Name = "Current Name")]
    public string CurrentDisplayedName { get; set; }

    [TempData]
    public string StatusMessage { get; set; }

    public class InputModel
    {
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [Display(Name = "New Name")]
        public string DisplayedName { get; set; }
    }

    public IActionResult OnGet()
    {
        CurrentDisplayedName = User.Claims
            .Where(c => c.Type == ApplicationClaimType.DisplayedName)
            .Select(c => c.Value).SingleOrDefault();
            
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        var claims = await _userManager.GetClaimsAsync(user);
        var displayedNameClaim = claims
            .Where(c => c.Type == ApplicationClaimType.DisplayedName)
            .FirstOrDefault();

        if (claims.Count > 0 && displayedNameClaim != null)
        {
            var removeClaimResult = await _userManager.RemoveClaimAsync(user, displayedNameClaim);
            if (!removeClaimResult.Succeeded)
            {
                throw new InvalidOperationException($"Unexpected error occurred removing " +
                    $"DisplayedName claim ({removeClaimResult.ToString()}) for user with ID '{user.Id}'.");
            }
        }

        var addClaimResult = await _userManager.AddClaimAsync(user,
            new Claim(ApplicationClaimType.DisplayedName,
            Input.DisplayedName));

        if (!addClaimResult.Succeeded)
        {
            throw new InvalidOperationException($"Unexpected error occurred setting " +
                $"DisplayedName claim ({addClaimResult.ToString()}) for user with ID '{user.Id}'.");
        }

        await _signInManager.RefreshSignInAsync(user);

        _logger.LogInformation("User updated their DisplayedName claim.");
        StatusMessage = "Your displayed name has been updated.";

        return RedirectToPage();
    }
}

Notice the await _signInManager.RefreshSignInAsync(user); which updates the claims principal.

Edit ManageNavPages.cs, add DisplayedName properties:
public static string ChangeDisplayedName => "ChangeDisplayedName";
public static string ChangeDisplayedNameNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangeDisplayedName);
Edit _ManageNav.cshtml, add DisplayedName nav-link:
<li class="nav-item">
    <a class="nav-link @ManageNavPages.ChangeDisplayedNameNavClass(ViewContext)"
        id="change-displayedname" asp-page="./ChangeDisplayedName">Displayed Name</a>
</li>
Edit Index.cshtml, add DisplayedName disabled input:
<div class="form-group">
    <label asp-for="DisplayedName"></label>
    <input asp-for="DisplayedName" class="form-control" disabled />
</div>
Edit Index.cshtml.cs, add DisplayedName property:
[Display(Name = "Displayed Name")]
public string DisplayedName { get; set; }
Edit Index.cshtml.cs > OnGetAsync, initialize the DisplayedName property:
DisplayedName = User.Claims
    .Where(c => c.Type == ApplicationClaimType.DisplayedName)
    .Select(c => c.Value).SingleOrDefault();

To demonstrate the use of the claim values, add a new row to Pages > Index.cshtml and edit the _LoginPartial.cshtml.

Edit Index.cshtml, add SignInManager references:
@using BootstrapNative.Entities;
@using Microsoft.AspNetCore.Identity
@inject SignInManager<ApplicationUser> SignInManager
Edit Index.cshtml, add DisplayedName and CreateDate values:
@if (SignInManager.IsSignedIn(User))
{
    <hr />
    <div class="row">
        <div class="col-12">
            @(User.Claims.Where(c => c.Type == ApplicationClaimType.DisplayedName).Select(c => c.Value).SingleOrDefault() ?? User.Identity.Name)
            - Member since: @(User.Claims.Where(c => c.Type == ApplicationClaimType.UserCreateDate).Select(c => c.Value).SingleOrDefault() ?? "Claim Not Found")
        </div>
    </div>
}
Edit _LoginPartial.cshtml, update the Manage nav-link to use the DisplayedName value:
<li class="nav-item">
    <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">
        Manage @(User.Claims.Where(c => c.Type == ApplicationClaimType.DisplayedName)
            .Select(c => c.Value).SingleOrDefault() ?? User.Identity.Name)
    </a>
</li>

Article Tags:

Bootstrap Claims Identity

Comment Count = 1

solarseven
solarseven
Posted 06/28/2019 15:18
Member Since 06/18/2019

Thanks Ken for another great guide. Crisp and clear, very tasty!

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications