JWT Guides

How I Debug a JWT 401 Unauthorized Error

See the order I use to debug JWT 401 errors: Authorization header, expiration, signature, issuer, audience, token type, scopes and server logs.

By JWTDecoder.toolsLast verified 2026-09-16

Editorial voice: first-person phrasing describes the documented JWTDecoder.tools workflow; JWTDecoder.tools is the publisher and organizational author.

JWT 401 debugging flow from request header through claims and signature
Check request header, time, signature, issuer, audience, token type and authorization policy in that order.

When an API returns 401 Unauthorized for a request carrying a JWT, I debug the authentication chain from the outside in. I start with the actual HTTP request, then time, signature, issuer, audience, and token type. I only spend time on scopes/roles after the token is being authenticated successfully.

If I know the expected issuer and audience, I use the JWT Debugger while I inspect server-side evidence.

Step 1: prove the Bearer token reaches the API

A normal request looks like:

GET /orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

I verify the header at the component that actually performs authentication, not only in browser devtools.

Common transport failures:

  • Authorization missing entirely;
  • Bearer prefix missing or malformed;
  • token wrapped in quotes;
  • reverse proxy/gateway strips the header;
  • frontend sends an old cached token;
  • ID Token is sent instead of Access Token;
  • cookie-based auth and Bearer middleware are both present but the wrong one runs.

A correct JWT cannot authenticate if the verifier never receives it.

Step 2: read WWW-Authenticate and server logs

RFC 6750 defines Bearer challenge/error information. A response may include:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"

invalid_token is still broad. I correlate it with server logs and look for a more specific phase:

token expired
invalid signature
invalid issuer
invalid audience
kid not found
no applicable key found
token not active
malformed token

If the public response intentionally hides detail, the server-side authentication log becomes the best source.

Step 3: check exp, nbf, and clock before touching keys

I use the JWT Expiry Checker to convert time claims.

now < nbf      -> token not active yet
now >= exp     -> token expired

I also verify seconds vs milliseconds and the clock of the service that rejects the token. My laptop clock is not authoritative.

Step 4: verify the signature with the expected trust path

A token that decodes cleanly may still have an invalid signature. I check:

  • allowed alg;
  • expected issuer;
  • trusted secret/public key/JWKS;
  • kid selection;
  • key rotation and cache freshness;
  • original header.payload bytes.

For direct key material I use JWT Signature Verifier; for JWKS I use JWKS Validator.

Step 5: compare iss exactly with the configured issuer

I do not treat these as interchangeable without profile-specific rules:

https://login.example.com/
https://login.example.com
https://login.example.com/dev
https://login.example.com/prod

Issuer mismatch often also means I am using the wrong metadata/JWKS source. I fix the environment or tenant relationship rather than accepting multiple issuers casually.

Step 6: compare aud with the API that is actually processing the token

A token can be properly signed by the correct issuer and still be meant for another recipient.

{ "aud": ["api://billing", "api://reporting"] }

If the Orders API expects api://orders, rejection is correct. The JWT audience mismatch guide covers string/array semantics and token acquisition.

Step 7: make sure it is an Access Token, not an ID Token

A common SPA/OIDC failure is sending the ID Token in the API Authorization header. Clues include:

  • aud looks like the frontend client ID;
  • OIDC authentication claims dominate the payload;
  • expected API scopes are absent;
  • obtaining a real Access Token fixes the request without changing the API.

I fix the client's token selection. I do not teach the API to accept an ID Token it was never designed to consume.

Step 8: distinguish authentication failure from authorization failure

My working model is:

StatusWhat I investigate first
401token absent, malformed, expired, untrusted, wrong issuer/audience
403authenticated identity lacks required permission/policy

Individual frameworks can map edge cases differently, but this separation prevents me from debugging roles when the signature has not even verified.

Reverse proxy and API gateway failures are easy to miss

In multi-hop systems I compare what each hop sees:

browser -> CDN -> gateway -> service mesh -> API middleware

Possible mismatches:

  • CDN/gateway removes Authorization;
  • gateway validates against a different issuer than the API;
  • edge component caches stale JWKS;
  • one service expects api://orders, another expects orders-api;
  • authentication middleware runs after a route guard that already returned 401.

A packet/request trace or correlated request ID can be more useful here than decoding the JWT again.

A useful 401 decision tree

Authorization header reaches verifier?
  no -> fix client/proxy/header forwarding
  yes
   ↓
JWT shape and time window valid?
  no -> malformed / refresh / clock
  yes
   ↓
signature valid with issuer-bound trusted key?
  no -> alg/kid/key/JWKS/rotation
  yes
   ↓
iss matches expected issuer?
  no -> tenant/environment/discovery
  yes
   ↓
aud contains expected API?
  no -> resource/token acquisition
  yes
   ↓
correct token type?
  no -> Access Token vs ID Token
  yes
   ↓
authorization policy (scope/role) or non-JWT application logic

Exact symptoms and the layer they usually point to

SymptomStart here
jwt malformedheader value / token shape
TokenExpiredErrorexp, clock, refresh
invalid signaturekey/algorithm/signed bytes
kid not foundJWKS/cache/rotation
invalid issuerissuer/environment
invalid audienceaudience/resource
works in Postman, fails in browserclient token/header/CORS/proxy differences
works directly, fails through gatewayheader forwarding/gateway validation

What I do not do to “fix” a 401

  • disable signature verification;
  • accept any issuer/audience;
  • add a huge clock tolerance;
  • swap Access Token and ID Token semantics;
  • copy a decoded payload into a newly constructed token;
  • turn every 401 into 403 without understanding the failing layer.

Those changes hide the symptom by weakening the contract.

My production 401 checklist

  1. Reproduce the failing request and keep status, response headers, request ID, and relevant logs.
  2. Confirm Authorization: Bearer <token> reaches the verifier.
  3. Decode the original token with the JWT Decoder without treating claims as trusted.
  4. Check exp, nbf, and verifier clock.
  5. Verify signature and algorithm with the correct trusted key path.
  6. Compare iss exactly with configured issuer.
  7. Compare aud with the API's expected audience.
  8. Confirm the client sent an Access Token intended for this API.
  9. Only after authentication passes, evaluate scope/role/application authorization.
  10. Fix the layer that failed and keep the security checks enabled.

A 401 is useful once I turn it from a generic status into a sequence of observable authentication checkpoints.

What I capture before changing configuration

I record the token issuer, intended audience, key identifier, token timestamps, verification algorithm, and the exact component that returned the 401. I redact the token itself and never copy it into a ticket. This small evidence set makes it possible to compare a working and failing request without weakening issuer, audience, lifetime, or signature policy while investigating the incident.

Sources

Related guides