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?
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.