A customer recently asked me how to set up authorization so they could authorize a service principal calling their API using AzureAD groups. Here is my GitHub repo that contains the sample code.

Normally, you would add the optional group claims
to the AAD app registration for the resource (API) so that all AAD access tokens will include this additional claim.
However, in this case, the client is a service prinicipal, not a signed in user. In this case, AAD will not emit the AAD groups that the service principal is a part of.
Luckily, we can add this additional information in ASP.NET before evaluating authorization policies.
IClaimsTransformation
The key to making this work is a custom IClaimsTransformation
implementation. We can add our implementation to the IoC container in ASP.NET and it will be called before the Authorization
policies are evaluated. This gives us a chance to add the additional AAD group claims. We can query the Graph API to get the list of AAD groups a service principal is a member of.
public class MicrosoftGraphClaimsTransformationService : IClaimsTransformation
{
...
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
ClaimsIdentity? identity = principal.Identity as ClaimsIdentity;
if (!principal.HasClaim(claim => claim.Type == Constants.GroupClaimType))
{
ClaimsIdentity groupClaimsIdentity = new ClaimsIdentity();
Claim? servicePrincipalObjectId = identity.Claims.FirstOrDefault(x => x.Type == Constants.ObjectIdentifierClaimType);
if (servicePrincipalObjectId != null)
{
_logger.LogDebug($"Retrieving group memebrship for {servicePrincipalObjectId.Value}");
var groups = await _graphServiceClient.ServicePrincipals[servicePrincipalObjectId.Value].MemberOf.Request().GetAsync();
foreach (var group in groups)
{
_logger.LogDebug("Adding groups claim to principal");
groupClaimsIdentity.AddClaim(new Claim(Constants.GroupClaimType, group.Id));
};
principal.AddIdentity(groupClaimsIdentity);
}
}
return principal;
}
}
Finally, we need to register this new IClaimsTransformation
interface implementation with the IoC container.
builder.Services.AddTransient<IClaimsTransformation, MicrosoftGraphClaimsTransformationService>();
Policy-based authorization
ASP.NET gives us several authorization schemes we can implement. We are going to use Policy-based authorization to check the claims of the tokens being presented by the caller.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(weatherApi.Constants.MemberOfDataReadAADGroupPolicyName,
policy =>
{
policy.RequireClaim(weatherApi.Constants.GroupClaimType, azureAdGroupsOptions.DataReadAADGroupObjectId);
});
...
In this example, we are going to check that the groups
claim matches the specific ObjectId of an AAD group.
Controller authorization
Now that we have enriched the identities of the caller, we can make authorization decisions on either the controller or the individual methods.
[HttpGet]
[Authorize(Policy = Constants.MemberOfDataReadAADGroupPolicyName)]
public IEnumerable<WeatherForecast> GetWeatherForecasts()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)],
});
}
No app roles
If you are not going to implement app roles, you will need to specify the following flag so ASP.NET doesn’t try to check for them.
{
"AzureAd": {
...
"AllowWebApiToBeAuthorizedByACL": true
},