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:
Authorizationmissing entirely;Bearerprefix 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;
kidselection;- key rotation and cache freshness;
- original
header.payloadbytes.
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:
audlooks 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:
| Status | What I investigate first |
|---|---|
| 401 | token absent, malformed, expired, untrusted, wrong issuer/audience |
| 403 | authenticated 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 expectsorders-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
| Symptom | Start here |
|---|---|
jwt malformed | header value / token shape |
TokenExpiredError | exp, clock, refresh |
invalid signature | key/algorithm/signed bytes |
kid not found | JWKS/cache/rotation |
invalid issuer | issuer/environment |
invalid audience | audience/resource |
| works in Postman, fails in browser | client token/header/CORS/proxy differences |
| works directly, fails through gateway | header 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
- Reproduce the failing request and keep status, response headers, request ID, and relevant logs.
- Confirm
Authorization: Bearer <token>reaches the verifier. - Decode the original token with the JWT Decoder without treating claims as trusted.
- Check
exp,nbf, and verifier clock. - Verify signature and algorithm with the correct trusted key path.
- Compare
issexactly with configured issuer. - Compare
audwith the API's expected audience. - Confirm the client sent an Access Token intended for this API.
- Only after authentication passes, evaluate scope/role/application authorization.
- 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.