In a previous post we looked at using Azure AD groups for authorization. I mentioned in that post how you need to be careful when pulling group membership claims from Azure AD. In this post we’ll look at the default processing of claims in ASP.NET Core and see how to avoid the overheard of carrying around too many group claims.
The first issue I want to address in this post is the change in claims processing with ASP.NET Core 2.
Dominick Baier has a blog post about missing claims in ASP.NET Core. This is a good post to read if you are using the OIDC services and middleware. The post covers a couple different issues, but I want to call out the “missing claims” issue specifically.
The OIDC options for ASP.NET Core include a property named ClaimActions. Each object in this property’s collection can manipulate claims from the OIDC provider. By manipulate, I mean that all the claim actions installed by default will remove specific claims. For example, there is an action to delete the ipaddr claim, if present. Dom’s post includes the full list.
I think ASP.NET Core is removing claims to reduce cookie bloat. In my experiments, the dozen or so claims dropped by the default settings will reduce the size of the authentication cookies by 1,500 bytes, or just over 30%. Many of the claims, like IP address, don’t have any ongoing value to most applications, so there is no need to store the value in a cookie and pass the value around in every request.
If you want the deleted claims to stick around, there is a hard way and a straightforward way to achieve the goal.
I’ve seen at least two software projects with the same 20 to 25 lines of code inside. The code originates from a Stack Overflow answer to solve the missing claims issue and explicitly parses all the claims from the OIDC provider.
If you want all the claims, you don’t need 25 lines of code. You just need a single line of code.
services.AddAuthentication() .AddOpenIdConnect(options => { // this one: options.ClaimActions.Clear(); });
However, make sure you really want all the claims saved in the auth cookie. In the case of AD group membership, the application might only need to know about 1 or 2 groups while the user might be a member of 10 groups. Let’s look at approaches to removing the unused group claims.
My first thought was to use the collection of ClaimActions on the OIDC options to remove group claims. The collection holds ClaimAction objects, where ClaimAction is an abstract base class in the ASP.NET OAuth libraries. None of the built-in concrete types do exactly what I’m looking for, so here is a new ClaimAction derived class to remove unused groups.
public class FilterGroupClaims : ClaimAction { private string[] _ids; public FilterGroupClaims(params string[] groupIdsToKeep) : base("groups", null) { _ids = groupIdsToKeep; } public override void Run(JObject userData, ClaimsIdentity identity, string issuer) { var unused = identity.FindAll(GroupsToRemove).ToList(); unused.ForEach(c => identity.TryRemoveClaim(c)); } private bool GroupsToRemove(Claim claim) { return claim.Type == "groups" && !_ids.Contains(claim.Value); } }
Now we Just need to add a new instance of this class to the ClaimActions, and pass in a list of groups we want to use.
options.ClaimActions.Add(new FilterGroupClaims( "c5038c6f-c5ac-44d5-93f5-04ec697d62dc", "7553192e-1223-0109-0310-e87fd3402cb7" ));
ClaimAction feels like an odd abstraction, however. It makes no sense for the base class constructor to need both a claim type and claim value type when these parameters go unused in the derived class logic. A ClaimAction is also specific to the OIDC handler in Core. Let’s try this again with a more generic claims transformation in .NET Core.
Services implementing IClaimsTransformation in ASP.NET Core are useful in a number of different scenarios. You can add new claims to a principal, map existing claims, or delete claims. For removing group claims, we first need an implementation of IClaimsTransformation.
public class FilterGroupClaimsTransformation : IClaimsTransformation { private string[] _groupObjectIds; public FilterGroupClaimsTransformation(params string[] groupObjectIds) { // note: since the container resolves this service, we could // inject a data access class to fetch IDs from a database, // or IConfiguration, IOptions, etc. _groupObjectIds = groupObjectIds; } public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { var identity = principal.Identity as ClaimsIdentity; if (identity != null) { var unused = identity.FindAll(GroupsToRemove).ToList(); unused.ForEach(c => identity.TryRemoveClaim(c)); } return Task.FromResult(principal); } private bool GroupsToRemove(Claim claim) { return claim.Type == "groups" && !_groupObjectIds.Contains(claim.Value); } }
Register the transformer during ConfigureServices in Startup, and the unnecessary group claims disappear.
Group claims are not difficult to use with Azure Active Directory, but you do need to take care in directories where users are members of many groups. Instead of fetching the group claims from Azure AD during authentication like we've done in the previous post, one could change the claims transformer to fetch a user’s groups using the Graph API and adding only claims for groups the application needs.