OdeToCode IC Logo

Role Based Authorization in ASP.NET Core with Azure AD Groups

Tuesday, February 20, 2018

Azure Active DirectoryAuthenticating users in ASP.NET Core using OpenID Connect and Azure Active Directory is straightforward. The tools can even scaffold an application to support this scenario.

In this post I want to go one step further and define authorization rules based on a user’s group membership in Azure AD.

Those Tired Old Intranet Apps

While the authentication picture is clear, authorization can be blurry. Authorization is where specific business rules meet software, and authorization requirements can vary from application to application even in the same organization. Not only will different applications need different types of authorization rules, but the data sources needed to feed data into those rules can vary, too.

Over the years, however, many applications have used group membership in Windows Active Directory (AD) as a source of information when making authorization decisions. Group membership in AD is reliable, and static. For example, a new employee in the sales department who is placed into the “Sales” group will probably remain in the sales group for the rest of their term.

Basing authorization rules on AD group membership was also easy in these apps. For ASP.NET developers building applications using IIS and Windows Authentication, checking a user’s group membership only required calling an IsInRole method.

These New-Fangled Cloud Apps

Cloud native applications trade Windows Active Directory for Azure Active Directory and move away from Windows authentication protocols like NTLM and Kerberos to Internet friendly protocols like OpenID Connect. In this scenario, an organization typically synchronizes their Windows Active Directory into Azure AD with a tool like ADConnect. The synchronization allows users to have one identity that works inside the firewall for intranet resources, as well as outside the firewall with services like Office 365.

Windows Active Directory and Azure Active Directory are two different creatures, but both directories support the concepts of users, groups, and group membership. With synchronization in place, the group membership behind the firewall are the same as the group memberships in the cloud.

Imagine we have a group named “sales” in Azure AD. Imagine we want to build an application like the old days where only users in the sales group are authorized to use the application.

Application Setup

I’m going to assume you already know how to register an application with Azure AD. There is plenty of documentation on the topic.

Unlike the old days, group membership information does not magically appear in an application when using OIDC. You either need to use the Graph API to retrieve the groups for a specific user after authenticating, which we can look at in a future post if there is interest, or configure Azure AD to send back claims representing a user’s group membership. We’ll take the simple approach for now and configure Azure AD to send group claims. There is a limitation to this approach I’ll mention later.

Configuring Azure AD to send group claims requires a change in the application manifest. You can change the manifest using the AD graph API, or in the portal. In the portal, go to App registrations => All apps => select the app => click the manifest button on the top action bar.

Edit the Application Manifest

The key is the “groupMembershipClaims” property you can see in the bottom screenshot. Set the value to “SecurityGroup” for Azure to return group information as claims. The app manifest includes a number of settings that you cannot reach through the UI of the portal, including appRoles. You'll probably want to define appRoles if you are building a multi-tenant app.

Testing Claims

With the above manifest in place, you should see one or more claims named “groups” in the collection of claims Azure AD will return. An easy way to see the claims for a user is to place the following code into a Razor page or Razor view:

<table class="table">
    @foreach (var claim in User.Claims)
    {
        <tr>
            <td>@claim.Type</td>
            <td>@claim.Value</td>
        </tr>
    }
</table>

With the default claim processing in ASP.NET Core (more on that in a future post), you’ll see something like the following for a user authenticated by Azure AD.

Claims from AAD

For group membership you'll want to focus on the groups claims. The value of the claims for AD groups will be object IDs. You’ll need to know the object ID of the group or groups your application considers important. You can look in the Azure portal for the IDs or use the Azure CLI.

az>> ad group show --group Sales
{
    "displayName": "Sales",
    "mail": null,
    "objectId": "c5038c6f-c5ac-44d5-93f5-04ec697d62dc",
    "objectType": "Group",
    "securityEnabled": true
}
With the ID in hand, you can now define an ASP.NET Core authorization policy.

Defining Authorization Policy

The authorization primitives in ASP.NET Core are claims and policies. Claims hold information about a user. Policies encapsulate simple logic to evaluate the current user against the current context and return true to authorize a user. For more sophisticated scenarios, one can also use authorization requirements and handlers in ASP.NET Core, but for group membership checks we can use the simpler policy approach.

Many people will place policy definitions inline in Startup.cs, but I prefer to keep some helper classes around and organized into a folder to make policy definitions easier to view. A helper class for a Sales policy could look like the following.

public static class SalesAuthorizationPolicy 
{
    public static string Name => "Sales";

    public static void Build(AuthorizationPolicyBuilder builder) =>
        builder.RequireClaim("groups", "c5038c6f-c5ac-44d5-93f5-04ec697d62dc");    
}


In Startup.cs, we use the helper to register the policy.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthorization(options =>
    {
        options.AddPolicy(SalesAuthorizationPolicy.Name, 
                          SalesAuthorizationPolicy.Build);
    });

    // ...

    services.AddMvc();
}

Applying Policy

There are many places where you can use a named policy in ASP.NET Core. There’s the Authorize attribute that’s been around forever.

[Authorize("Sales")]
public class HomeController : Controller
{
   // ...
}

However, I strongly encourage developers to build custom attributes to be more expressive and hide string literals.

public class AuthorizeSales : AuthorizeAttribute
{
    public AuthorizeSales() : base(SalesAuthorizationPolicy.Name)
    {
    }
}

// elsewhere in the code ...

[AuthorizeSales]
public class ReportController : Controller
{
}

For imperative code, inject IAuthorizationService anywhere and use the AuthorizeAsync method.

public async Task Tessalate(IAuthorizationService authorizationService)
{
    var result = await authorizationService.AuthorizeAsync(
                        User, SalesAuthorizationPolicy.Name);
    if (result.Succeeded)
    {
        // ... 
    }
}
You can also protect Razor Pages with a named policy.

services.AddMvc()
        .AddRazorPagesOptions(o =>
        {
             o.Conventions.AuthorizeFolder("/Protected", 
                    SalesAuthorizationPolicy.Name);
        });

Claims, Overages, and What’s Next

In larger organizations a user might be in hundreds of groups. If a user is in more than 250 groups, you’ll need to fall back to using the Graph API as Azure AD will not respond with the full list of user groups. Even if the user is only in 5 groups, your application may only care about 1 or 2 of the groups. In that case, you’ll want to cull the group claims to reduce the size of the authorization cookie that ASP.NET Core sends to the client browser. We’ll cover that topic and more in the next post.