ASP.NET Core 8.0 - Cookies And Claims

This article introduces a series about the ASP.NET Core 8.0 - Cookies And Claims Project. The series describes how to implement a cookie authentication scheme and claim-based authorization. The project includes many of the utilities I use in KenHaggerty.Com and premium projects. 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.

I revisited my popular article, ASP.NET Core 3.1 - Users Without Identity which describes a cookie authentication scheme without the Authentication Type - Individual Accounts (ASP.NET Core Identity framework) template. See Tutorial: Get started with Razor Pages in ASP.NET Core.

I developed the Cookies And Claims Project (CACP) from a new ASP.NET Core 8.0 Razor Pages project. The new razor pages project template without Identity or Individual User Accounts builds and runs without any NuGet packages installed. The CACP only requires the ASP.NET Core Identity UI NuGet package. This package is the default Razor Pages built-in UI for the ASP.NET Core Identity framework.

NuGet Manager.

I implement the AppUsers.json and AppUserClaims.json files in a Data folder to demonstrate a simple cookie authentication scheme and claim-based authorization with just 2 users, Administrator and Member. Both use "P@ssw0rd" for their password but the password is hashed and stored in AppUsers.json. AppUserClaims.json has 1 Administrator claim associated with the Administrator user only. The CACP implements a SignalR OnlineCountHub.cs in the Hubs folder. The Models/Entities folder contains the entity models for the AppUser and AppUserClaim. The Models/InputModels folder contains form models for Login and utilities pages. The Pages folder contains Account, Admin, Members, Shared, and Utilities folders to demonstrate authentication and authorized access to different pages. The Services folder contains user and utility code.

Solution Explorer Top.
Solution Explorer Pages.
Solution Explorer Users.

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:
namespace CookiesAndClaims.Services;
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;
    }
}

The Login page uses dependency injection to access the UserService. If an AppUser is found, the entered password is verified against the stored password hash by the CustomPasswordHasher. If the password is verified, a ClaimsIdentity is constructed from the AppUser. Then the ClaimsIdentity is added to the HttpContext with a cookie scheme and AuthenticationProperties.

Pages > Account > Login.cshtml.cs:
namespace CookiesAndClaims.Pages.Account;
public class LoginModel(IUserService userService) : PageModel
{
    private readonly IUserService _userService = userService;

    [BindProperty]
    public LoginInputModel Input { get; set; } = new();
    public string? ReturnUrl { get; set; }

    public async Task OnGetAsync(string? returnUrl = null)
    {
        // Clear the existing external cookie
        await HttpContext.SignOutAsync(AppSettings.ApplicationScheme);

        returnUrl ??= Url.Content("~/");
        ReturnUrl = returnUrl;
    }

    public async Task<IActionResult> OnPostAsync(string? returnUrl = null)
    {
        if (!Url.IsLocalUrl(returnUrl)) returnUrl = Url.Content("~/");
        ReturnUrl = returnUrl;

        if (ModelState.IsValid)
        {
            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);
        }
        // Something failed. Redisplay the form.
        return Page();
    }
}

This series will describe the cookie authentication scheme and claim-based authorization features in more detail. This series also describes the utilities the CACP implements.

Cookie Authentication

This article will describe the implementation of a simple cookie authentication scheme. It will describe the default configuration and overriding some of the options.

Remember Me Or Not

This article will describe the implementation of a configurable AuthenticationProperties, which determines the cookie's lifetime.

Authorized Access

This article will describe authorization conventions and authorization attributes to restrict access to pages for anonymous users.

Administrator Claim

This article will describe the implementation of an AppUserClaim from storage to a ClaimsPrincipal's authorization claim and authorization policy.

Admin Idle Logout

This article will describe the implementation of an idle logout function which combines JavaScript and html in a razor partial view.

Cookie Consent

This article will describe the implementation of Microsoft. AspNetCore. CookiePolicy to request user consent for non-essential cookies.

Online User Count

This article will describe the implementation of Microsoft.AspNetCore.SignalR to count online users.

AES Cipher

This article will describe the implementation of the AES Cipher.

Password Hasher

This article will describe the implementation of a custom password hasher.

Message Modal Generator

This article will describe the implementation of a global Bootstrap message modal.

Ken Haggerty
Created 07/17/24
Updated 10/24/24 20:59 GMT

Log In or Reset Quota to read more.

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 ?