ASP.NET Core 3.1 - Cookie Validator
This article will demonstrate the implementation of verifying the authentication cookie on every request. I will assume you have downloaded the ASP.NET Core 3.1 - Users Without Identity Project or created a new ASP.NET Core 3.1 Razor Pages project. See Tutorial: Get started with Razor Pages in ASP.NET Core. You should review the earlier articles of the Users Without Identity Project series.
Users Without Identity Project and Article Series
Creation Series
- ASP.NET Core 3.1 - Users Without Identity
- ASP.NET Core 3.1 - User Entity
- ASP.NET Core 3.1 - Password Hasher
- ASP.NET Core 3.1 - User Management
- ASP.NET Core 3.1 - Admin Role
- ASP.NET Core 3.1 - Cookie Validator
- ASP.NET Core 3.1 - Concurrency Conflicts
- ASP.NET Core 3.1 - Must Change Password
- ASP.NET Core 3.1 - User Database Service
- ASP.NET Core 3.1 - Rename Related Entities
2FA Series
- ASP.NET Core 3.1 - 2FA Without Identity
- ASP.NET Core 3.1 - 2FA User Tokens
- ASP.NET Core 3.1 - 2FA Cookie Schemes
- ASP.NET Core 3.1 - 2FA Authenticating
- ASP.NET Core 3.1 - 2FA Sign In Service
- ASP.NET Core 3.1 - 2FA QR Code Generator
- ASP.NET Core 3.1 - Admin 2FA Requirement
- ASP.NET Core 3.1 - 2FA Admin Override
Enhanced User Series
- ASP.NET Core 3.1 - Enhanced User Without Identity
- ASP.NET Core 3.1 - 2FA Recovery Codes
- ASP.NET Core 3.1 - Login Lockout
- ASP.NET Core 3.1 - Created And Last Login Date
- ASP.NET Core 3.1 - Security Stamp
- ASP.NET Core 3.1 - Token Service
- ASP.NET Core 3.1 - Confirmed Email Address
- ASP.NET Core 3.1 - Password Recovery
- The app's cookie authentication system continues to process requests based on the authentication cookie.
- The user remains signed into the app as long as the authentication cookie is valid.
Most examples use LastChanged as string to compare the cookie to the user record stored in the database. The LastChanged property should be updated every time the user is edited. We can use a RowVersion byte[] property with the [TimeStamp] data annotation which will automatically update on SQL Insert and SQL Update.
Edit Entities > AppUser.cs:
public class AppUser { [Key] public int Id { get; set; } [Required] [Display(Name = "Login Name")] [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] public string LoginName { get; set; } [Required] [StringLength(32)] public string LoginNameUppercase { get; set; } [Required] [MaxLength(84, ErrorMessage = "The {0} is max {1} characters long.")] public string PasswordHash { get; set; } [Display(Name = "Admin")] public bool IsAdmin { get; set; } [Timestamp] public byte[] RowVersion { get; set; } }
Add the RowVersion migration.
Package Manager Console:
add-migration RowVersion -o Data/Migrations
DotNet CLI:
dotnet-ef migrations add RowVersion -o Data/Migrations
The add migration produces a MigrationBuilder which will add the RowVersion column to the AppUsers table. Notice rowVersion: true. The file name prepends a date time stamp to RowVersion.
20200120183748_RowVersion.cs:
public partial class RowVersion : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn<byte[]>( name: "RowVersion", table: "AppUsers", rowVersion: true, nullable: true); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: "RowVersion", table: "AppUsers"); } }
If all looks good, update the database.
Package Manager Console:
update-database
DotNet CLI:
dotnet-ef database update
We need to convert the RowVersion byte[] property to string to store the value in a claim. Add a UserData claim = rowVersionString to the Claims Principle at login. Be sure to add the NameIdentifier claim.
Edit Account/ Login.cshtml.cs > OnPost:
string rowVersionString = Encoding.Unicode.GetString(user.RowVersion); var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.Integer), new Claim(ClaimTypes.Name, user.LoginName) new Claim(ClaimTypes.UserData, rowVersionString) };
Create a new class named CookieValidator in the Services folder.
Services/ CookieValidator.cs:
public static class CookieValidator { public static async Task ValidateAsync(CookieValidatePrincipalContext context) { if (!await ValidateCookieAsync(context)) { context.RejectPrincipal(); await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } } private static async Task<bool> ValidateCookieAsync(CookieValidatePrincipalContext context) { var claimsPrincipal = context.Principal; var uid = (from c in claimsPrincipal.Claims where c.Type == ClaimTypes.NameIdentifier select c.Value).FirstOrDefault(); if (!int.TryParse(uid, out int userId)) { return false; } var rowVersionString = (from c in claimsPrincipal.Claims where c.Type == ClaimTypes.UserData select c.Value).FirstOrDefault(); if (string.IsNullOrEmpty(rowVersionString)) { return false; } byte[] rowVersion = Encoding.Unicode.GetBytes(rowVersionString); var applicationDbContext = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>(); return await applicationDbContext.AppUsers .AsNoTracking() .Where(a => a.Id == userId) .Select(a => a.RowVersion.SequenceEqual(rowVersion)) .FirstOrDefaultAsync() .ConfigureAwait(false); } }
Configure the OnValidatePrincipal in the CookieAuthenticationEvents options to call the CookieValidator. ValidateAsync function.
Edit Startup.cs > ConfigureServices:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Events = new CookieAuthenticationEvents { OnValidatePrincipal = CookieValidator.ValidateAsync }; });
Build, run and test. Set a breakpoint on the CookieValidator. ValidateAsync function. Notice the breakpoint is only hit when a user is signed in. If the cookie doesn't validate the user is signed out. To test, you can use 2 browsers like Chrome and Firefox. Login to one as Administrator and login to the other as a different user. Edit and update the other user with Administrator. Refreshing the page for the other user will sign out the other user.
Update 02/23/2021
I added the Enhanced User Series' article links.
Comments(0)