How to use AzureAD group membership to authorize a service principal in ASP.NET

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.

C#
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.

C#
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.

C#
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.

C#
[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.

JSON
{
  "AzureAd": {
   ...
    "AllowWebApiToBeAuthorizedByACL": true
  },

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *