Accessing Azure AD protected resources using OpenID Connect

Last time we had a look at the canonical OAuth2 Authorization Grant and tested it with ASP.NET Cored based API and web applications. We had identified key characteristics of the flow and emphasized authorization nature of it and the OAuth2 protocol in general. This time let's have a look at the user identity side of the story and the OpenID Connect protocol that reveals the identity to client applications.

Pretty much everything related to setting applications up in Azure Active Directory that I described in the earlier post applies here as well so I am not going to repeat it. Configuring the API application middleware to handle JWT tokens stays the same too so in this post we're mostly going to focus on the client application.

OpenID Connect

But first let's have a quick introduction of OpenID Connect and the scenarios it supports. As defined in the specification:

OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It enables Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.

It brings an additional artifact to the game called 'ID token' that is client side parsable and verifiable and contains information about the user identity. There are 3 flows in the protocol that makes it applicable in various scenarios:

Authorization Code flow

This flow is very similar to the OAuth2 Authorization Code Grant and we get the ID token when we redeem the authorization code against the token endpoint.

OpenID Connect Authorization Code flow

We identify the flow we want with the response_type parameter in the request to the authorization endpoint. For the current flow we just want to get a code back so we set it to code and get the ID token later from the token endpoint. This is somewhat more secure as the 'ID token' is retrieved as a result of the server to server call. Also note the scope parameter that must include openid value to indicate to the authorization server that we are talking OpenID Connect.

Normally there will also be state and nonce parameters that are used to prevent forgery. state can be used together with browser cookies to prevent CSRF and in fact state is also recommended in vanilla OAuth2. nonce is generated by the client and ties the authentication session with the issued ID token as the authorization server will include in the token and the client can verify it by comparing the value from the token with a values it stored somewhere, e.g. a cookie.

Even though the response from the authorization framework is presented as a 302 redirect on the diagram it really depends on yet another parameter called response_mode. There are a couple of additional specifications (this and this) that define 3 possible values in total: query, fragment and form_post. The last one is interesting and is the default used by the OpenID Connect middleware for ASP.NET Core that we're going to configure in a moment. Instead of redirecting back to the client app, it makes Azure AD return a form containing the requested artifacts (in our case code) that auto-posts itself to the client app with a little help of some embedded JavaScript. On the diagram we can see the case when query was used as the response mode and this may be somewhat less optimal as the artifacts may get saved in the browser history for a while.

Implicit flow

This flow is pretty close to the OAuth2 Implicit Grant and is to be used by non-confidential clients such JavaScript or native applications. The idea is that access and ID tokens are returned directly from the authorization endpoint and clients are not authenticated.

OpenID Connect Implicit flow

In OpenID Connect the response_type should be set to either id_token or id_token token to enable the flow. If we don't need to call other services and we just want to perform a federated authentication we can only request 'id_token' from the endpoint.

Tokens are returned in the URI fragment (notice the # sign) and thus remain seen on the client side only. User agent performs the requested redirect and the client app returns a page with embedded JavaScript that is able to retrieve the tokens from the fragment.

Hybrid flow

As the name implies this flow allows us to decide when we want to return any of the artifacts. The acceptable values for the response_type parameter are code id_token, code token or code id_token token.

OpenID Connect Hybrid flow

Sometimes we want to get the ID token earlier with a response from the authorization endpoint to be able to set up a security context in our client applications before we call the token endpoint.

UserInfo and metadata endpoints

There are a couple of more things I'd like to mention before I wrap up this quick introduction to OpenID Connect. While ID token provides minimal viable information about the user identity to the client application, the authority also exposes a UserInfo endpoint (/userinfo) that we can use to obtain additinal claims about the user.

Also, part of the OpenID Connect discovery the authority implements a metadata endpoint that relying parties can use to get the necessary metadata to validate tokens (both ID and access ones). The metadata endpoint is normally exposed at the following address: <authority URL>/.well-known/openid-configuration and contains, among others, URLS of authorization and token endpoints, issuer, UserInfo endpoint and JSON Web Key Set that contains public keys we should use to verify the signature of JWT tokens (both ID and access ones).

Configure OpenID Connect middleware in ASP.NET Core

Now that we have a general understanding of the protocol let's see how we can configure the OpenID Connect middleware in an ASP.NET Core web application to work with Azure Active Directory.

We actually need to add a couple of middleware to the pipeline:

"dependencies": {
  "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-rc2-final",
  "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0-rc2-final",
  "Microsoft.IdentityModel.Clients.ActiveDirectory": "3.10.305231913"
}

We need the cookies middleware too as normally in web applications a successful authentication results in a cookie being added to the response so that subsequent requests wouldn't require the user to go through the authentication process all over again.

Active Directory Authentication Library (ADAL)

The OpenID Connect middleware is not Azure AD specific and can work with just about any identity provider that implements the protocol. Now the problem is that Azure AD has its own dialect as it requires a resource parameter being added to requests to its token endpoints. And while the OpenID Connect middleware is able to redeem the authorization code on its own it won't work because it won't add this parameter to the request. Note that I'm talking about v1 endpoints of Azure AD and things are different in v2 which are currently in preview.

Besides support for the Azure AD dialect we should also take care about persistence of the tokens and handling token refresh. In my previous post I used cookies to store tokens which has its pros and cons but when combined with the task of handling token refresh server side persistence starts looking as a preferable option.

We are going to use the Active Directory Authentication Library (aka ADAL) to help us with all of these issues: dialect, token persistence and refresh. There are versions of the library for various platforms and languages and its source is open and available at GitHub.

ADAL provides an in memory token cache however it has extensibility points that allow us to persist the serialized cache to and load it from the storage of our choice as needed. You can read more about the cache and its model on Vittorio Bertocci's blog.

What about refreshing tokens? ADAL handles it too. In fact, v3 of the library stopped exposing refresh tokens from the AuthenticationResult object that we get after calling token endpoints. When you request an access token from ADAL (the cache to be exact) and it finds out that the token has already expired or about to get expired and there is a valid refresh token in the cache, ADAL will issue a request to the token endpoint with the refresh token, put the new tokens in the cache and notify you to persist the updated cache. Of course it will return the newly obtained access token for you to use.

This is really handy and it frees us from writing this somewhat tedious infrastructural logic.

Back to configuring the middleware

public void Configure(IApplicationBuilder app, 
    IHostingEnvironment env, 
    ILoggerFactory loggerFactory,
    IOptions<Infrastructure.Authentication.AuthenticationOptions> authOptions)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AutomaticAuthenticate = true
    });

    app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
    {
        AutomaticChallenge = true,

        Authority = authOptions.Authority,
        ClientId = authOptions.ClientId,
        ClientSecret = authOptions.ClientSecret,

        ResponseType = OpenIdConnectResponseTypes.CodeIdToken,

        SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
        PostLogoutRedirectUri = authOptions.PostLogoutRedirectUri,

        Events = CreateOpenIdConnectEventHandlers(authOptions)
    });
}

private static IOpenIdConnectEvents CreateOpenIdConnectEventHandlers(AuthenticationOptions authOptions)
{
    return new OpenIdConnectEvents
    {
        OnAuthorizationCodeReceived = async context =>
        {
            var clientCredential = new ClientCredential(authOptions.ClientId, authOptions.ClientSecret);
            var authenticationContext = new AuthenticationContext(authOptions.Authority);
            await authenticationContext.AcquireTokenByAuthorizationCodeAsync(context.TokenEndpointRequest.Code,
                new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute), 
                    clientCredential, authOptions.ApiResource);

            context.HandleCodeRedemption();
        }
    };
}

I can't help but directing you again to my post on configuring the OAuth2 middleware for the Authorization Code Grant because most of the options have already been explained there.

You've probably noticed that we set response_type to code id_token which effectively enables the hybrid flow and we get the ID token when we get the control back from the authorization endpoint.

And we also intercept the OnAuthorizationCodeReceived event to take adavantage of ADAL to redeem the code and cache tokens. We make sure to notify the OpenID Connect middleware by calling context.HandleCodeRedemption() that we've handled this part and it doesn't need to try to redeem the code on its own.

Requesting an access token from ADAL

When we need to make a call to a protected resource we should first get the access token from ADAL and then add to our request to the resource. Here's what it would normally look like in a web application:

public static async Task<string> AcquireAccessTokenAsync(AuthenticationOptions authOptions)
{
    var clientCredential = new ClientCredential(authOptions.ClientId, authOptions.ClientSecret);
    var authenticationContext = new AuthenticationContext(authOptions.Authority);

    try
    {
        var authenticationResult = await authenticationContext.AcquireTokenSilentAsync(authOptions.ApiResource,
            clientCredential, UserIdentifier.AnyUser);

        return authenticationResult.AccessToken;
    }
    catch (AdalSilentTokenAcquisitionException)
    {
        // TODO: log it or do whatever makes sence for your app
        return null;
    }
}

That AcquireTokenSilentAsync is where all the ADAL magic happens. And it will also take advantage of the multi-resource nature of Azure AD refresh tokens should we request an access token for a resource that is different from the one we initially used in OnAuthorizationCodeReceived event handler (if the token for that resource hasn't been cached yet).