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

Explaining the code behind authenticating MVC5 app with Azure AD

This post explains the code outlined in the last post on installing Azure AD authentication to an existing (or new) ASP.NET MVC 5 (or 3 or 4) application.

Practical Microsoft Azure Active Directory Blog Series

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

Microsoft.Owin.Security.ActiveDirectory

The Microsoft.Owin.Security.ActiveDirectory package is part of the Katana project, which produces a bunch of libraries that build on top of Owin.

It allows your application to accept a Bearer Authorization header in the HTTP request that contains a JSON Web Token (JWT) token issued from Azure AD and will create a ClaimsPrincipal in the thread from that token. This is mainly useful for creating Web APIs and thus is optional if you just need web authentication.

Note: if you use bearer tokens make sure you request resources with HTTPS.

This package is enabled up by the app.UseWindowsAzureActiveDirectoryBearerAuthentication(...) call in Startup.cs.

There are two configurations in the Startup.cs code to configure the package:

  • TokenValidationParameters – this controls how tokens that are presented by a user are checked for validity
    • In the code example in the previous blog post we set ValidAudience, which ensures that any tokens presented are valid for the given audience (alternatively, you can use ValidAudiences if you want to accept tokens from multiple audiences)
    • There is more information later in this post about audiences
  • Tenant – This sets which Azure AD tenant you are accepting tokens from

WSFederationAuthenticationModule (WS-FAM) and SessionAuthenticationModule (SAM)

These modules are part of WIF via System.IdentityModel.Services and are the mechanism by which the authentication hooks into ASP.NET and works. For this to work you need to enter a bunch of code in web.config, but Microsoft is currently working on OWIN-only components that hide all of that away and provide for a much simpler configuration so in the future you won’t need to do any of this. At the time of writing the samples don’t quite seem to work (for me at least) yet, so for now it makes sense to keep using the WIF modules, but it’s worth keeping out an eye on what happens with the samples Microsoft are working on, in particular the OpenIdConnect one.

So what do these WIF modules do? From the WSFederationAuthenticationModule documentation:

When an unauthenticated user tries to access a protected resource, the [Relying Party (RP)] returns a “401 authorization denied” HTTP response. The WS-FAM intercepts this response instead of allowing the client to receive it, then it redirects the user to the specified [Security Token Service (STS)]. The STS issues a security token, which the WS-FAM again intercepts. The WS-FAM uses the token to create an instance of ClaimsPrincipal for the authenticated user, which enables regular .NET Framework authorization mechanisms to function.

Because HTTP is stateless, we need a way to avoid repeating this whole process every time that the user tries to access another protected resource. This is where the SessionAuthenticationModule comes in. When the STS issues a security token for the user, SessionAuthenticationModule also creates a session security token for the user and puts it in a cookie. On subsequent requests, the SessionAuthenticationModule intercepts this cookie and uses it to reconstruct the user’s ClaimsPrincipal.

From the SessionAuthenticationModule documentation:

The SAM adds its OnAuthenticateRequest event handler to the HttpApplication.AuthenticateRequest event in the ASP.NET pipeline. This handler intercepts sign-in requests, and, if there is a session cookie, deserializes it into a session token, and sets the Thread.CurrentPrincipal and HttpContext.User properties to the claims principal contained in the session token.

These modules are then configured by the system.identityModel and system.identityModel.services sections in web.config.

issuerNameRegistry and signing key refresh

This configures which tenants and issuers of authentication your application trusts as well as the thumbprint of their public signing key certificates.

The certificate thumbprints will change over time for security reasons so hardcoding the keys in web.config is not a good option hence you need to make sure to implement code that changes the keys for you. The simplest, built-in way to do that is using ValidatingIssuerNameRegistry.WriteToConfig, which updates web.config for you automatically when it changes. That’s the instruction that was given in the last blog post.

Another option is to store the keys in a database, which is what the default code that Visual Studio’s Identity and Access Tools add (using EntityFramework). Yet another option is to store them in-memory and there is a class floating about that you can use to do that. Storing it in-memory is probably the nicest option.

Audiences and realm

The audienceUris configuration in web.config allows your application to control a list of identifiers that will be accepted for the scope of a presented authentication token (in Azure AD this maps to the App ID URI of your Azure AD application).

The realm attribute in the wsFederation element of the federationConfiguration in web.config tells the WSFederationAuthenticationModule to specify the WS Federation wtrealm to use for the request, which identifies the security realm that will be used for the request. For Azure AD this provides the App ID URI of the Azure AD application that should be used to service the authentication request. Thus, this value will generally be the same as the audience you configured unless you are doing multi-tenancy.

The difference between realm and audience is explained in this StackOverflow post.

securityTokenHandlers

A security token handler provides a way for interpreting a security token of some sort into a Claims Principal for a given request. The code in the previous post gets you to remove the default handler that takes care of looking at cookies in the request, SessionSecurityTokenHandler, because it uses DPAPI to encrypt and decrypt the cookies and that doesn’t work in an environment with multiple web servers. Instead you are guided to add a MachineKeySessionSecurityTokenHandler, which uses the machine key to encrypt and decrypt the cookie.

The configured securityTokenHandler will be what the SessionAuthenticationModule will make use of to store and retrieve the token from and to the cookie.

certificateValidation

On first thought, it might be confusing to see that certificate validation is turned off, but this is by design. The validating issuer name registry as explained above is a replacement for standard certificate validation. The only information that I’ve been able to find that explains this further is a post on the MSDN forums.

To illustrate what happens when you try using certificate validation, you can change certificateValidationMode to, say, ChainTrust and then you will get the following error:

The X.509 certificate CN=accounts.accesscontrol.windows.net chain building failed. The certificate that was used has a trust chain that cannot be verified. Replace the certificate or change the certificateValidationMode. A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider.

HTTPS

You can ensure that the security cookie is set to require SSL with the requireSSL attribute of the cookieHandler element in web.config and ensure that the authentication requests require a HTTPS connection with the requireHttps attribute of the wsFederation element in web.config.

In production environments it’s absolutely essential that you set both to true otherwise you are vulnerable to MITM attacks. You can set them to true locally if you use https with IIS Express or via a self-signed cert with IIS.

issuer

Setting this attribute on the wsFederation element in web.config determines where sign-in and sign-out requests are redirected.

passiveRedirectEnabled

Setting this attribute on the wsFederation element in web.config to true allows the WSFederationAuthenticationModule to automatically redirect the user to the authentication server in the event of a 401. Without this set to true you would need to explicitly call SignIn to log the user in.

reply

Setting this attribute on the wsFederation element in web.config allows the application to control where the user is taken after they authenticate. The last post didn’t tell you to set it because by default it’s not required. When using Azure AD, in the instance that it’s not specified, the user will be redirected to the first Reply URL specified in the Azure AD application.

This requires that you can only have a one-to-one relationship between an Azure AD application and a web application requiring authentication. You can actually add multiple reply URLs to your Azure AD application though. This fact in combination with the reply attribute means that you can support multiple web applications (e.g. local, dev, staging, prod or even just different applications altogether) with the same Azure AD application. You just need to config transform your web.config file for each different environment as explained in this post.

If you are in a situation where you want to only change an app setting to control the reply URL (e.g. you are using VSO to deploy different branches to separate Azure Web Sites) then you can change the reply URL in code like so:

    public static class IdentityConfig
    {
        public static void ConfigureIdentity()
        {
            ...
            FederatedAuthentication.FederationConfigurationCreated += FederatedAuthentication_FederationConfigurationCreated;
        }

        ...

        private static void FederatedAuthentication_FederationConfigurationCreated(object sender, FederationConfigurationCreatedEventArgs e)
        {
            var federationConfiguration = new FederationConfiguration();
            federationConfiguration.WsFederationConfiguration.Reply =
                ConfigurationManager.AppSettings["AuthenticationReplyUrl"];
            e.FederationConfiguration = federationConfiguration;
        }
    }

Anti-forgery config

AntiForgeryConfig.UniqueClaimTypeIdentifier allows you to set which claim from the claims-based authentication token can be used to uniquely identify a user for the purposes of creating an appropriate anti-forgery token. Side note: this post about anti-forgery token is great. The claim that was shown in the last post is the correct one to use for Azure AD.

Logout

There are two parts to the logout code in the last post. Firstly, there is a call to the SessionAuthenticationModule, which will cause the cookie you have on the current site to be dropped by your browser. Secondly, there is a redirect to a URL that the WS Federation code generates that will log you out of the source system (in this case your Azure AD session) and then redirect you back to a callback page with [AllowAnonymous] so they don’t get redirected back to login again straight away.