.netauthenticationjwtopenid-connect

.NET OIDC token mapping different between id_token and userinfo endpoint


I'm creating a website that needs to be able to use different OIDC providers. This website will map a claim, myclaim, to user roles.

The two providers I test with returns this claim in either the id_token or in the userinfo endpoint.

When the claim is in the id_token I can do the following to map the claim to user roles:

services.AddAuthentication()
    .AddOpenIdConnect(options =>
        options.TokenValidationParameters.RoleClaimType = "myclaim");

This does not work when the claim is in the userinfo endpoint. For that to work I have to remove the above and do this instead:

services.AddAuthentication()
    .AddOpenIdConnect(options =>
       options.ClaimActions.MapJsonKey(
            "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
            "myclaim"));

So, the first code block does not work when myclaim is in userinfo, and the second code block does not work when myclaim is in the id_token.

options.GetClaimsFromUserInfoEndpoint = true; is set for both examples.

Is there a way for .NET to support both methods at once?


Solution

  • I'm still not sure why the .MapJsonKey() only works for claims for the userinfo endpoint, and the .TokenValidationParameters.RoleClaimType = "myclaim" only works for claims from the id_token, and I haven't found any rationale for why this is so.

    I did find the IClaimsTransformation interface (Microsoft documentation), which receives claims from both the id_token and the userinfo endpoint, which makes it possible to write your own claims transformations (the name sort of gives that away).

    So basically, create a class of your own that implements IClaimsTransformation, do your claims transformations in it, and don't forget to add it to dependency injection, and Bob's your uncle.

    Example:

    public class MyClaimsTransformation : IClaimsTransformation
    {
        public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
        {
            var sourceClaims = principal.FindAll(claim => claim.Type == "mySourceClaim");
    
            ClaimsIdentity claimsIdentity = new();
    
            foreach (Claim sourceClaim in sourceClaims)
            {
                if (!principal.HasClaim(claim => claim.Type == "myTargetClaim" &&
                                                 claim.Value == sourceClaim.Value))
                {
                    claimsIdentity.AddClaim(new Claim("myTargetClaim", currentRoleClaim.Value));
                }
            }
    
            principal.AddIdentity(claimsIdentity);
    
            return Task.FromResult(principal);
        }
    }
    

    Then you must remember to add it to dependency injection:

    builder.Services.AddTransient<IClaimsTransformation, MyClaimsTransformation>();
    

    If mySourceClaim comes from the userinfo endpoint you also need to make sure it is mapped by specifying it in the OIDC configuration:

    builder.Services
    .AddAuthentication()
    .AddOpenIdConnect("scheme", options =>
        options.ClaimActions.MapJsonKey("mySourceClaim", "mySourceClaim");
    );
    

    This does add some more complexity to your code, but there are no conditional accesses needed to needlessly clutter things up, so it evens out for the better.