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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
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
- https://techcommunity.microsoft.com/t5/Azure-Active-Directory-Identity/Azure-Active-Directory-now-with-Group-Claims-and-Application/ba-p/243862
- https://stackoverflow.com/questions/57147812/windows-authentication-in-net-core-web-api-angular-application
- https://github.com/Azure-Samples/active-directory-dotnet-webapp-groupclaims#step-2–register-the-sample-with-your-azure-active-directory-tenant
- https://github.com/Azure-Samples/active-directory-dotnet-webapp-groupclaims#step-3-configure-your-application-to-receive-group-claims
Discover more from Matt Ruma
Subscribe to get the latest posts sent to your email.
Hi Matt,
Today I came across the same problem; a user account that’s a member of more than 200 groups, so Azure returns a JWT token with an overage claim.
Ready to implement the solution you proposed in your blog, I realized after testing that neither the Active Directory Graph API nor the Microsoft Graph API return the group names. I only get a list of GUIDs which aren’t directly usable as a claim values.
I tried two different tenants but in both cases I get only group GUIDs from the getMemberObjects endpoint.
So I’m wondering, how did you get your implementation to work?