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.
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;
2) 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. :smile:
sub is not the name of a user. It's the unique user id.
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?
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.
The JwtSecurityTokenHandler should allow to be constructed without a mapping table (instead of requiring to override it), what could be done with a flag in the constructor.
If you construct the handler yourself you can clear the instance InboundClaimTypeMap property.
Yea - not a fan at all - but thats the way it is - legacy.
Yes, that's what I do in my first workaround above. :smile:
I guess the way to go, is to add a constructor overload to the JwtSecurityTokenHandler, where the value of the InboundClaimTypeMap can be specified with an argument. When it's null, then the field will be set to an empty map. The constructor can then _specify why the map may be needed_ (i.e. identity processing with "full" claim names), so that the consumer of the API knows what is happening under the hood.
So it would look something like this:
var params = new TokenValidationParameters();
var jwtHandler = new JwtSecurityTokenHandler(null); //TODO
SecurityToken token;
ClaimsPrincipal principal = jwtHandler.ValidateToken(serializedJwt, params, out token);
string sub = principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
More than happy to make a PR if that's appreciated.
@bgever @leastprivilege it seems you are both suggesting that mapping should be OFF by default.
Correct?
The legacy was the result of normalizing between SAML and JWT tokens so an authorization provider would not need to see the difference.
IIRC we asked for the mapping to be off by default a long time ago. People using the JWT library are all targeting OIDC where those claims types are specified and it's burdensome to keep seeing XML namespaces for the legacy WS-* claim types.
So in short we do see the difference today in Katana because we have to explicitly disable the mapping in every API and every client app. It'd be nice to not have to explain why claims in your app don't match claims that we see in the JWT.
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)
Agreed, the mapping should be off, it is unexpected application logic as @leastprivilege said.
I guess the reason why it is happening in the JWT handler, is that the ClaimsPrincipal which is returned from ValidateToken, is created with a mapping. This logic is defined in TokenValidationParameters.CreateClaimsIdentity. It is possible to override the behavior, but the mapping table suits the default claim types for name and role. Relating to why I opened this issue in the first place.
@brentschmaltz I think there's now sufficient evidence that this is confusing behavior and should be fixed. Note my earlier suggestion of adding the mapping table as an argument to the constructor, also accepting a null value to initiate without mappings.
@bgever This is legacy code, and making changes to the default mapping would likely break a lot of existing apps that depend on the JwtSecurityTokenHandler.
When we go async with the JsonWebTokenHandler, there will not be any mapping happening by default.
@mafurman Could you elaborate "When we go async with the JsonWebTokenHandler" a bit more please?
This continues to bite people in 2018... https://github.com/aspnet/Mvc/issues/7760
Seriously, closed? I just lost two days trying to get the following to work
opts.AddPolicy("ProjectAccessScope", policy => {
policy.RequireClaim("scp", Components.AppScopes.ProjectMember);
});
It was constantly failing, I had to turn off policy-based authorization and inspect the token manually to discover that it has been renamed. And where is renaming of token types mentioned in the aspnet.core documentation? NOWHERE!
EDIT: I understand the reluctance to make a breaking change, but it should at least be clearly documented in a warning box in the documentation in one of the sections on authorization!!!!!!!!!!!!!!!!!!!!!!
@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.
Im having an issue even after clearing the default mapping. Here麓s my code:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(cfg =>
{
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = configuration["Security:Tokens:Issuer"],
ValidateAudience = true,
ValidAudience = configuration["Security:Tokens:Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Security:Tokens:Key"])),
};
});
And when retrieving the Claims Principal via the User from any controller I still get the mapping to legacy claims:

Any suggestions?
@ggonzalez94 as soon as your Appdomain loads write:
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
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.
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.
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
Most helpful comment
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
DefaultInboundClaimTypeMapproperty on the JWT handler.After that - yes do a
FindFirstonsub.