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.
- ASP.NET Core 8.0 - Cookies And Claims
- ASP.NET Core 8.0 - Cookie Authentication
- ASP.NET Core 8.0 - Remember Me Or Not
- ASP.NET Core 8.0 - Authorized Access
- ASP.NET Core 8.0 - Administrator Claim
- ASP.NET Core 8.0 - Admin Idle Logout
- ASP.NET Core 8.0 - Cookie Consent
- ASP.NET Core 8.0 - SignalR Online User Count
- ASP.NET Core 8.0 - AES Cipher
- ASP.NET Core 8.0 - Password Hasher
- ASP.NET Core 8.0 - Message Generator
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.
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.
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.
Comments(0)