What JwtBearer does with a token
AddJwtBearer reads the Authorization: Bearer header, decodes the token, checks the signature, then the lifetime (exp and nbf, with 5 minutes of ClockSkew), then the audience, then the issuer. The first check that fails decides the 401 and the IDX error in your logs. If everything passes, it turns the payload into claims on HttpContext.User. This page runs the same steps, in the same order, and was checked against a real ASP.NET Core app on .NET 8 and .NET 10 with thousands of tokens.
The claim mapping surprise
With the default MapInboundClaims = true, JwtBearer renames 73 short claim names to long ClaimTypes URIs: sub becomes ClaimTypes.NameIdentifier, role and roles become ClaimTypes.Role, email becomes ClaimTypes.Email. So User.FindFirst("sub") returns null. Worse, name is not mapped, while User.Identity.Name looks for ClaimTypes.Name, which only comes from unique_name. A typical token therefore gives a null name.
The clean fix is to keep the token's names and say which ones mean "name" and "role":
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
The 401s that cost an afternoon
- IDX10517 "The token's kid is missing". Nothing to do with
kid: no key verified the signature. Usually the secret differs, is shorter than the algorithm needs (32 bytes for HS256, 48 for HS384, 64 for HS512), or one side Base64-decodes it and the other uses the text. - IDX10503 with a kid. The token names a key you do not have. With a JWKS, the issuer rotated keys or the token comes from another tenant or environment. On this path .NET reports a lifetime, issuer or audience problem first if there is one.
- IDX10214 audience. The match is exact and case-sensitive; only a trailing slash is ignored. On .NET 10 the
WWW-Authenticateheader even saysThe audience '(null)' is invalid, so check the log, not the header. - IDX10205 issuer. Exact match, and here the trailing slash does count. Azure AD v1 and v2 tokens have different issuers (
sts.windows.netvslogin.microsoftonline.com/.../v2.0). - An expired token that still works.
ClockSkewadds 5 minutes afterexp. - IsInRole is false. Roles in
permissions,groupsor a nested object are not roles until you setRoleClaimTypeor map them. Values are case-sensitive, and on .NET 10 so is the claim type. - A NullReferenceException from JwtBearer. When the token's
kidmatches an HMAC key that is too short and other keys are configured, Microsoft.IdentityModel throws it instead of a key-size error.
FAQ
Why is User.Identity.Name null with a JWT?
Identity.Name reads the ClaimTypes.Name claim, and JwtBearer only creates that from a unique_name claim. The standard name claim is not mapped, so a token with name but no unique_name leaves it null. Set NameClaimType to "name" (or whatever claim your issuer uses).
Why does sub become a long nameidentifier URI?
MapInboundClaims is true by default, so JwtBearer renames 73 short claim names to long ClaimTypes URIs: sub becomes ClaimTypes.NameIdentifier, role and roles become ClaimTypes.Role, email becomes ClaimTypes.Email. Set MapInboundClaims to false to keep the token's original claim names.
What does IDX10517 "The token's kid is missing" mean?
No key verified the signature. The missing kid is only why .NET could not pick a key by id; the real problem is almost always a wrong key, a secret shorter than the algorithm needs (32 bytes for HS256), a key of the wrong type, or a Base64 secret decoded on one side and read as plain text on the other.
Why does an expired JWT still work for a few minutes?
ClockSkew defaults to 5 minutes, so JwtBearer keeps accepting a token until 5 minutes after its exp. Lower it, or set it to zero, if tokens must stop working exactly at exp.
Why is IsInRole false when the token has roles?
IsInRole checks claims of RoleClaimType, which defaults to ClaimTypes.Role and comes from role and roles claims. Roles kept in other claims, such as permissions, groups or a nested realm_access object, are not seen. Values are case-sensitive, and on .NET 10 so is the claim type. Set RoleClaimType or map the claims yourself.
Learn more
- JWT generator: create signed test tokens and the AddJwtBearer setup
- JWT Authentication in ASP.NET Core Web API
- How to Decode JWT Tokens in .NET
- Using Refresh Tokens in ASP.NET Core Authentication
- How to Use HttpOnly Cookie in .NET Core for Authentication and Refresh Token Actions
- Secure Microservices Using JWT With Ocelot in .NET Core