A JWT validation error is the verifier doing its job, and the most dangerous fix is the one that makes the error go away by validating less. When an API starts returning 401s, engineers are tempted to widen the audience list, add clock tolerance, accept another algorithm or skip signature checks "temporarily". This guide maps the real error messages from three widely used libraries (Microsoft.IdentityModel for .NET, jsonwebtoken for Node.js and PyJWT for Python) to the validation stage that failed, lists the likely causes from most to least common, and gives the fix that keeps the check intact. It is for API, platform and identity engineers running OAuth 2.0 or OpenID Connect resource servers. Library messages were checked against each project's source or documentation on 11 November 2026. JWT design and hardening are covered in the JWT Security Guide, which this page does not repeat.
Key takeaways
- Locate the failing stage first. Every JWT error belongs to one stage: parsing, algorithm, key lookup, signature, issuer, audience, time window or required claims. The message usually names it.
- Signature failures after a quiet period are usually key rotation. A verifier with a pinned key or a stale JWKS cache rejects tokens signed with the issuer's new key; refresh on unknown
kid, rate limited. - Issuer and audience mismatches are configuration, not attacks, most of the time. Microsoft v1.0 and v2.0 tokens carry different issuers, and an API must accept only tokens whose
audis its own identifier. - Library defaults differ on clock skew. Microsoft.IdentityModel allows 300 seconds by default; PyJWT's
leewaydefaults to 0. The same token can pass one service and fail the next. - Never fix by validating less. Do not decode without verifying, accept every algorithm in the header, or add another resource's audience. Storm-0558 showed what happens when a verifier trusts a key it should not.
Find the failing stage: message to stage map
Verifiers check a token in a broadly fixed order. Match the message to the stage, then go to that section.
| Stage | Microsoft.IdentityModel (.NET) | jsonwebtoken (Node.js) | PyJWT (Python) |
|---|---|---|---|
| Parsing | IDX10209 token larger than MaximumTokenSizeInBytes | jwt malformed; invalid token | DecodeError |
| Signature present | IDX10504 token does not have a signature | jwt signature is required | DecodeError |
| Algorithm | Surfaces as a signature failure | invalid algorithm when the token's alg is not in the allowed list | InvalidAlgorithmError |
| Key lookup | IDX10500 no security keys provided; IDX10503 / IDX10517 (kid missing) | Your getKey callback error | PyJWKClientError; PyJWKClientConnectionError |
| Signature | IDX10511 signature validation failed, keys tried | invalid signature | InvalidSignatureError |
| Issuer | IDX10205 issuer validation failed; IDX10204 no valid issuer configured | jwt issuer invalid. expected: ... | InvalidIssuerError |
| Audience | IDX10214 / IDX10215 audience validation failed; IDX10208 none configured | jwt audience invalid. expected: ... | InvalidAudienceError |
| Expiry | IDX10223 the token is expired | TokenExpiredError: jwt expired | ExpiredSignatureError |
| Not before | IDX10222 the token is not yet valid | NotBeforeError: jwt not active | ImmatureSignatureError |
| Required claims | IDX10225 missing an Expiration Time | Subject or jwtid mismatch messages | MissingRequiredClaimError |
Microsoft.IdentityModel hides token contents in messages by default (PII logging off). Turning PII logging on in a test environment shows the actual issuer and audience values compared; do not enable it in production, because messages then include token data.
Inspect the token safely
- Capture from a non-production request or from the failing client's debug output. Treat it as a live credential until
exppasses; never paste production tokens into online decoders. See the JWT and Session Tokens in Code and Logs Guide. - Check the shape: a JWS has three base64url segments separated by dots. Two dots and an empty third segment means unsigned; five segments means it is encrypted (JWE); no dots means it is an opaque token that must be introspected, not validated as a JWT.
- Read the header:
alg,kidandtyp. RFC 9068 access tokens carrytypat+jwt. - Read the payload:
iss,aud,exp,nbf,iat,tidor tenant claims,scporroles. - Compare with the verifier's configuration character for character, then with the issuer's discovery document (
/.well-known/openid-configuration) and itsjwks_uri.
Signature and key errors
Likely causes, most to least common
- Issuer rotated its signing key and the verifier has not caught up. The token's
kidis not in the cached key set. Microsoft states that Entra ID rotates keys periodically, applications must handle changes automatically, and checking for updated keys every 24 hours is reasonable. - Pinned key or certificate in configuration. A public key copied into app settings works until the first rotation, then fails everywhere at once.
- Wrong metadata endpoint. Tenant-specific, common and v1.0 or v2.0 metadata paths publish different documents; a verifier pointed at the wrong one fails on issuer or key.
- Token is not meant for this API. Microsoft says Microsoft Graph tokens cannot be validated with the standard rules because of their proprietary format, and accepting tokens meant for another resource is a confused deputy problem. A client that sends its Graph token to your API produces a signature error.
- Algorithm mismatch. A verifier expecting RS256 receives HS256 (a test token signed with a shared secret) or ES256 (a new issuer). Node's jsonwebtoken reports this as
invalid algorithmwhen the header alg is not in its allowed list. - Token altered in transit: a proxy, header size limit or logging middleware truncates the
Authorizationheader.
Diagnose
- Is the
kidin the live JWKS? Fetch the issuer'sjwks_uriand search for it. If yes, the verifier's cache is stale. If no, the token came from a different issuer or tenant, or is forged. - What keys did the verifier try?
IDX10511lists keys tried, the number of keys in TokenValidationParameters and in configuration, and where the matched key was found. Zero keys in configuration means metadata was never retrieved; check outbound network access to the issuer. - PyJWKClient:
PyJWKClientConnectionErrormeans the JWKS request failed;PyJWKClientErrormeans no matching signing key even after a refresh.
Fix
- Load keys from metadata, never from config. Microsoft.IdentityModel's ConfigurationManager refreshes metadata automatically every 12 hours by default and allows a forced refresh no more often than every 5 minutes by default. PyJWKClient caches the JWK set for 300 seconds by default and applies a 30-second cooldown between forced refreshes after an unknown
kid. - Refresh on unknown
kid, rate limited: one refresh, then reject. Unlimited refetching lets an attacker turn randomkidvalues into a denial of service against the issuer and you. - Pin the algorithm list in code: PyJWT's documentation tells you to hard-code
algorithmsrather than derive it from the token. Use the algorithm the issuer publishes. - Return the token to sender: if the client sent a token for another resource, the fix is in the client's scope request, not your verifier.
Key rollover from the issuer's side is covered in the JSON Web Key Sets (JWKS) and Signing Key Rotation Guide.
Issuer errors
- v1.0 versus v2.0 Microsoft tokens: the v2.0 issuer is
https://login.microsoftonline.com/{tenantid}/v2.0; v1.0 tokens carry thehttps://sts.windows.net/{tenant}/form. The API's manifest propertyrequestedAccessTokenVersiondecides which the API receives (nullor1gives v1.0,2gives v2.0). A mismatch between that setting and the configured issuer is the most commonIDX10205on Entra-protected APIs. - Trailing slash and scheme: issuer comparison is an exact string match.
https://idp.example.comandhttps://idp.example.com/are different issuers. - Multi-tenant APIs: the metadata issuer contains a
{tenantid}placeholder. Microsoft's guidance is to substitute the token'stid, confirmtidis a GUID, then compare withiss, and separately decide which tenants you accept. Accepting any issuer that matches the pattern accepts every tenant in the cloud. - Environment drift: staging tokens presented to production, or a custom domain added to the IdP that changes
iss. - Nothing configured:
IDX10204means neither ValidIssuer nor ValidIssuers is set. Configure it; do not switch issuer validation off.
Audience errors
- Client ID versus App ID URI: Entra tokens may carry the API's application (client) ID or its App ID URI in
aud, depending on token version and how the client requested scope. Configure the values your API actually receives, both if needed, and nothing else. Microsoft: APIs must only accept tokens containing one of their App ID URIs as theaudclaim. - Client requested the wrong resource: the token's
audis Graph or another API. Fix the client'sscopeparameter (for exampleapi://your-api/.defaultin client credentials). - Array audiences: RFC 7519 allows
audas a string or array. Most libraries pass if any value matches; check your library does not require the first element. - Never add another resource's identifier to ValidAudiences to make an error disappear. That is the confused deputy Microsoft warns against.
Time window errors: expired, not yet valid, skew
| Library | Clock skew default | Setting |
|---|---|---|
| Microsoft.IdentityModel | 300 seconds | TokenValidationParameters.ClockSkew |
| PyJWT | 0 seconds | leeway on decode() |
| jsonwebtoken (Node.js) | Not set unless configured | clockTolerance (seconds) on verify() |
- Client caches tokens past expiry: the most common cause of
jwt expired. The client must refresh beforeexp, not after a 401. Entra assigns access tokens a random lifetime of 60 to 90 minutes (75 on average) by default, so a client hard-coding "one hour" will sometimes send expired tokens. - Clock drift between hosts:
IDX10222,jwt not activeorImmatureSignatureErroron freshly issued tokens means the verifier's clock is behind the issuer's. Fix NTP on the host or container node; a leeway of 30 to 60 seconds is a reasonable buffer, minutes is not. - Long-running jobs: a batch job acquires one token and uses it for hours. Acquire per call or refresh on a timer.
- Missing
exp:IDX10225. A token without expiry should be rejected; fix the issuer, not the verifier.
Malformed tokens and missing claims
- Header formatting:
Bearerprefix sent twice, missing, or the token wrapped in quotes from a JSON config. Strip exactly oneBearerprefix. - Opaque or encrypted token: some IdPs issue opaque access tokens unless an audience or API is specified in the request; others encrypt. Validate the format you receive: introspect opaque tokens (RFC 7662), decrypt JWEs with the right key.
- Size: Microsoft.IdentityModel rejects tokens above 250 KB by default (
IDX10209); group overage claims and large role lists push tokens towards proxy header limits long before that. - Required claims: PyJWT's
options.requirechecks presence only, not validity, so pair it with the issuer and audience arguments.
Returning errors correctly to clients
Clients can only self-correct if the API responds per RFC 6750 section 3.1.
| Error code | When | HTTP status (RFC 6750) | Client action |
|---|---|---|---|
| (none) | No credentials sent | 401 with WWW-Authenticate: Bearer | Acquire a token |
invalid_request | Malformed request, token sent by more than one method | 400 | Fix the request |
invalid_token | Expired, revoked, malformed or otherwise invalid | 401 | May get a new token and retry |
insufficient_scope | Valid token lacking required privileges | 403, optionally with scope | Request the scope, do not retry blindly |
Keep error_description generic in production. Log the detailed reason (stage, kid, iss, aud, client ID) server-side.
Tempting fixes and the safe alternative
| Tempting fix | Why it is dangerous | Safe fix |
|---|---|---|
Use decode() instead of verify() | jsonwebtoken warns decode does not verify the signature; any forged token passes | Fix key retrieval |
| Accept the algorithm named in the token header | Opens algorithm confusion attacks (RFC 8725) | Allowlist the issuer's algorithm in code |
| Turn off issuer or audience validation | Accepts tokens from any tenant or meant for any API | Configure the exact values |
| Set clock skew to hours | Extends the life of stolen tokens | Fix NTP; keep leeway under a minute or two |
| Hard-code the current public key | Breaks at next rotation and blocks emergency rollover | Load from jwks_uri with caching |
Detection and measurement
- Log per failure: stage, library error code,
kid,iss,aud, client ID and source. Never log the token. - Alert on: signature failures with a
kidabsent from every trusted JWKS (possible forgery), sudden spikes in one stage after a deployment or IdP change, and tokens from unexpected tenants. - Metrics: 401 rate by validation stage; share of signature failures caused by unknown
kid; time from issuer key rotation to zerokid-not-found errors; count of verifiers with pinned keys or disabled checks (target zero); median token remaining lifetime at use.
Common failure patterns
- Trusting a key outside its scope: in the Microsoft Storm-0558 key breach, tokens signed with a consumer signing key were accepted for enterprise mailboxes. Check issuer and key scope together.
- Fail-open on JWKS outage: code that skips signature checks when the key fetch fails. Fail closed and use the last known good key set.
- Global leeway raised during an incident and never lowered.
- Accepting ID tokens as access tokens: check
typand token purpose (OWASP ASVS V9.2.2). - Every service validating differently: centralise validation in a gateway or shared library so one fix applies everywhere.
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| OWASP ASVS 5.0 | V9.1.1, V9.1.2, V9.2.1, V9.2.2, V9.2.3 | Signature validation, algorithm allowlist, validity window, token type, audience |
| NIST SP 800-53 Rev 5 | SC-23 Session Authenticity; IA-9 Service Identification and Authentication; SC-12 | Accept only authentic tokens; authenticate services; key management for signing keys |
| NIST CSF 2.0 | PR.AA-03 | Users, services and hardware are authenticated |
| MITRE ATT&CK v20 | T1606 Forge Web Credentials; T1550.001 Application Access Token | Forged tokens and stolen tokens replayed against APIs |
Practitioner checklist
- Every failure logged with stage and library code, never the token.
- Keys loaded from the issuer's metadata, with refresh on unknown
kidrate limited. - Algorithm allowlist hard-coded per issuer.
- Issuer and audience set to exact values; multi-tenant APIs restrict accepted tenants.
- Clock skew under two minutes, and NTP monitored on verifier hosts.
- Clients refresh tokens before
exp. - Responses follow RFC 6750 error codes.
- No production code path calls a decode-only function on untrusted tokens.
Standards and references
- IETF: RFC 7519 JSON Web Token (JWT)
- IETF: RFC 8725 JSON Web Token Best Current Practices
- IETF: RFC 6750 OAuth 2.0 Bearer Token Usage
- IETF: RFC 9068 JWT Profile for OAuth 2.0 Access Tokens
- OWASP: Application Security Verification Standard 5.0
- Microsoft Learn: Access tokens in the Microsoft identity platform
- PyJWT: API Reference
- GitHub: node-jsonwebtoken README
Related NHI Mgmt Group resources: JWT Security Guide, JSON Web Key Sets (JWKS) and Signing Key Rotation Guide, OAuth 2.0 and OpenID Connect Guide, Machine-to-Machine OAuth (Client Credentials) Guide, Secret Rotation Outage Troubleshooting Guide.