ASP.NET Core 8.0 - Administrator Claim

This article will describe the implementation of an AppUserClaim from storage to a ClaimsPrincipal's authorization claim and authorization policy. You should review the earlier articles of the Cookies And Claims Project series. Registered users can download the ASP.NET Core 8.0 - Cookies And Claims Project for free.

Cookies And Claims Project and Article Series

Free project download for registered users!

I developed the Cookies And Claims Project (CACP) to demonstrate a simple cookie authentication scheme and claim-based authorization with a clear and modifiable design. The CACP is developed with Visual Studio 2022 and the MS Long Term Support (LTS) version .NET 8.0 framework. All Errors, Warnings, and Messages from Code Analysis have been mitigated. The CACP implements utilities like an AES Cipher, Password Hasher, native JavaScript client form validation, password requirements UI, Bootstrap Native message modal generator, loading/spinner generator, SignalR online user count, and an automatic idle logout for users with administration permissions.

There are plenty of discussions about role vs claims authorization. An interesting observation is that a role is a claim. The main difference is how and where the data is stored. An advantage of a claim is that it has type and value. If you employ claims authorization, there is no need for roles. With claims authorization, you need to use authorization policies. The CACP implements a static class named AppSettings.cs for global project settings which declares a policy name and a claim type.

AppSettings.cs:
/// <summary>
/// Authorization Policy value used to identify the Administrator only policy.
/// </summary>
public const string AdministratorOnlyPolicy = "AdministratorOnly";
/// <summary>
/// Authorization Claim Type - Access to Pages/Admin/Index.
/// </summary>
public const string AdministratorClaimType = "Administrator";
/// <summary>
/// Claim value used to identify the Issuer.
/// </summary>
public const string ClaimIssuer = "CookiesAndClaimsProject";

The CACP adds the policy to the services container with the AddAuthorizationBuilder. The policy has one claim requirement. To succeed the policy, the UserPrincipal must include a claim with the administrator claim type and any claim value.

Program.cs:
builder.Services.AddAuthorizationBuilder()
    .AddPolicy(AppSettings.AdministratorOnlyPolicy, policy =>
        policy.RequireClaim(AppSettings.AdministratorClaimType, []));

The CACP implements the AppUsers.json and AppUserClaims.json files in a Data folder to demonstrate claim-based authorization. The Administrator's AppUser has an Id = 1000. The AppUserClaims.json stores one claim with an AdministratorClaimType and an AppUserId = 1000.

Data > AppUserClaims.json:
{
  "AppUserClaims": [
    {
      "Id": 1000,
      "AppUserId": 1000,
      "Type": "Administrator",
      "Value": "true",
      "ValueType": "bool"
    }
  ]
}

The UserService's GetAppUser method parses the AppUsers.json and AppUserClaims.json files and returns the AppUser entity with any AppUserClaims if the LoginName is found.

Services > UserService.cs:
public class UserService(IWebHostEnvironment webHostEnvironment) : IUserService
{
    private readonly IWebHostEnvironment _webHostEnvironment = webHostEnvironment;
    public AppUser? GetAppUser(string loginName)
    {
        if (string.IsNullOrEmpty(loginName)) return null;
        var rootPath = _webHostEnvironment.ContentRootPath;
        var appUsersPath = Path.Combine(rootPath, "Data/AppUsers.json");
        var appUsersJson = System.IO.File.ReadAllText(appUsersPath);
        if (string.IsNullOrWhiteSpace(appUsersJson)) return null;
        var appUserList = JsonSerializer.Deserialize<AppUserList>(appUsersJson);
        if (appUserList!.AppUsers == null || appUserList.AppUsers.Count == 0) return null;
        var appUser = appUserList.AppUsers.FirstOrDefault(u => String.Equals(u.LoginName, loginName, StringComparison.CurrentCultureIgnoreCase));
        if (appUser == null) return null;
        var appUserClaimsPath = Path.Combine(rootPath, "Data/AppUserClaims.json");
        var appUserClaimsJson = System.IO.File.ReadAllText(appUserClaimsPath);
        if (string.IsNullOrWhiteSpace(appUserClaimsJson)) return null;
        var appUserClaimHashSet = JsonSerializer.Deserialize<AppUserClaimHashSet>(appUserClaimsJson);
        if (appUserClaimHashSet!.AppUserClaims == null || appUserClaimHashSet.AppUserClaims.Count == 0) return null;
        appUser.AppUserClaims = appUserClaimHashSet.AppUserClaims.Where(c => c.AppUserId == appUser.Id).ToHashSet();
        return appUser;
    }
}

During the login process, if the password is validated, a ClaimsIdentity is created from the AppUser. The ClaimsIdentity is used to add the UserPrincipal to the HttpContext.

Pages > Account > Login.cshtml.cs:
var loginName = Input.LoginName.Trim();
var appUser = _userService.GetAppUser(loginName);
if (appUser == null)
{
    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
    return Page();
}

var passwordHasher = new CustomPasswordHasher();
var hasherResult = passwordHasher.VerifyPassword(appUser.PasswordHash, Input.Password);
if (!hasherResult.Succeeded)
{
    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
    return Page();
}

List<Claim> claims =
[
    new(ClaimTypes.NameIdentifier, appUser.Id.ToString(), ClaimValueTypes.Integer, AppSettings.ClaimIssuer),
    new(ClaimTypes.Name, appUser.LoginName, ClaimValueTypes.String, AppSettings.ClaimIssuer)
];

foreach (var appUserClaim in appUser.AppUserClaims)
    claims.Add(new Claim(appUserClaim.Type, appUserClaim.Value, appUserClaim.ValueType, AppSettings.ClaimIssuer));

var claimsIdentity = new ClaimsIdentity(claims, AppSettings.ApplicationScheme);

AuthenticationProperties authenticationProperties = new()
{                
    AllowRefresh = AppSettings.LoginRememberMeDays > 0 && Input.RememberMe,
    IsPersistent = AppSettings.LoginRememberMeDays > 0 && Input.RememberMe,
    ExpiresUtc = AppSettings.LoginRememberMeDays > 0 ? DateTimeOffset.UtcNow.AddDays(AppSettings.LoginRememberMeDays) : null
};

await HttpContext.SignInAsync(
    AppSettings.ApplicationScheme,
    new ClaimsPrincipal(claimsIdentity),
    authenticationProperties);

return LocalRedirect(ReturnUrl);

You can implement authorization attributes with the Microsoft .AspNetCore .Authorization Namespace. In its most basic form, applying the [Authorize] attribute to a controller, action, or Razor Page, limits access to that component to authenticated users. Apply policies to Razor Pages by using the [Authorize] attribute with the policy name. See MS - Policy-based authorization in ASP.NET Core.

Pages > Admin > Index.cshtml.cs:
using Microsoft.AspNetCore.Authorization;
namespace CookiesAndClaims.Pages.Admin;
[Authorize(Policy = AppSettings.AdministratorOnlyPolicy)]
public class IndexModel : PageModel

The CACP implements authorization conventions at startup. See MS - Razor Pages authorization conventions in ASP.NET Core. The AuthorizeFolder extension with RazorPagesOptions's Conventions accepts a policy name parameter. The UserPrincipal must succeed the AdministratorOnlyPolicy to access to the Admin folder.

Program.cs:
builder.Services.AddRazorPages(options =>
    {
        options.Conventions.AuthorizeFolder("/Account/Manage");
        options.Conventions.AuthorizeFolder("/Members");
        options.Conventions.AllowAnonymousToPage("/Members/IdleLogout");
        options.Conventions.AuthorizeFolder("/Admin", AppSettings.AdministratorOnlyPolicy);
    })
    .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; })
    .AddCookieTempDataProvider(options => { options.Cookie.IsEssential = true; });

For more about implementing policy and claims based authorization see ASP.NET Core 6.0 - Claims-Based Authorization.

Ken Haggerty
Created 07/24/24
Updated 10/24/24 20:56 GMT

Log In or Reset Quota to read more.

Article Tags:

Authorization Claims
Successfully completed. Thank you for contributing.
Processing...
Something went wrong. Please try again.
Contribute to enjoy content without advertisments.
You can contribute without registering.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?