PSA: Stop Pasting JWTs in Slack — Decode Them Locally Instead
Someone on your team pasted a production Bearer token into a random “jwt decode” site because Auth0 returned 401 and the exp claim “looked fine.” That token is now in a third-party log. This happens weekly on r/webdev — and the fix is never “decode harder,” it’s decode locally and verify server-side.
- “Decoding proves the token is valid” — Decode only Base64Url-decodes JSON. Anyone can read the payload; signature verification requires your secret or public key on the server.
- “jwt.io is fine for prod” — Hosted decoders may log inputs, appear in analytics, or leak via support tickets. Use a browser-local tool for real credentials.
- “Payload says admin: true, ship it” — Clients can craft tokens with
alg: noneor wrong keys if your API misconfigures algorithm allowlists. - “401 means expired” — Often it’s
aud,iss, clock skew, or a rotated signing key.expis one field among many.
JWT structure (the 30-second version)
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.SIGNATURE
│ Header │ Payload │ Signature │
| Part | What decode shows | What decode cannot do |
|---|---|---|
| Header | alg, typ, kid | Prove algorithm wasn’t tampered |
| Payload | exp, sub, aud, custom claims | Validate signature |
| Signature | Opaque string | Recompute without secret/key |
Example payload after decode:
{
"sub": "user_8f3a",
"exp": 1720000000,
"aud": "https://api.myapp.com",
"iss": "https://login.myapp.com"
}
Convert exp with new Date(exp * 1000) — always in UTC. Off-by-one timezone bugs cause “but it expires tomorrow” confusion.
Debug workflow that won’t get you paged
- Reproduce minimally — One failing
fetchfrom DevTools Network tab. Copy theAuthorization: Bearer …value only; avoid dumping full HAR files into tickets. - Decode locally — Paste into a JWT decode tool that runs in the browser. Confirm header alg is expected (not
noneunless you explicitly allow it in dev). - Check the usual suspects — Table below covers 90% of “claims look fine” cases.
- Compare with server logs — Your API should log why verification failed (signature, exp, aud). Decode tells you what the client sent; logs tell you what the server rejected.
- Rotate if leaked — If the token hit Slack, a screenshot, or a hosted decoder, treat it as compromised. Revoke refresh tokens / force re-auth.
| Symptom | Check in payload | Server-side check |
|---|---|---|
| Random 401 after deploy | iss, aud still match new env | JWKS URL, key rotation |
| Works locally, fails prod | Different aud or cookie domain | CORS + cookie SameSite |
| ”Expired” but clock says OK | exp in UTC vs local | NTP skew on container |
| Admin flag in client only | Custom claim not in signed schema | Never trust client claims |
Real scenarios from threads (anonymized)
Slack paste — SRE pastes access token to ask “why 401?” Token valid 15 more minutes. Attacker with log access doesn’t need your API — they need that string once.
Algorithm confusion — Legacy API accepts HS256 and RS256. Attacker sends alg: none or swaps to HMAC with public key as secret. Decode shows pretty JSON; server must enforce allowed algs.
Refresh vs access — Frontend sends refresh token to resource API. Decode shows long exp; API expects short-lived access token. Fix is client logic, not “decode again.”
When decode is enough vs when it is not
| Scenario | Decode locally | Server verify |
|---|---|---|
| ”Why am I logged out?” | Check exp, nbf, iat | Optional |
| Authorization / RBAC | Never alone | Always |
| Sharing in JIRA | Redact + use test token | — |
| Incident response | Yes, to see scope of leak | Revoke sessions |
Tool: decode without uploading
The jwt-decode tool runs entirely in your browser. Paste the token, inspect header and payload JSON, copy claims for your ticket — no account, no server upload. Use it the way you’d use DevTools, not the way you’d use a pastebin.
TL;DR
Decode locally for debugging. Verify on the server for security. Never paste prod tokens into random websites. If exp looks fine but you still get 401, check aud, iss, keys, and clock skew — not another decoder.