Add role-based authorisation based on Azure AD group membership

This post describes how to use Azure AD groups for role-based authorisation in your ASP.NET web application.

Practical Microsoft Azure Active Directory Blog Series

This post is part of the Practical Microsoft Azure Active Directory Blog Series.

Add role-based authorisation based on Azure AD group membership

These instructions will help you easily add role-based authorisation based on Azure AD group membership to your existing ASP.NET application with Azure AD authentication.  The links show either a commit from the example project or to relevant documentation.

Note: Ignore the ...‘s and replace the {SPECIFIED_VALUES} with the correct values.

  1. Create groups in your Azure AD tenant
  2. Assign your users to relevant groups
  3. Configure your Azure AD application to have application permissions to read directory data from Azure Active Directory
    • If you get a “Insufficient privileges to complete the operation.” exception then you might need to wait for a few minutes or an hour since it seems to cache the old permissions, or it may be the problem mentioned by Jeff Dunlop in the comments
  4. In the Configure tab of your Azure AD application create a key in the keys section and copy it
  5. Configure the client id of your Azure AD application and the key you created in the last step in your web.config file
     <appSettings>
         ...
         <add key="ida:ClientId" value="{AZURE_AD_APP_CLIENT_ID}" />
         <add key="ida:Password" value="{AZURE_AD_APP_KEY}" />
     </appSettings>
    
  6. Install-Package Microsoft.Azure.ActiveDirectory.GraphClient -Version 1.0.3 (alternatively, you can use the latest version if you follow the steps mentioned by Jeff Dunlop in the comments)
  7. Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory
  8. Create an AzureADGraphConnection class:
     // Infrastructure\Auth\AzureADGraphConnection.cs
     using System;
     using System.Collections.Generic;
     using System.Linq;
     using System.Security.Claims;
     using Microsoft.Azure.ActiveDirectory.GraphClient;
     using Microsoft.IdentityModel.Clients.ActiveDirectory;
     namespace {YOUR_NAMESPACE}.Infrastructure.Auth
     {
         public interface IAzureADGraphConnection
         {
             IList<string> GetRolesForUser(ClaimsPrincipal userPrincipal);
         }
         public class AzureADGraphConnection : IAzureADGraphConnection
         {
             const string Resource = "https://graph.windows.net";
             public readonly Guid ClientRequestId = Guid.NewGuid();
             private readonly GraphConnection _graphConnection;
             public AzureADGraphConnection(string tenantName, string clientId, string clientSecret)
             {
                 var authenticationContext = new AuthenticationContext("https://login.windows.net/" + tenantName, false);
                 var clientCred = new ClientCredential(clientId, clientSecret);
                 var token = authenticationContext.AcquireToken(Resource, clientCred).AccessToken;
                 _graphConnection = new GraphConnection(token, ClientRequestId);
             }
             public IList<string> GetRolesForUser(ClaimsPrincipal userPrincipal)
             {
                 return _graphConnection.GetMemberGroups(new User(userPrincipal.Identity.Name), true)
                     .Select(groupId => _graphConnection.Get<Group>(groupId))
                     .Where(g => g != null)
                     .Select(g => g.DisplayName)
                     .ToList();
             }
         }
     }
    
  9. Create an AzureADGraphClaimsAuthenticationManager class:
     // Infrastructure\Auth\AzureADGraphClaimsAuthenticationManager.cs
     using System.Configuration;
     using System.Security.Claims;
     namespace AzureAdMvcExample.Infrastructure.Auth
     {
         public class AzureADGraphClaimsAuthenticationManager : ClaimsAuthenticationManager
         {
             public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
             {
                 if (incomingPrincipal == null || !incomingPrincipal.Identity.IsAuthenticated)
                     return incomingPrincipal;
                 // Ideally this should be the code below so the connection is resolved from a DI container, but for simplicity of the demo I'll leave it as a new statement
                 //var graphConnection = DependencyResolver.Current.GetService<IAzureADGraphConnection>();
                 var graphConnection = new AzureADGraphConnection(
                     ConfigurationManager.AppSettings["AzureADTenant"],
                     ConfigurationManager.AppSettings["ida:ClientId"],
                     ConfigurationManager.AppSettings["ida:Password"]);
                 var roles = graphConnection.GetRolesForUser(incomingPrincipal);
                 foreach (var r in roles)
                     ((ClaimsIdentity)incomingPrincipal.Identity).AddClaim(
                         new Claim(ClaimTypes.Role, r, ClaimValueTypes.String, "GRAPH"));
                 return incomingPrincipal;
             }
         }
     }
    
  10. Configure your application to use the AzureADGraphClaimsAuthenticationManager class for processing claims-based authentication in your web.config file:
    <system.identityModel>
        <identityConfiguration>
        <claimsAuthenticationManager type="{YOUR_NAMESPACE}.Infrastructure.Auth.AzureADGraphClaimsAuthenticationManager, {YOUR_ASSEMBLY_NAME}" />
        ...
        </identityConfiguration>
    </system.identityModel>
    
  11. Add [Authorize(Roles = "{AZURE_AD_GROUP_NAME}")] to any controller or action you want to restrict by role and call User.IsInRole("{AZURE_AD_GROUP_NAME}") to check if a user is a member of a particular group

Explaining the code

Microsoft.Azure.ActiveDirectory.GraphClient and AzureADGraphConnection

The ActiveDirectory.GraphClient provides a wrapper over the Azure AD Graph API, which allows you to query the users, groups, etc.

The AzureADGraphConnection class constructs a graph client connection and a method to take a user and return a list of the groups that user is a member of.

This is needed because the claims that the Azure AD token comes with by default do not include any roles.

AzureADGraphClaimsAuthenticationManager

This class provides a claims authentication manager that hooks into the point that authentication occurs and augments the Claims Principal that is generated by default by getting the Azure AD Groups that the user is a member of (via AzureADGraphConnection) and turning them into a ClaimTypes.Role claim. ClaimTypes.Role is the claim type that automatically hooks into ASP.NETs roles processing.

The web.config change is how you override the Claims Authentication Manager.

Using an enum for roles

To avoid the use of magic strings in your application and assuming the group names in AD are relatively stable you can encapsulate them in an enum. There is a corresponding commit in the example project that demonstrates how to do it.

This involves three main steps:

  1. Define an enum with your roles and using the [Description] attribute to tag each role with the Display Name of the equivalent Azure AD group
  2. Parse the group name into the corresponding enum value by using Humanizer.Dehumanize in AzureADGraphConnection
  3. Create an AuthorizeRolesAttribute that extends AuthorizeAttribute and an extension on IClaimsPrincipal that provides an IsInRole method that both take the enum you defined rather than magic strings to define the roles