Controlling a Web App’s session duration
When you use the OpenId Connect (OIDC) or the WS-Federation middleware (MW) in an ASP.NET app, a successful authentication (eg, a transaction resulting in your app receiving a valid user token) results in the production of a session cookie – courtesy of the cookie middleware. As long as the session cookie sticks around and is valid, the app considers the user authenticated.
By default, in ASP.NET 4.6 the amount of time for which this session is matches the validity timeframe of the token that prompted the generation of the session in the first place. Say that you are using the OIDC MW with Azure AD: the id_token received by the app during the user authentication transaction will last one hour, hence the session cookie for your app will also last 1 hour. Somewhat counter-intuitively, this behavior will be enforced regardless of session-modifying settings (such as a specific duration) you add to the cookie MW options. However, it is safe to say that you will often want your app to have sessions that last more that one hour, or whatever duration the original token carries. True, by default the cookie MW provides sliding sessions… but users don’t necessarily stay active all the time. Certain web apps stay quiet in a tab all day, and are used only at sparse times – but with inactivity, the session times out so every tab switch becomes a new auth gesture. Not fun.
There are at least a couple of ways you can extend the duration of your session. One is quick and somewhat dirty; the other is more… thoughtful, and safer: but it also requires a bit more code. Let’s take a look.
Decouple the session duration from the token validity
The easiest way out of this is to decouple the session duration from the expiration times carried by the original token. That is super easy: you just tell the OIDC MW to stop controlling this aspect in the cookie MW, by passing the following option:
1: app.UseOpenIdConnectAuthentication(
2: new OpenIdConnectAuthenticationOptions {
3: ...
4: UseTokenLifetime = false,
5: }
6: );
As simple as that. With UseTokenLifetime set to false, the cookie MW will now honor whatever settings you add in the cookie MW options.
The challenge with that approach is that now the app session is entirely decoupled from the session the user has with Azure AD (or whatever IdP you are working with). That’s dangerous: your app session might outlive the IdP session, which might have been terminated for good reasons (admins find out the user was compromised; lost device scenarios; and so on).
Set up session renewing logic
The ideal would be to ensure that our app’s session lasts as long as the IdP session, or at least approximate it. Note: the duration of the IdP session is NOT the duration of the id_token obtained when the user authenticated to your app. When your user was bounced to the Azure AD pages, he/she went through an authentication ceremony which ended up with two artifacts:the id_token your app requested AND a session cookie bound to the Azure AD domain. That cookie is what makes possible for the user to avoid entering credentials if he/she ends up needing in short order another token from Azure AD. The validity timeframe of that cookie is the validity timeframe of the IdP session. How can we latch to that session and ensure that our own web app’ session follows it?
If you ever worked with ADAL JS, you know that it uses a neat trick (explained here) to renew the tokens it needs for the JS frontend to access its API backend. The idea is that ADAL JS injects in the web app a hidden iframe, and uses that iframe to silently request new tokens to Azure AD. Given that the hidden frame points to the Azure AD domain, as long as there is a valid cookie for Azure AD maintaining a user session, requests for new tokens will not require the user to reenter credentials – and will obtain new tokens silently.
That works for the SPA application architecture, where the web app “session” is really carried by the tokens attached to every web API call. In the case of apps protected by OIDC and cookie MWs, the session is really the cookie issued by the web app itself. Could we tweak the trick we use in ADAL JS to keep getting fresh session cookies from the web app as long as the Azure AD session is valid? Yes we can! Here there’s a way to do it:
1. add to the web app a route that will always result in a new authorization request, regardless of whether the user is already signed in or not
2. add a hidden iframe in the web app, which hits the new “forced” sign in route at regular time intervals
…and that’s all there is to it.
The point #1 is only necessary because I am assuming you are using the ASP.NET project templates or our github samples for OIDC web apps – in the regular app templates and samples the SignIn route only triggers the Challenge if the user isn’t signed in yet. In fact, all you need to do is to go in the Account controller, duplicate the SignIn method, rename it ForcedSignIn or equivalent, and get rid of the if:
1: public void ForcedSignIn()
2: {
3: // Send an OpenID Connect sign-in request.
4: HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" },
5: OpenIdConnectAuthenticationDefaults.AuthenticationType);
6: }
That done, all you need to do is to inject an iframe in the app pages that will periodically hit the new route. For example, you can add the below right in –Layout.cshtml, in the body (right after all the <div>s).
1: <iframe id="renewSession" hidden></iframe>
2: <script>
3: setInterval( function ()
4: { @if (Request.IsAuthenticated) {
5: <text>
6: var renewUrl = "/Account/ForcedSignIn";
7: var element = document.getElementById("renewSession");
8: console.log("sending request to: " + renewUrl);
9: element.src = renewUrl;
10: </text>
11: } else {
12: <text>
13: console.log("No renewal attempt without a valid session");
14: </text>
15: }
16: },
17: 1000*60*45
18: );
19: </script>
It’s that simple! With that JS in place, the app will trigger a new sign in, completely transparently, every 45 minutes. Note that this will happen only if the current web app session is still valid. If there is a valid Azure AD session, the app will successfully drive a new authentication dance that will result in a new cookie; otherwise – no harm done.
I made a little test just to be sure. I created two apps, one with the session refresh trick and one without. I deployed both to their own azure websites. I opened Chrome and signed in in both apps, from 2 different tabs, at 10:12pm. Then I selected the tab with the app without session trick, and started watching Mockingjay part II (don’t judge me. Also, meh).
At 23:20 I went back to Chrome, selected the tab with the app with the session trick, and clicked on About. I got access right away – sign that the session was still valid.
I switched to the tab with the app with no session trick. I clicked about, and sure enough I got redirected to Azure AD before gaining access – sign that the session expired as usual. Fiddler:
This is all pretty neat, isn’t it. I already know that some of you like to sprinkle just a bit of AJAX in their MVC apps, and this trick allows you to also secure web API with the OIDC and cookie MW instead of switching to the more appropriate OAuth2 bearer. Not that I approve – tokens are better than cookies – but I understand that for apps that are mostly MVC adding a whole JS subsystem might be impractical.
The approach isn’t completely issues-free, though. For example: the Azure AD session is itself a sliding session. Which means that this trick will generate traffic and keep the Azure AD session alive where user inactivity could have eventually lead to that session expiring. But, as long as you know what you are doing, it’s an extra trick in your bag
Happy coding!
V.