Combining ADAL JS with role-based security in ASP.NET Web API

In October 2014, Vittorio Bertocci introduced ADAL JavaScript. This library makes it possible for single-page-apps to use Azure Active Directory authentication from within the browser. ADAL JS uses the OAuth2 Implicit Grant for this. The introductory blog post and an additional post when v1 was released explain in detail how to configure and use the library.

One interesting 'limitation' of implicit grants is that the access token you receive once you're authenticated, is returned in a URL fragment. This limits the amount of information that can be stored inside the token since URL's have a limited length (this varies per browser and server). This means that the token does not contain any role information, otherwise the URL might become too long. So even when you're a member of one or more groups in Azure AD, this information will not be exposed through the access token.

So, what to do when you actually wanted to use role-based authorization in your backend API? Luckily, there is the Azure AD Graph API for that1. It allows you to access the users and groups from your Azure AD tenant. The flow is then as follows:

  1. ADAL JS sees the current user is not authenticated and redirects the browser to the configured Azure AD endpoint.
  2. The user authenticates and a token is returned to the browser in a URL fragment. ADAL JS extracts some information from the token for use by a client-side script and stores the token itself in session storage or local storage (this is configurable). Note that ADAL JS does not actually validate the token, this is the backend's job.
  3. On subsequent requests to the backend API (in my case an ASP.NET Web API), the token is sent along in the Authorization header as a bearer token.
  4. In the backend API the token is validated and during the validation process, we use the Graph API to get more information about the user: the groups he or she is a member of.
  5. The groups are added as role claims to the authenticated principal.

In code, this looks like this. I use the aptly named extension method UseWindowsAzureActiveDirectoryBearerAuthentication from the Microsoft.Owin.Security.ActiveDirectory NuGet package to add the necessary authentication middleware to the Owin pipeline. I left out some of the necessary error handling and logging.

// Apply bearer token authentication middleware to Owin IAppBuilder interface.
private void ConfigureAuth(IAppBuilder app)  
{
  // ADAL authentication context for our Azure AD tenant.
  var authContext = new AuthenticationContext(
    $"https://login.windows.net/{tenant}", validateAuthority: true,
    TokenCache.DefaultShared);

  // Secret key, generated in the Azure portal to enable authentication of
  // an application (in this case our Web API) against an Azure AD tenant.
  var applicationKey = ...;

  // Root URL for Azure AD Graph API.
  var azureGraphApiUrl = "https://graph.windows.net";
  var graphApiServiceRootUrl = new Uri(new Uri(azureGraphApiUrl), tenantId);

  // Add bearer token authentication middleware.
  app.UseWindowsAzureActiveDirectoryBearerAuthentication(
    new WindowsAzureActiveDirectoryBearerAuthenticationOptions
    {
      // The id of the client application that must be registered in Azure AD.
      TokenValidationParameters =
        new TokenValidationParameters { ValidAudience = clientId },
      // Our Azure AD tenant (e.g.: contoso.onmicrosoft.com).
      Tenant = tenant,
      Provider = new OAuthBearerAuthenticationProvider
      {
        // This is where the magic happens. In this handler we can perform
        // additional validations against the authenticated principal or
        // modify the principal.
        OnValidateIdentity = async ctx =>
        {
          try
          {
            // Retrieve user JWT token from request.
            var authorizationHeader =
                ctx.Request.Headers["Authorization"].First();
            var userJwtToken =
                authorizationHeader.Substring("Bearer ".Length).Trim();

            // Get current user identity from authentication ticket.
            var authenticationTicket = ctx.Ticket;
            var identity = authenticationTicket.Identity;

            // Credential representing the current user. We need this to
            // request a token that allows our application access to the
            // Azure Graph API.
            var userUpnClaim = identity.FindFirst(ClaimTypes.Upn);
            var userName = userUpnClaim == null
              ? identity.FindFirst(ClaimTypes.Email).Value
              : userUpnClaim.Value;
            var userAssertion = new UserAssertion(userJwtToken,
                "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);

            // Credential representing our client application in Azure AD.
            var clientCredential = new ClientCredential(clientId, applicationKey);

            // Get a token on behalf of the current user that lets Azure AD
            // Graph API access our Azure AD tenant.
            var authResult = await authContext.AcquireTokenAsync(
              azureGraphApiUrl, clientCredential, userAssertion).ConfigureAwait(false);

            // Create Graph API client and give it the acquired token.
            var adClient = new ActiveDirectoryClient(graphApiServiceRootUrl,
                () => Task.FromResult(authResult.AccessToken));

            // Get current user groups.
            var pagedUserGroups = await
              adClient.Me.MemberOf.ExecuteAsync().ConfigureAwait(false);
            do
            {
              // Collect groups and add them as role claims to our principal.
              var directoryObjects = pagedUserGroups.CurrentPage.ToList();
              foreach (var directoryObject in directoryObjects)
              {
                var group = directoryObject as Group;
                if (group != null)
                {
                  // Add ObjectId of group to current identity as role claim.
                  identity.AddClaim(
                      new Claim(identity.RoleClaimType, group.ObjectId));
                }
              }
              pagedUserGroups = await
                  pagedUserGroups.GetNextPageAsync().ConfigureAwait(false);
            } while (pagedUserGroups != null);
          }
          catch (Exception e)
          {
            throw;
          }
        }
      }
    });
}

Quite a lot of code (and comments) but the flow should be rather easy to follow:

  1. First we extract the token that ADAL JS gave us from the HTTP request.
  2. Using this token and another uniquely identifying characteristic of the user2 we create a UserAssertion that represents the current user.
  3. With the user assertion and a credential that represents our registered application in Azure AD we ask the ADAL AuthenticationContext for a token that gives our application access to the Azure Graph API on behalf of the current user.
  4. With this token, we use the ActiveDirectoryClient class from the Graph API library to obtain information on the current user. You might wonder how this client knows who the 'current user' is. This is determined by the token we provided: remember we asked for a token on-behalf-of the current user. An additional advantage is that we only need minimal access rights for our application: a user should be able to read his own groups.
  5. The groups the user is a member of are added as role claims to the current principal.
Access rights for the Graph API

The Graph API is an external application that we want to use from our own application. We need to configure the permissions our application requires from the Graph API to be able to retrieve the necessary information. Only two delegated permissions are needed:

Delegated permissions

Notes
  1. The Azure AD Graph API is being replaced by Microsoft Graph. However, this is still very much beta so I chose not to use it (yet).
  2. The UserAssertion class also has a constructor that accepts just a token and no other information that could uniquely identify a user. Using this constructor causes a serious security issue with the TokenCache.DefaultShared that we use. Tokens that should be different because we obtained them via a different user assertion, are regarded as equal by the cache. This may cause a cached token from one user to be used for another user.