12

This is more of a design/approach question...

I think I'm missing something here. We're building an Asp.Net MVC 5 web application and securing it with Azure AD using the following scenario:

https://azure.microsoft.com/en-us/documentation/articles/active-directory-authentication-scenarios/#web-browser-to-web-application

https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect

The token/cookie is an absolute expiry and expires after one hour. So what does that do for the user experience? Every hour they have to log back in no matter what? In our testing, when the user expires, the browser is redirected back to AD and the user prompted for credentials. This, of course, breaks any AJAX calls we have loading partial views and none of our DevExpress controls are stable as a result.

Based on the response to this SO post: MVC AD Azure Refresh Token via ADAL JavaScript Ajax and KnockoutJs

...what I'm seeing is expected? It seems to me like not a very viable solution for a cloud hosted line-of-business application where users are logged in and working all day.

Am I missing something? Or is this just not an ideal scenario for business apps?

Community
  • 1
  • 1
Jason P.
  • 123
  • 1
  • 5

2 Answers2

10

We faced a similar set of problems, as well as the same thoughts about how you could use Azure AD with ASP.NET MVC in web apps with such a low session timeout (60 minutes).

The solution we came up with, that seems to be working (albeit with limited testing), is to have an iFrame on the page that we refresh every 5 minutes.

<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms" id="refreshAuthenticationIframe" src="@Url.Action("CheckSessionTimeout", "Home", new { area = "" })" style="display:none;"></iframe>

The "CheckSessionTimeout" page is basically blank.

In a Javascript file referenced by the whole app, we have:

var pageLoadTime = moment();

setInterval(refreshAuthenticationCookies, 1000);

function refreshAuthenticationCookies() {
    if (moment().diff(pageLoadTime, "seconds") > 300) {
        document.getElementById("refreshAuthenticationIframe").contentDocument.location = "/Home/ForceSessionRefresh";
        pageLoadTime = moment();
    }
}

(NB: moment is a JS date/time library we use). On the Home controller, we have:

    public ActionResult CheckSessionTimeout() => View();

    public ActionResult ForceSessionRefresh()
    {
        HttpContext.GetOwinContext()
               .Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Home/CheckSessiontimeout" },
                   OpenIdConnectAuthenticationDefaults.AuthenticationType);

        return null;
    }

I am not sure if any of that is the best way/approach. It's just the best we can do to fix up what seems like a set of difficult constraints with Azure AD and ASP.NET MVC apps (that are not SPAs, not using Web API but are using Ajax calls), relative to where we are coming from where none of this matters with on-premises apps doing Kerberos auth (and our user's expectations that session timeout is nothing they want to see or worry about).

Richard
  • 116
  • 1
  • 6
  • I'm presently dealing with a similar issue. Can you explain more about the OpenIdConnectAuthenticationDefaults.AutehtnicationType constant? Presently hitting ForceSessionRefresh just always returns HTTP 401 and doesn't redirect back to Azure AD. – illvm Mar 10 '17 at 21:49
  • 1
    In the end we got this to go, but it was still a pain. Then I found out we can indeed set our own custom timeout (measured in days) with Azure AD which is what we really wanted (for an internally facing LOB app). See description here: http://www.cloudidentity.com/blog/2016/07/25/controlling-a-web-apps-session-duration-2/ – Richard Mar 12 '17 at 21:48
2

There are two ways to handle this (at least this is how we are doing it in our application; it would be interesting to see what AD gurus have to say about this so that we can also fix it if it is not the right way to to do things):

General Approach - Use Refresh Token

When you get an access token from AD, today you get 3 things back - access token, access token expiry and a refresh token. What you do is cache all three of them in your application. Till the time access token is expired, you can simply use that access token. Once the token is expired, you can make use of refresh token to get a new access token. The method in ADAL you want to use for this purpose is AcquireTokenByRefreshToken.

Having said that, you should not take a hard dependency in your application on Refresh Token. Based on the best practices described here, a refresh token can expire or invalidated. Furthermore based on Vittorio's post, a refresh token is not even returned in ADAL version 3. So you may want to consider that.

Other Approach - Acquire Token Silently

Other approach you could take is acquire a new token silently on behalf of the user once the token expires. I believe this requires that a user must sign in manually at least once in your application and follow the OAuth2 flow. The method you want to use is AcquireTokenSilent.

Here's the pseudo code for our approach:

var now = DateTime.UtcNow.Ticks;
if (now <= tokenExpiry && !string.IsNullOrWhiteSpace(accessToken))
  return accessToken;

var clientCredential = new ClientCredential(ClientId, ClientSecret);
var authContext = new AuthenticationContext(string.Format("{0}/{1}",
                                AzureActiveDirectorySignInEndpoint,
                                azureADTenantId));
AuthenticationResult authResult = null;

if (!string.IsNullOrWhiteSpace(refreshToken))
{
    authResult = await authContext.AcquireTokenByRefreshTokenAsync(refreshToken,
                                clientCredential,
                                ADEndpoint);
}
else
{
    authResult = await authContext.AcquireTokenSilentAsync(Endpoint,
                                clientCredential,
                                new UserIdentifier(userId, UserIdentifierType.UniqueId));
}
    
return authResult.AccessToken;//Also you may want to cache the token again
Zhaph - Ben Duguid
  • 26,785
  • 5
  • 80
  • 117
Gaurav Mantri
  • 128,066
  • 12
  • 206
  • 241