Auto sign-out using ASP.NET Core Razor Pages with Azure AD B2C

Auto sign-out using ASP.NET Core Razor Pages with Azure AD B2C

This article shows how an ASP.NET Core Razor Page application could implement an automatic sign-out when a user does not use the application for n-minutes. The application is secured using Azure AD B2C. To remove the session, the client must sign-out both on the ASP.NET Core application and the Azure AD B2C identity provider or whatever identity provider you are using.

Code: https://github.com/damienbod/AspNetCoreB2cLogout

Sometimes clients require that an application supports automatic sign-out in a SSO environment. An example of this is when a user uses a shared computer and does not click the sign-out button. The session would remain active for the next user. This method is not fool proof as the end user could save the credentials in the browser. If you need a better solution, then SSO and rolling sessions should be avoided but this leads to a worse user experience.

The ASP.NET Core application is protected using Microsoft.Identity.Web. This takes care of the client authentication flows using Azure AD B2C as the identity provider. Once authenticated, the session is stored in a cookie. A distributed cache is added to record the last activity of of each user. An IAsyncPageFilter implementation is used and added as a global filter to all requests for Razor Pages. The SessionTimeoutAsyncPageFilter class implements the IAsyncPageFilter interface.

builder.Services.AddDistributedMemoryCache();

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
	.AddMicrosoftIdentityWebApp(builder.Configuration, "AzureAdB2c" )
	.EnableTokenAcquisitionToCallDownstreamApi(Array.Empty<string>())
	.AddDistributedTokenCaches();

builder.Services.AddAuthorization(options =>
{
	options.FallbackPolicy = options.DefaultPolicy;
});

builder.Services.AddSingleton<SessionTimeoutAsyncPageFilter>();

builder.Services.AddRazorPages()
.AddMvcOptions(options =>
{
	options.Filters.Add(typeof(SessionTimeoutAsyncPageFilter));
})
.AddMicrosoftIdentityUI();

The IAsyncPageFilter interface is used to catch the request for the Razor Pages. The OnPageHandlerExecutionAsync method is used to implement the automatic end session logic. We use the default name identifier claim type to get an ID for the user. If using the standard claims instead of the Microsoft namespace mapping, this would be different. Match the claim returned in the id_token from the OpenID Connect authentication. I check for idle time. If no requests was sent in the last n-minutes, the application will sign-out, in both the local cookie and also on Azure AD B2C. It is important to sign-out on the identity provider as well. If the idle time is less than the allowed time span, the DateTime timestamp is persisted to cache.

public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
	var claimTypes = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
	var name = context.HttpContext
		.User
		.Claims
		.FirstOrDefault(c => c.Type == claimTypes)!
		.Value;

	if (name == null) throw new ArgumentNullException(nameof(name));

	var lastActivity = GetFromCache(name);

	if (lastActivity != null && lastActivity.GetValueOrDefault()
		.AddMinutes(timeoutInMinutes) < DateTime.UtcNow)
	{
		await context.HttpContext
			.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
		await context.HttpContext
			.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
	}

	AddUpdateCache(name);

	await next.Invoke();
}

Distributed cache is used to persist the user idle time from each session. This might be expensive for applications with many users. In this demo, the UTC now value is used for the check. This might need to be improved and the cache length as well. This needs to be validated. if this is enough for all different combinations of timeout.

private void AddUpdateCache(string name)
{
	var options = new DistributedCacheEntryOptions()
		.SetSlidingExpiration(TimeSpan
			.FromDays(cacheExpirationInDays));

	_cache.SetString(name, DateTime
		.UtcNow.ToString("s"), options);
}

private DateTime? GetFromCache(string key)
{
	var item = _cache.GetString(key);
	if (item != null)
	{
		return DateTime.Parse(item);
	}

	return null;
}

When the session timeouts, the code executes the OnPageHandlerExecutionAsync method and signouts.

This works for Razor Pages. This is not the only way of supporting this and it is not an easy requirement to fully implement. Next step would be to support this from SPA UIs which send Javascript or ajax requests.

Links

https://learn.microsoft.com/en-us/azure/active-directory-b2c/openid-connect#send-a-sign-out-request

https://learn.microsoft.com/en-us/aspnet/core/razor-pages/filter?view=aspnetcore-7.0

https://github.com/AzureAD/microsoft-identity-web

This content was originally published here.