Graph API access tokens and ASP .NET session expiration

Let’s assume you have created an ASP .NET-based Web application using the default Visual Studio project template, and that you have set up authentication to be based on Work and School Accounts (i.e. Azure based), supporting multiple tenants, and targeting Office 365 users, for example:

AzureAuth.png

You now have code that performs all Azure authentication calls for you, allowing end users to logon to your application using their Office 365 accounts. Crazy, ain’t it? But it works! Kinda.

The default template doesn’t complete the job, however. It doesn’t add ay code to handle receiving the authorization code, needed later to generate an access token required for Graph API calls, but you can do it yourself (in App_Start/StartupAuth.cs file, adding an AuthorizationCodeReceived handler):

string authorizationCode = null;
app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        ...,
        RedirectUri = "https://...",
        Notifications = new OpenIdConnectAuthenticationNotifications()
        {
            ...,   
            AuthorizationCodeReceived = (context) =>
            {
                authorizationCode = context.Code;
                return Task.FromResult(0);
            },
        }
    });
app.UseStageMarker(PipelineStage.Authenticate);


Note that the RedirectUrl value is needed when you release your app to the cloud (but it’s not added by default in your code), and should be the same as the return URL you will configure later, on acquiring token by authorization code (see below).

And if you want to support multiple environments (e.g. be able to test both on localhost and from the public application URL) you will need to configure this in Web.config and have appropriate transforms for different configurations. For example in Web.config you can have an appSettings entry like this:

  <appSettings>
    ...
    <add key="ApplicationUri" value="https://localhost:port/" />
  </appSettings>

And in Web.Release.config you can update the element:

  <appSettings>
    <add key="ApplicationUri" value="https://appname.azurewebsites.net/" 
         xdt:Locator="Match(key)" xdt:Transform="SetAttributes(value)" />
  </appSettings>

Eventually, you can read the appropriate element value from StartupAuth.cs file:

private static string applicationUri = ConfigurationManager.AppSettings["ApplicationUri"];
...
app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        ...,
        RedirectUri = applicationUri,
        ...
    });

At this point, you may think you can save the authorizationCode value into the Session. But trying to use HttpContext.Current.Session inside the AuthorizationCodeReceiveHandler will not work: Session itself is not yet initialized at that point, and you’d only get a runtime exception.

To resolve the issue you should hold the authorization code value into a local variable until the session object becomes available. Add the following lines of code after the UseStageMarker call above:

app.Use((context, next) =>
{
    var httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
    var session = httpContext.Session;
    if (session != null && session["AccessToken"] == null && authorizationCode != null)
    {
        var authContext = new AuthenticationContext(authority);
        var authResult = authContext.AcquireTokenByAuthorizationCodeAsync(
            authorizationCode,
            new Uri("https://...", UriKind.Absolute),
            new ClientCredential(clientId, clientSecret),
            "https://graph.microsoft.com").Result;
        session["AccessToken"] = authResult.AccessToken;
        authorizationCode = null;
    }
    return next();
});

Notice that the authorizationCode is used to obtain a token only when httpContext.Session is not null, and only when an acces token is needed (we don’t yet have it in session).

This works, and AccessToken is set up correctly. You can use it from all app pages that require authenticated context, and you can call the Graph API as you need, impersonating the logged on Office 365 end user. As the documentation states, the token is valid for 1 hour. Great!

Still, there is a big remaining issue! You’d notice it if you you’d start the application in a browser and leave it there more than 20 minutes but less than 1 hour. Then open (or refresh) a page that calls the Graph API using the access token and see what happens. At that time the ASP .NET session would have expired, and the access token is lost! And the Azure authentication hasn’t got automatically recalled either, since the authorization code could have been used to generate a token that is still valid at the time, and therefore the authorization code notification handler has not yet been called a second time. Your app will not be able to access the Graph API anymore, but the user won’t be redirected automatically to the signing in page either! Not so great anymore!

There is an easy way to resolve the problem, tough: just synchronize the ASP .NET session timeout with the 60 minutes’ validity of the access token. You can do that directly in the previous set of statements, such as before setting authorizationCode value to null:

        ...
        session["AccessToken"] = authResult.AccessToken;
        session.Timeout = 60;
        authorizationCode = null;
        ...

By the way, setting authorizationCode variable to null after you have obtained the access token is optional from a technical point of view, but I personally think it’s important to clear it once you used its value both for security reasons and increased code elegance.

PS: For anyone interested, a Graph API call in a standard page would look like this:

var accessToken = Session["AccessToken"] as string;
using (var client = new HttpClient())
{
    using (var request = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/..."))
    {
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        using (var response = client.SendAsync(request).Result)
        {
            var jsonContent = response.Content.ReadAsStringAsync().Result;
            var json = JObject.Parse(jsonContent);
            ...
        }
    }
}

 

Advertisements

About Sorin Dolha

My passion is software development, but I also like physics.
This entry was posted in ASP .NET and tagged , , , , , , , , , , . Bookmark the permalink.

Add a reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s