ASP.NET Core 6.0 - Administrator Claim

Ken Haggerty
Created 01/12/2022 - Updated 01/12/2022 02:30 GMT

This article describes the UWD2FAP AppUserClaim, creating a ClaimsPrincipal, and migrating the AppUser Administrator property to an AppUserClaim. I will assume you have downloaded the ASP.NET Core 6.0 - Users With Device 2FA Project.

Users With Device 2FA Project and Article Series

This article series is about the implementation of ASP.NET Core 6.0 with Visual Studio 2022. The ASP.NET Core 6.0 - Users With Device 2FA Project (UWD2FAP) implements WebAuthn, also known as FIDO2, instead of authenticator apps for two-factor authentication (2FA). The project implements Bootstrap v5 and Bootstrap Native. After a user registers, they can enable 2FA with Windows Hello, Android Lock Screen, or a FIDO2 security key. If I had this project when I created KenHaggerty. Com three years ago, I would have started with this user authentication template. The latest version is published at Preview. KenHaggerty. Com. I encourage you to register and evaluate multiple 2FA devices in Manage Account > Two-Factor Authentication. Details, screenshots, and related articles can be found at ASP.NET Core 6.0 - Users With Device 2FA Project. The details page includes the version change log.

Updated with links as the articles become published.

The ASP.NET Core 3.1 - Users Without Identity Project implements a boolean AdministratorRole property for the AppUser to determine if the logged in user's claims principle has an Administrator claim. The CreateUserPrincipalAsync method in the SignInService evaluated the AppUser's properties to determine the ClaimsPrinciple's claims.

SignInService.cs
/// Creates a <see cref="ClaimsPrincipal"/> for the user specified by <paramref name="appUserId"/>, asynchronous.
/// </summary>
/// <param name="appUserId">The id for the user to create a <see cref="ClaimsPrincipal"/> for.</param>
/// <param name="authenticationMethod">Name of the method used to authenticate the user.</param>
/// <returns><see cref="Task{TResult}"/> for <see cref="ClaimsPrincipal"/></returns>
private async Task<ClaimsPrincipal> CreateUserPrincipalAsync(int appUserId, string authenticationMethod)
{
    var appUser = await _userService.GetAppUserByIdAsync(appUserId).ConfigureAwait(false);
    var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, appUser.Id.ToString(), ClaimValueTypes.Integer),
            new Claim(ClaimTypes.Name, appUser.LoginName),
            new Claim(ClaimTypes.Sid, appUser.SecurityStamp),
            new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod),
            new Claim(UWIPConstants.TwoFactorEnabledClaimType, appUser.TwoFactorEnabled.ToString())
        };

    if (appUser.AdministratorRole)
        claims.Add(new Claim(ClaimTypes.Role, UWIPConstants.AdministratorRole));
    if (appUser.MustChangePassword)
        claims.Add(new Claim(UWIPConstants.MustChangePasswordClaimType, string.Empty));

    var applicationIdentity = new ClaimsIdentity(claims, UWIPConstants.ApplicationScheme);

    return new ClaimsPrincipal(applicationIdentity);
}

The UWD2FAP implements an AppUserClaim entity for claims-based authorization.

AppUserClaim.cs
public class AppUserClaim
{
    /// <summary>
    /// Creates a new instance of <see cref="AppUserClaim"/>.
    /// </summary>
    public AppUserClaim()
    {
    }
    /// <summary>
    /// <see cref="AppUserClaim"/> constructor with parameters.
    /// </summary>
    /// <param name="appUserId"></param>
    /// <param name="type"></param>
    /// <param name="value"></param>
    /// <param name="valueType"></param>
    public AppUserClaim(int appUserId, string type, string value, string valueType = "")
    {
        AppUserId = appUserId;
        Type = type;
        Value = value;
        ValueType = valueType;
        CreatedDate = DateTimeOffset.UtcNow;
    }

    [Key]
    public int Id { get; set; }
    [Required]
    public int AppUserId { get; set; }
    [Required]
    [StringLength(512)]
    public string Type { get; set; } = string.Empty;
    [StringLength(512)]
    public string Value { get; set; } = string.Empty;
    [StringLength(512)]
    public string ValueType { get; set; } = string.Empty;
    public DateTimeOffset CreatedDate { get; set; }
}

The AppUser in the UWD2FAP implements a nullable virtual HashSet property for AppUserClaims. Entity Framework Core allows you to use the navigation properties in your model to load related entities. See MSDocs - Loading Related Data.

public virtual HashSet<AppUserClaim>? AppUserClaims { get; set; }

The UWD2FAP implements the AppUserClaim constructor with parameters to create a new Administrator claim.

applicationDbContext.AppUserClaims!.Add(new AppUserClaim(appUser.Id, ApplicationClaimType.Administrator, string.Empty));

The CreateUserPrincipalAsync method in the UWD2FAP implements a for each loop to add claims.

SignInService.cs
/// <summary>
/// Creates a <see cref="ClaimsPrincipal"/> for the <paramref name="appUser"/>, asynchronous.
/// </summary>
/// <param name="appUser">The the user to create a <see cref="ClaimsPrincipal"/> for.</param>
/// <param name="authenticationMethod">Name of the method used to authenticate the user.</param>
/// <returns><see cref="Task{TResult}"/> for <see cref="ClaimsPrincipal"/></returns>
private async Task<ClaimsPrincipal> CreateUserPrincipalAsync(AppUser appUser, string authenticationMethod)
{
    var claims = new List<Claim>
            {
                new Claim(ClaimTypes.NameIdentifier, appUser.Id.ToString(), ClaimValueTypes.Integer),
                new Claim(ClaimTypes.Name, appUser.LoginName),
                new Claim(ClaimTypes.Sid, appUser.SecurityStamp),
                new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod)
            };

    var appUserClaims = await _userService.GetAppUserClaimsByAppUserIdAsync(appUser.Id).ConfigureAwait(false);
    foreach (var appUserClaim in appUserClaims)
        claims.Add(new Claim(appUserClaim.Type, appUserClaim.Value ?? string.Empty, appUserClaim.ValueType!));

    return new ClaimsPrincipal(new ClaimsIdentity(claims, ApplicationClaimPolicy.ApplicationScheme));
}

The UWD2FAP implements a HasAdministratorClaim boolean property for the AppUser with a NotMapped attribute which denotes that a property or class should be excluded from database mapping. See MSDocs - NotMappedAttribute Class.

[NotMapped]
[Display(Name = "Admin")]
public bool HasAdministratorClaim { get; set; }

The GetAppUserByIdAsync method in the UserService sets the HasAdministratorClaim.

UserService.cs
public async Task<AppUser?> GetAppUserByIdAsync(int appUserId)
{
    return await _dbAppUserSet
        .Include(a => a.AppUserClaims)
        .AsNoTracking()
        .Where(u => u.Id == appUserId)
        .Select(a => new AppUser
        {
            Id = a.Id,
            LoginName = a.LoginName,
            HasAdministratorClaim = a.AppUserClaims != null && a.AppUserClaims
                .Any(c => c.Type == ApplicationClaimType.Administrator),
            PasswordHash = a.PasswordHash,
            HasMustChangePasswordClaim = a.AppUserClaims != null && a.AppUserClaims
                .Any(c => c.Type == ApplicationClaimType.MustChangePassword),
            Email = a.Email,
            EmailConfirmed = a.EmailConfirmed,
            UnconfirmedEmail = a.UnconfirmedEmail,
            TwoFactorEnabledValue = a.AppUserClaims != null && a.AppUserClaims
                .Any(c => c.Type == ApplicationClaimType.TwoFactorEnabled & c.Value == bool.TrueString),
            LoginNameUppercase = a.LoginNameUppercase,
            EmailUppercase = a.EmailUppercase,
            UserHandle = a.UserHandle,
            SecurityStamp = a.SecurityStamp,
            LastLoginDate = a.LastLoginDate,
            CreatedDate = a.CreatedDate,
            TOSDateString = a.AppUserClaims == null ? "Not Found" : a.AppUserClaims
                .Where(c => c.Type == ApplicationClaimType.TermsOfService)
                .Select(c => c.Value).SingleOrDefault() ?? "Not Found"
        })
        .FirstOrDefaultAsync()
        .ConfigureAwait(false);
}

The UWD2FAP implements an ApplicationClaimType class to manage type strings.

ApplicationClaimType.cs
public class ApplicationClaimType
{
    /// <summary>
    /// Policy Claim - The value used to identify the Administrator claim type.
    /// </summary>
    public const string Administrator = "userswithdevice2fa/claims/administrator";
    /// <summary>
    /// User Claim - The value used to hold the user's TwoFactorEnabled state.
    /// </summary>
    public const string TwoFactorEnabled = "userswithdevice2fa/claims/twofactorenabled";
    /// <summary>
    /// User Claim - The value used to redirect a user to the change password page.
    /// </summary>
    public const string MustChangePassword = "userswithdevice2fa/claims/mustchangepassword";
    /// <summary>
    /// User Claim - The value used to redirect a user to the Terms Of Service agreement when not found.
    /// </summary>
    public const string TermsOfService = "userswithdevice2fa/claims/termsofservice";
}

Article Tags:

Authorization Claims EF Core

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications