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 authenticationContext = new AuthenticationContext(
    $"https://login.windows.net/{tenant}", validateAuthority: true, TokenCache.DefaultShared);

  // Secret key that can be generated in the Azure portal to enable authentication of a
  // specific 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 context =>
        {
          try
          {
            // Retrieve user JWT token from request.
            var authorizationHeader = context.Request.Headers["Authorization"].First();
            var userJwtToken = authorizationHeader.Substring("Bearer ".Length).Trim();

            // Get current user identity from authentication ticket.
            var authenticationTicket = context.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 authenticationResult = await authenticationContext.AcquireTokenAsync(
              azureGraphApiUrl, clientCredential, userAssertion).ConfigureAwait(false);

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

            // Get current user groups.
            var pagedUserGroups =
              await activeDirectoryClient.Me.MemberOf.ExecuteAsync().ConfigureAwait(false);
            do
            {
              // Collect groups and add them as role claims to our current 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.

Related articles

  • Cloud Native
  • Implementation and Adoption
  • Platform Engineering
  • Hybrid Cloud
  • Private: ITTS (IT Transformation Services)
  • Private: Managed Security Operations
  • Managed Cloud Platform
  • Private: Backup & Disaster Recovery
Visit our knowledge hub
Visit our knowledge hub
ITQ

Let's talk!

Knowledge is key for our existence. This knowledge we use for disruptive innovation and changing organizations. Are you ready for change?

"*" indicates required fields

First name*
Last name*
Hidden