How to Debug a 401 Unauthorized by Decoding the JWT (2026)
When an API returns 401 Unauthorized in 2026, the JWT is usually the smoking gun. Decode the token, read exp, aud, iss — and fix the bug in 90 seconds.
How to Debug a 401 Unauthorized by Decoding the JWT
You hit an API endpoint, the request has an Authorization: Bearer ... header, and the response is 401 Unauthorized. The token looks plausible — three dot-separated parts, hundreds of characters long. Before you escalate to backend, decode the JWT and read what the server is actually rejecting. In 2026 the answer is almost always in a single claim.
TL;DR
- A 401 with a JWT present means the token failed validation — decode it first, don't guess.
- The fastest fix is checking
expagainst the current Unix timestamp; expired tokens are the #1 cause. aud,iss, and thealgfield cover most of the remaining failures across multi-service backends.- Decode locally — never paste a production token into a server-side debugger.
- A 401 with no
WWW-Authenticateheader usually means a proxy stripped the credentials before they reached the app.
What "401 Unauthorized" Actually Means in 2026
The HTTP 401 status code is defined by IETF RFC 7235, and despite the literal word "Unauthorized," it specifically means authentication failed or is missing — not "you don't have permission." Permission failures are 403. This distinction is the source of half the wasted debugging hours in API work.
401 vs 403: the spec-level difference
When you get a 401, the server is telling you: "I have no idea who you are, or I don't trust the credentials you sent." When you get a 403, the server is saying: "I know exactly who you are, and that identity isn't allowed to do this." If you're seeing 401 with a JWT attached, the server either rejected the signature, the claims, the format of the Authorization header, or never saw the header at all.
How to read the WWW-Authenticate header
A compliant 401 response includes a WWW-Authenticate header that explains the failure. RFC 6750, which defines the Bearer scheme for OAuth 2.0, specifies error codes inside this header:
WWW-Authenticate: Bearer realm="api",
error="invalid_token",
error_description="The token expired at 2026-05-28T14:02:11Z"
The three error codes you'll see are invalid_request (malformed header), invalid_token (signature, expiry, or claim problem), and insufficient_scope (technically a 403 case but some implementations return 401). If your provider sets the error_description field, half your debugging is already done. If they don't, decode the token.
Why a 401 sometimes has no body
If you got a 401 with an empty response body and no WWW-Authenticate header, the request probably never reached your application. A CDN, API gateway, or reverse proxy intercepted it. Common culprits: Cloudflare's Access rules, AWS API Gateway's authorizer caching, and Nginx configurations that drop Authorization on internal proxy hops. Check your network tab and inspect the response headers — the absence of WWW-Authenticate is the signal.
How to Decode a JWT in 30 Seconds
A JWT is three base64url-encoded segments joined by dots: header.payload.signature. The first two are JSON; the third is a binary signature. You can decode the readable parts in any browser without ever exposing the token to a network request.
Splitting the three parts
Paste the token into the iKit JWT Decoder and it splits the segments and renders the JSON for you. Everything happens inside the page — the token doesn't leave your browser. That's the right pattern for production tokens, which can grant real access for the remaining lifetime of the exp claim.
If you want to do it manually in a terminal, the shape of a JWT lets you do this with two commands:
TOKEN="eyJhbGciOi..."
echo "$TOKEN" | cut -d. -f1 | base64 -d
echo "$TOKEN" | cut -d. -f2 | base64 -d
Note that base64 -d doesn't always accept base64url's URL-safe alphabet without translation; tr '_-' '/+' plus padding-correction is sometimes needed. The browser decoder handles all that internally.
Reading the header
The header tells you which algorithm the token was signed with and which key (if there's a kid) to verify against:
{
"alg": "RS256",
"typ": "JWT",
"kid": "k_2026_05"
}
If alg is none, stop debugging and start patching — see the alg: none vulnerability writeup for why that's a security incident, not a curiosity. If alg doesn't match what your backend expects (HS256 instead of RS256, or vice-versa), the verifier will reject the signature regardless of how valid the token otherwise looks.
Reading the payload
The payload is the part you'll spend the most time on. A representative one looks like:
{
"iss": "https://auth.example.com/",
"sub": "user_a8f9c2",
"aud": "https://api.example.com/v2",
"exp": 1748441531,
"iat": 1748437931,
"nbf": 1748437931,
"scope": "read:invoice write:invoice"
}
Every field there can cause a 401. A complete walkthrough of what each one means lives in our standard claims field-by-field guide; for debugging purposes you mostly care about the four time/identity claims.
Why Your JWT Returns 401 — The 7 Most Common Causes
Out of every 100 JWT-related 401s a team will see in a year, roughly 95 fall into one of these categories. Decode the token, then walk the table top-to-bottom.
| # | Symptom in decoded token | Most likely cause | First thing to check |
|---|---|---|---|
| 1 | exp < now |
Expired token | Re-acquire via refresh token |
| 2 | iat > now |
Clock skew | NTP on client or server |
| 3 | aud ≠ your API |
Token issued for another service | Auth provider audience setting |
| 4 | iss is a different domain |
Wrong tenant / environment | Env var pointing at staging vs prod |
| 5 | alg ≠ backend expectation |
Algorithm mismatch | Backend algorithms allowlist |
| 6 | kid references a missing key |
Key rotated, JWKS not refreshed | Restart service or clear JWKS cache |
| 7 | Token decodes but signature fails | Wrong secret / wrong public key | Compare HS256 secret or RS256 JWKS |
Expired token (the #1 cause, by far)
Compare exp to the current Unix timestamp. If exp is the smaller number, the token is dead. The iKit Unix Timestamp converter turns the number into a date you can read at a glance — you can paste 1748441531 in and immediately see "May 28, 2026 14:02 UTC." Refresh-token rotation is the production fix; in development, just re-login.
Wrong audience or issuer
In any multi-service architecture, JWTs are scoped to a specific audience. A token issued for https://api.example.com/v1 will be rejected by https://billing.example.com even though both services trust the same identity provider. The aud claim is the contract. Same goes for iss — staging and production identity providers issue tokens with different iss values, and a token mis-routed across environments will 401 every time.
Signature mismatch after key rotation
Asymmetric tokens (RS256, ES256) verify against a public key fetched from the issuer's JWKS endpoint. When the issuer rotates keys, old tokens reference a kid that's no longer in the published set, or your service has a cached JWKS that doesn't yet include the new kid. Compare the token's kid to the keys at the JWKS URL — if the kid is missing, you've found the bug. RS256 vs HS256 has its own gotchas covered in our HS256 vs RS256 comparison.
Reproducing the 401 From curl
If you can reproduce the failure with curl, you can isolate it from your app's auth library and confirm whether the issue is the token, the header, or the network path.
Hitting the endpoint without a token
This establishes the baseline. The server should return 401 with a WWW-Authenticate header naming the scheme:
curl -i https://api.example.com/v2/me
If you get 401 without a WWW-Authenticate header, the endpoint isn't speaking RFC 7235 properly — that's worth flagging but doesn't break your debugging.
Hitting it with the broken token
Send the actual failing token verbatim. Use single quotes so shell substitution doesn't mangle anything:
curl -i \
-H 'Authorization: Bearer eyJhbGciOi...' \
https://api.example.com/v2/me
Compare what curl -v shows on the wire to what your application is sending. A surprising number of 401s come from libraries that double-prefix the header (Bearer Bearer ...) or strip the Bearer token type.
Hitting it with a valid one
Reproduce the failure first, then re-acquire a token via your refresh flow or login endpoint, then retry. If a fresh token works and the old one doesn't, you've confirmed it was an exp problem and no further investigation is needed.
A Debugging Checklist For 401 in 2026
When the answer isn't obvious from the response body, run through this list in order. It's ranked by how often each item is the actual culprit:
- Decode the token in a browser-only decoder; don't paste production tokens into web forms backed by servers.
- Convert
expto a real datetime and compare to now — clock skew of 60+ seconds is common in containerized environments. - Verify
audmatches the API host you're calling, character-for-character. - Verify
issmatches the identity provider your backend is configured to trust. - Confirm the
algin the header matches what your verifier allows; rejectnoneexplicitly. - For RS256/ES256: check that the
kidexists in the issuer's current JWKS. - For HS256: check that the secret your backend uses matches the one used at sign time.
- Inspect the raw
Authorizationheader on the wire — proxies, service workers, and middleware can rewrite or strip it.
Compare exp and iat to current Unix time
exp and iat are both seconds since 1970-01-01 UTC. If exp - iat is unusually short (say, 60 seconds), the issuer is configured with an aggressive expiry — you'll see flapping 401s during traffic spikes when refreshes can't keep up. If iat is in the future, the client clock is ahead of the issuer's clock. Both are fixable but not by the application code.
Verify alg matches your backend expectation
This is where the alg: none family of attacks lives. A correct verifier hard-codes the allowed algorithm rather than trusting the value in the header. If your backend accepts alg from the token itself, an attacker can downgrade RS256 to HS256 and sign with the public key — a known class of vulnerability. If 401s appear after a library upgrade, check that the new version didn't loosen its algorithms configuration.
Inspect aud and iss for environment drift
When a deployment pipeline copies an .env from staging to production by mistake, the staging issuer remains trusted in production and tokens minted in either environment pass signature validation but fail at the aud check. This pattern produces 401s that only happen in production and only for some users — a classic "works on my machine" footprint.
Privacy Note: Why You Should Decode JWTs Locally
JWTs are bearer tokens. Anyone who has a valid one can act as the user it represents, until it expires. A casual habit of pasting tokens into the first JWT debugger that comes up on Google is the same as casually pasting passwords into a cloud notebook. Use a tool that runs entirely in your browser — the iKit decoder doesn't send the token anywhere, doesn't log it, doesn't have a backend at all. We unpack the reasoning and how to verify it yourself in our JWT Decoder vs jwt.io comparison.
For more on what's actually inside the token you just decoded, RFC 7519 is the canonical spec — it defines every standard claim and the rules for processing them. Worth a bookmark.
Related on iKit
- How to Decode a JWT in 2026 — Auth0 & Firebase Examples — A walkthrough of decoding tokens from the two most common identity providers in the wild, including what each provider puts in custom claims.
- JWT Decoder vs jwt.io: Privacy and Features Compared — Why where you decode the token matters as much as how you decode it, with a feature-level comparison.
- Inside a JWT: A Field-by-Field Guide to Standard Claims — The companion reference for every claim mentioned above:
iss,sub,aud,exp,nbf,iat,jti. - HS256 vs RS256: Which JWT Algorithm Should You Pick — Algorithm-mismatch 401s are the second-most-common bug after expiry; this explains the tradeoffs and how to lock down your verifier.
- alg: none JWT Vulnerability: Why It Still Bites in 2026 — A 401 caused by
alg: noneis a security finding, not a debugging task. Required reading if you're touching JWT verification code.
Related posts
Convert Unix Timestamp to Date Without Timezone Bugs (2026)
Converting a Unix timestamp to a date is one division operation, but the timezone you format it in is the whole bug. Here's how to get it right.
How to Debug a 400 Bad Request With URL Decoding (2026)
A 400 Bad Request usually traces to a URL decoding bug. Here's how to decode the query string, spot the broken character, and ship a fix in five minutes.
How to Decode a JWT in 2026 — Auth0 & Firebase Examples
Decode a JWT in 2026 with real Auth0 and Firebase tokens — header, payload, signature explained, common debugging traps, and why pasting tokens online is risky.