azure-activedirectory-identitymodel-extensions-for-dotnet: Default 'sub' claim mapping does not resolve to ClaimsPrincipal.Identity.Name

To process a JWT, the API consumer is going to use most likely the JwtSecurityTokenHandler.ValidateToken method, to get a ClaimsPrincipal.

RFC7519 states for the registered ‘sub’ claim (emphasis mine):

The “sub” (subject) claim identifies the principal that is the subject of the JWT. The claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The “sub” value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL.

The ClaimsPrincipal object returned from the ValidateToken method allows to identify the subject via its default interface with the Identity.Name property only. There are no other default properties that expose a principal identity unless you’d query the claims.

Because the default mapping of the claims controlled with ClaimTypeMapping links sub to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier, it does not match with the claim http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name used to query the Name property.

Therefore, when a JWT has the sub claim only, there is no easy API to access its value.

The default mapping should be changed to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name, so that ClaimsPrincipal.Identity.Name resolves to the sub claim.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 32 (21 by maintainers)

Commits related to this issue

Most upvoted comments

I personally think it is evil that the JWT handler converts the standard claim types to the Microsoft favoured ones.

you can turn that globally off by setting the static DefaultInboundClaimTypeMap property on the JWT handler.

After that - yes do a FindFirst on sub.

For anyone hitting this with this issue - any ASP .NET 5 API you work on that is receiving JWTs, you need to ensure JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); gets called at the top of your ConfigureServices method in Startup otherwise standardized claim names will get transformed to legacy XML names.

This is especially pertinent in a microservices architecture - this will need to be added to ALL API Gateways and ALL microservices to ensure that any tokens that are passed downstream don’t get auto-magically changed into the old legacy formats.

I just lost 2 hours to this - in 2020. Again, repeating what others have said above - it is not clearly documented and the issue has been open for a long time now. You want to reach new developers and not come across as ‘old’ Microsoft yet you favor supporting legacy code over just adding in common sense behaviour to a brand new framework. Its perfectly reasonable to clearly document a breaking change like that and expect people who are porting legacy projects to ensure this works well.

Making legacy projects work on a new framework is not how you ensure said new framework takes off - ensuring your APIs behave in ways that make sense and delight the developer experience is how you make .NET 5 fly. Please please please fix stuff like this. Im trying to pursuade my devs to move over to the new open source framework but this is like the third thing ive ran into this week that frustrates the developer experience.

@zvrba @khellang @bgever we created a new handler JsonWebTokenHandler that does not perform mapping. We need to give asp.net users a way to specify that handler. Additionally, it is about 25% faster.

We are working on it.

This continues to bite people in 2018… https://github.com/aspnet/Mvc/issues/7760

Yep, came here cause of same issue. Sub claim created via new Claim(JwtRegisteredClaimNames.Sub, userId.ToString())

after parsing/extracting from principal becomes http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier

very confusing

Two workarounds for the current versions (tested with 5.0 RC2):

  1. Clear the mapping table, and set the NameClaimType on the TokenValidationParameters:
var params = new TokenValidationParameters { NameClaimType = "sub" };
var jwtHandler = new JwtSecurityTokenHandler();
jwtHandler.InboundClaimTypeMap.Clear();
SecurityToken token;
ClaimsPrincipal principal = jwtHandler.ValidateToken(serializedJwt, params, out token);
string sub = principal.Identity.Name;
  1. Override the default mapping table to use the Name claim type rather than NameIdentifier:
var params = new TokenValidationParameters();
var jwtHandler = new JwtSecurityTokenHandler();
jwtHandler.InboundClaimTypeMap[JwtRegisteredClaimNames.Sub] = ClaimTypes.Name;
SecurityToken token;
ClaimsPrincipal principal = jwtHandler.ValidateToken(serializedJwt, params, out token);
string sub = principal.Identity.Name;

The first one is more “true” to the original JWT claims, as none of the claims will expand to “long” versions. Second one is the quickest operation, since only one claim type mapping needs to be overridden, and the ClaimsPrincipal will use default claim type to look up the name.

Obviously, I still prefer to see the default behavior changed. 😄

@mafurman Could you elaborate “When we go async with the JsonWebTokenHandler” a bit more please?

sub is not the name of a user. It’s the unique user id.

@ggonzalez94 as soon as your Appdomain loads write: JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

What’s the framework’s recommended way of retrieving the sub (i.e. unique user id) from the ClaimsPrincipal object?

string sub = principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;

This would not work, because by default the claims are converted to their long values. That’s a framework feature, but not something I would expect when I’m just parsing a JWT.

So the following would work, but this behavior isn’t clear to someone calling the API:

string sub = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;

The workarounds I’ve posted before work for me, or I could even query the claim as here, but I wonder how other consumers of the API could discover this implementation specific behavior?

Just ran into this, what worked for me was:

            JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
            handler.MapInboundClaims = false;

There will be always some sort of mapping going on - but the JWT handler is the wrong place to do that IMO. That’s application logic.

So yes - I still opt for turning it off (now’s the last chance)