Promoting SoC Through Application Layering

Creating a layered design in an application is a fundamental element of modern software architecture. The goal is to promote the Separation of Concerns (SoC) design principle. Separation of Concerns The ideas behind SoC date back to Dijkstra's 1974 paper "On the role of scientific thought" . In computer science, separation of concerns (sometimes abbreviated as SoC) is a design principle for separating a computer program into distinct sections. Each section addresses a separate concern, a set of information that affects the code of a computer program. A concern can be as general as "the details of the hardware for an application", or as specific as "the name of which class to instantiate". A program that embodies SoC well is called a modular program. Modularity, and hence separation of concerns, is achieved by encapsulating information inside a section of code that has a well-defined interface. - Wikipedia SoC is a broad design principal th...

Customizing Authentication Cookie Expiration Settings in ASP.NET

There may be times when very specific authentication expiration rules are required for an ASP.NET application. When using Individual Accounts authentication and authentication cookies, you can create custom rules for expiring the authentication cookie.

For this demonstration, we'll work with a .NET 8 Blazor Web App configured for Individual Accounts authentication. 

Visual Studio Project Creation Screenshot

We can modify the authentication cookie behavior when initializing our application in Program.cs by setting the CookieAuthenticationOptions. Let's look at a simple example.

builder.Services.Configure<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme, options =>
{
    options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
    options.Cookie.MaxAge = options.ExpireTimeSpan; // set this cookie to have a fixed timeout rather than being a session cookie
    options.SlidingExpiration = true;
});

The code above will set the Application authentication cookie to expire after a 20 minute sliding window. Additionally, we set the cookie to have a fixed timeout by setting the Cookie.MaxAge to options.ExpireTimeSpan (rather than the default, which is a session cookie). As long as the application is hitting the server, the cookie timeout will be be extended. By default, the cookie's expiration date/time will be extended after it is more than halfway through the expiration window. In this case, when the cookie expiration date is less then 10 minutes away, the next server hit will extend the cookie for another 20 minutes. After 20 minutes with no activity, the user will have to login again. Below is a screenshot showing that the cookie has a fixed expiration date/time.

Browser Cookie Information Screenshot

That level of customization is sufficient for many applications, but what if the requirements are to have a sliding window of 20 minutes, and a user can only stay logged in for a maximum of 8 hours (even if they are using the application every 20 minutes). There is no built-in solution for this requirement. ASP.NET gives us the ability to implement this type of expiration by creating a custom CookieAuthenticationEvents class.

Let's create a custom class that inherits from CookieAuthenticationEvents that can handle the unique timeout requirements.

public class AuthenticationCookieMaximumAgeTimeout : CookieAuthenticationEvents
{
	public override Task SigningIn(CookieSigningInContext context)
	{
		// add an "ExpiresInTicks" string value to the authentication cookie, this is the current date/time in ticks plus one day
		context.Properties.SetString("ExpiresInTicks", (DateTime.UtcNow.Ticks + TimeSpan.FromHours(8).Ticks).ToString());
		return base.SigningIn(context);
	}

	public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
	{
		// grab the "ExpiresInTicks" string value from the authentication cookie
		var expiresAtStringValue = context.Properties.GetString("ExpiresInTicks");

		// if there is no "ExpiresInTicks" value, reject the authentication request
		if (string.IsNullOrEmpty(expiresAtStringValue))
		{
			context.RejectPrincipal();
			return;
		}

		long expiresAtLongValue;

		// if the "ExpiresInTicks" value cannot be parsed to a long, reject the authentication request
		if (!long.TryParse(expiresAtStringValue, out expiresAtLongValue))
		{
			context.RejectPrincipal();
			return;
		}

		// if the "ExpiresInTicks" value is less than the current time, reject the authentication request
		if (DateTime.UtcNow.Ticks > expiresAtLongValue)
		{
			context.RejectPrincipal();
			return;
		}

		// otherwise, continue validation of the authentication cookie
		await base.ValidatePrincipal(context); 
	}
}

Now we can register this class to handle the Application cookie's authentication events. This requires two changes to the Program.cs. First, let's register the new class with the Service collection.

builder.Services.AddTransient<AuthenticationCookieMaximumAgeTimeout>();

Next, we need to update the CookieAuthenticationOptions to call the new class as below:

builder.Services.Configure<s;CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme, options =>
{
    options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
    options.Cookie.MaxAge = options.ExpireTimeSpan;
    options.SlidingExpiration = true;
    options.EventsType = typeof(AuthenticationCookieMaximumAgeTimeout);
});

The new AuthenticationCookieMaximumAgeTimeout class can be completely customized to override any cookie authentication events. If needed, we could change the way the sliding expiration window works and extend the cookie expiration date/time after 5 minutes like this:

public override Task CheckSlidingExpiration(CookieSlidingExpirationContext context)
{
	// rather than extending the cookie's expiration after it's more than halfway through the expiration window, renew the cookie after 5 minutes
	context.ShouldRenew = context.ElapsedTime > TimeSpan.FromMinutes(5);
	return base.CheckSlidingExpiration(context);
}

Further information on CookieAuthenticationEvents can be found at Use cookie authentication without ASP.NET Core Identity and Microsoft.AspNetCore.Authentication.Cookies Namespace.

There were significant changes to Identity for .NET 8. A summary can be found at What’s new with identity in .NET 8

This example was created using Visual Studio 2022 running a Blazor Web App project.

Source code: https://github.com/jharrell-bits/BlazorCookieAuthCustomDemo