Adventures with Azure AD: Group Overage Claim

I have an ASP.NET Core 2.2 Web App.

My app authenticates users with OpenIdConnect against Azure Active Directory.

I want to use the Security Groups of the authenticated user for role authorization inside my app.

To do this, I needed to setup my App Registration in Azure AD to return the Security Groups as claims.

Seemed easy enough! Then I modified my Startup.cs to convert the groups to roles.

services.AddAuthentication(auth =>
{
    auth.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    Configuration.Bind("OpenIdConnect", options);
    options.Events = new OpenIdConnectEvents
    {
        OnAuthorizationCodeReceived = ctx =>
            {
                if (ctx.Principal.Identity is ClaimsIdentity identity)
                {
                    var claims =
                        ctx.Principal.Claims.Where(x => x.Type == "groups").ToList();

                    foreach (var claim in claims)
                    {
                        identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
                    }
                }

                return Task.CompletedTask;
            }
    };
});

This worked great!

But, there was one issue that manifested itself.

Would you believe it? There is a limit to the number of groups that can be returned and if that limit is exceeded a different claim is returned, called a Group Overage Claim.

The Group Overage Claim is composed of two claims, _claim_names and _claim_sources.

When presented with the Group Overage Claim I was going to have to call the Microsoft Graph API to get the groups for the user, an endpoint to call to get the groups was provided.

But, I ignored the provide endpoint, for a couple of reasons:

  • It points to the older Active Directory Graph API (https://graph.windows.net) instead of the newer Microsoft Graph API (https://graph.microsoft.com)
  • It is incomplete, to call this endpoint you would need to include the api-version

In order to call the Microsoft Graph API, I was going to need an access_token.

To do this, I gave my app the Directory.Read.All permission to the Microsoft Graph API, and I granted admin consent for for all users – click the button under Grant Consent.

I made the following modifications to my code to look for the groups claim or the _claim_names claim.

When I have the _claims_name claim, I get an access_token and then call the Microsoft Graph API, sending the access_token.

services.AddAuthentication(auth =>
{
    auth.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    Configuration.Bind("OpenIdConnect", options);
    options.Events = new OpenIdConnectEvents
    {
        OnAuthorizationCodeReceived = async ctx =>
            {
                if (ctx.Principal.Identity is ClaimsIdentity identity)
                {
                    if (ctx.Principal.Claims.Any(x => x.Type == "groups"))
                    {
                        var claims =
                            ctx.Principal.Claims.Where(x => x.Type == "groups").ToList();

                        foreach (var claim in claims)
                        {
                            identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
                        }
                    }
                    else if (ctx.Principal.Claims.Any(x => x.Type == "_claim_names"))
                    {
                        var authenticationContext =
                            new AuthenticationContext(ctx.Options.Authority);
                        var clientCredentials =
                            new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);

                        var result =
                            await authenticationContext.AcquireTokenAsync("https://graph.microsoft.com", clientCredentials);

                        using (var httpClient = new HttpClient())
                        {
                            httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {result.AccessToken}");

                            var tenantId =
                                ctx.Principal.Claims.Single(x => x.Type == "http://schemas.microsoft.com/identity/claims/tenantid").Value;
                            var userId =
                                ctx.Principal.Claims.Single(x => x.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

                            var httpResponse =
                                await httpClient.PostAsJsonAsync(
                                    $"https://graph.microsoft.com/v1.0/{tenantId}/users/{userId}/getMemberObjects",
                                    new { SecurityEnabledOnly = false });

                            httpResponse.EnsureSuccessStatusCode();

                            var jsonResult =
                                await httpResponse.Content.ReadAsAsync<dynamic>();

                            foreach (var value in jsonResult.value)
                            {
                                identity.AddClaim(new Claim(ClaimTypes.Role, value.ToString()));
                            }
                        }
                    }
                }
            }
    };
});

Now my app can handle both use cases, users with groups less than 200 and users with groups greater than 200!

Were time limitless, I would spend some of it refactoring the loading of the claims, maybe only pull out the roles my app truly cares about, just make things cleaner.

The complete code can be found at https://github.com/mattruma/SampleAzureADAuthentication.

For your testing, you can use the scripts at https://github.com/Azure-Samples/active-directory-dotnet-webapp-groupclaims/tree/master/AppCreationScripts to create and delete a large number of Security Groups.

Related links

Leave a Reply

Your email address will not be published.