The mystery of session timeout in ASP.NET Core 3.1 & 5
By: Yousry Mohamed
Tags: Azure Ad, Vscode, Aspnetcore

Azure AD housekeeping

Time to get our hands dirty with some code as I am a big fan of learning by doing. The application I am going to investigate authenticates users using Azure AD. You will need an Azure AD instance to create an app registration for the the application we will create.

Follow [Step 1: Register your application] section in this Microsoft docs page to create an app registration.

P.S. Latest versions of VS Code use port 5001 to serve .NET Core web apps. So it is recommended to change any port reference in the above guide to 5001.

Keep a note of the following:

Domain name (may look like some-domain@onmicrosoft.com ). In Azure AD overview page, it would be the primary domain inside a box titled “Tenant Information”.

). In Azure AD overview page, it would be the primary domain inside a box titled “Tenant Information”. Tenant Id (This is the unique identifier of your Azure AD instance)

Application Id

Creating .NET Core 3.1 app

You will need latest version of VS Code plus .NET 3.1 & 5 SDKs. A quick Google search will help you download and install them if you don’t already have them on your machine. The steps outlined in this article are for a Windows machine but I would say they should work pretty much the same on Linux or Mac.

Fire up a new terminal window and run the below to bootstrap a .NET Core 3.1 app that authenticates against Azure AD. Replace the values inside square brackets with values from your own setup.

dotnet new mvc --auth SingleOrg --domain [DOMAIN-NAME] --tenant-id [TENTANT-ID] --client-id [CLIENT-ID]--framework netcoreapp3.1 --language C# --name MyApp3.1

Open the newly created folder named MyApp3.1 using VS Code and run the web app by hitting Ctrl+F5. If prompted by VS Code to selected environment type, pick .NET Core. If things work as expected, you will be auto redirected to an Azure AD login screen. Provide valid credentials for any user in your Azure AD instance and you will be redirected to application home page.

You can read more on OAuth2 and implicit grant flow here. For the sake of simplicity, all we need to know that once the user is authenticated with Azure AD a form post will be triggered at https://localhost:5001/signin-oidc where form data submitted contains an ID token plus a few extra verification fields. You can inspect this network call using browser dev tools.

Copy the value of id_token form field and decode it on https://jwt.io

The obfuscated fields above contain your Azure AD app client Id and tenant Id respectively. Then there are three timestamps. The first two are token “issued at” time and “not before” time. Both have same value which is 5 minutes before real token issuance time. I guess this is to account for any clock differences on difference machines across the globe.

The final timestamp named “exp” is the expiry time of the token. In our case, it is one hour (ignoring the 5 minutes thing) after the token issuance time.

We got the first bit of information here. Azure AD id tokens (with default configuration) have an expiry of one hour. Memorize this fact as we would need it shortly.

A .NET Core 3.1 web app authenticating with Azure AD has a nuget dependency on Microsoft.AspNetCore.Authentication.AzureAD.UI. This library is part of the same .NET Core repo on GitHub. Keep this in mind as well.

Setup VS Code to step into external code

.NET Core is an open source technology developed on GitHub and has an amazing community and tooling. One of the cool features that would help us here is the ability to single step inside framework code using Microsoft symbol servers. To enable this feature, edit .vscode/launch.json file as follows. Have a look here for more details.

Now hit F5 (not Ctrl+F5) to launch the application in debugging mode. Everything should work as before but you will see lots of lines in debug console window showing download of files from Microsoft reference servers or loading them from local folder if they were already downloaded from a previous debugging session. Also be patient, it takes a bit of time to load all dependencies but the ROI is worth it.

Before digging into source code, let’s inspect session cookie in the browser. Once the application is loaded and logged in, open browser dev tools (usually hitting F12 will do the job). Switch to application tab and expand the Cookies link in side menu and click the line with our app URL.

There are two cookies one for user session information and one for Azure Application Insights. Session cookie has no explicit Expires/Max Age attribute. It’s a session cookie not a persistent/permanent cookie. If the browser is closed, it will be deleted. Information about when the session should expire if the browser is kept open is injected into cookie contents but in an encrypted form readable only by the server.

Inspecting contents of session cookie

Stop debugging session if it is still open by hitting Shift+F5. Now open debug window in VS Code and add a breakpoint on CookieAuthenticationHandler class constructor. Breakpoints can be added by clicking the + sign that appears when you hover inside that breakpoints section.

I found it to be a good hack to force loading a certain class into debugger session. Now hit F5 and let’s start debugging.

The breakpoint should be hit. Congratulations, you can single step into ASP.NET core source code and investigate stuff for yourself and even apply other breakpoints on other lines in same class.

I could stop here and leave it for you to do the backtracking and put on the hat of Sherlock Holmes but I would like to continue this journey with you. In the same file, search for the line with this content and put a breakpoint on it.

return AuthenticateResult.Success(ticket);

Adding this breakpoint can be doing using the classic way of clicking to the left of line number then hit F5 to continue. The second breakpoint will be hit and we can add ticket variable to the watch to inspect it.

Great, we can see all the raw details of session cookie (assuming user is authenticated) and inspect individual values. Session timeout is not a mystery anymore and you can identify the exact value of session timeout which is the ExpiresAt property of the ticket variable inspected above. Once you are aware of the real session timeout value, you can dig deeper and understand why it has this specific value. If you stop here, it is still valuable to know for sure how the application is supposed to function.

Due to the complexity the HTTP pipeline, multiple moving parts and so many abstraction layers of authentication process, it maybe hard to tell just from documentation or online resources what should be the session timeout value for a certain application. The approach outlined here can be used when using different authentication methods like Identity Server or the .NET Core built-in identity features.

Explaining the one hour session

Let’s move on using the same debugger technique and try to understand why session has a one hour duration. I guess you might expect that it is related somehow to the duration of the id token received from Azure AD.

You are 100% right but let’s prove it. I will start to be less verbose from now on.

Stop the current debugging session if still open and add the following new breakpoints:

Constructor of AzureADOpenIdConnectOptionsConfiguration then once hit add a breakpoint on the line Options.UseTokenLifetime = true;

then once hit add a breakpoint on the line Constructor of OpenIdConnectHandler then add a breakpoint on the line starting with private ClaimsPrincipal ValidateToken

I will explain what you would normally get from that debugging session. Microsoft.AspNetCore.Authentication.AzureAD.UI library (which is part of ASP.NET Core repo) sets an OpenID Connect config option called UseTokenLifetime to true. This flag instructs cookie pipeline to take id token duration and use it to control session duration.

ValidateToken method in OpenIdConnectHandler class inspects the UseTokenLifetime property and if it is true, it will set a value in a properties bag (which is later used to control session duration) to the duration of id token retrieved from Identity Provider which is Azure AD in our case.

This explains the one hour session duration when using Azure AD with a .NET Core 3.1 web app.

Upgrading to .NET 5

Now let’s say we would like to use the shiny toys and upgrade to .NET 5 and also upgrade the Azure AD authentication library to latest version. The project will compile but we would get some warnings that Microsoft.AspNetCore.Authentication.AzureAD.UI is obsolete and we should upgrade to Microsoft.Identity.Web which is actually hosted outside ASP.NET Core repo.

We can continue tweaking the code to do that upgrade but we can also use the dotnet new command and pick the ASP.NET 5 template which comes with a nuget reference to Microsoft.Identity.Web and has the needed pipeline setup.

In a terminal window run the following snippet which is very similar to the previous run but with a different project name and target framework.

dotnet new mvc --auth SingleOrg --domain [DOMAIN-NAME] --tenant-id [TENTANT-ID] --client-id [CLIENT-ID]--framework net5.0 --language C# --name MyApp5.0

Run the new web app from VS Code and make sure it authenticates fine with Azure AD as the previous .NET Core 3.1 app.

You can verify that project file has reference to .NET 5 target framework and Microsoft.Identity.Web.

What is the session duration for the upgraded app?

You can use the same debugging approach used for the .NET Core 3.1 app to find session duration for the .NET 5 app.

Surprise!

Session duration is 14 days now. That’s a radical deviation from the original one hour session. But why? Is it something with .NET 5 or due to the change to Microsoft.Identity.Web.

Unfortunately Microsoft.Identity.Web is not part of .NET Core repo so its debug information is probably not available on Microsoft Symbol servers. Still, it is open source and we can pull the repo locally for inspection or check source code directly on GitHub.

https://github.com/AzureAD/microsoft-identity-web/blob/00bb0a87f7c4583b3c4aed7763fba019209c22e6/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs#L408

UseTokenLifetime is initialized not to the hardcoded true but to a value sourced from another class called MicrosoftIdentityOptions which inherits from OpenIdConnectOptions. It seems unless the value is explicitly set, it will default to the default value of boolean variables which is false.

Ok, this means that session duration is not linked with id token expiry anymore. So, what controls it now?

With default configuration, there is a class used to set cookie authentication options and it sets cookie expiry to 14 days by default.

This is used to specify an expiry value that would be assigned to session cookie expiry property later hence setting session duration to 14 days (remember that I am ignoring sliding expiration although it defaults to true).

Are we stuck with 14 days?

Nope, we can tweak session duration to whatever value that makes sense for the use case. Let’s say session duration should be one day. We can use another overload when bootstrapping authentication pipeline to set custom values for settings like session duration or sliding expiration.

If you inspect session cookie now, it should reflect the above change.

I am not sure if the old Azure AD authentication library has similar tooling to tweak session timeout or what. Actually since I upgraded to .NET 5 & Microsoft.Identity.Web, I did not need to try tweaking the old library.

Wrap up

Sometimes the documentation is not so obvious about certain aspects or libraries/frameworks change quickly in a short timespan. So following online resources could be a source of confusion not help. In this article, I hope I have shed some light on how session timeout is controlled in ASP.NET Core applications and how to troubleshoot or investigate low level application behaviour if you want to be 100% sure about what happens behind the scenes.

I presume there is someway to include debugging symbols for libraries like Microsoft.Identity.Web but that’s a challenge for another day.

Further reading