PSA: Stop Pasting JWTs in Slack — Decode Them Locally Instead

(Updated: July 16, 2026 ) JWT jwt decode security webdev

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: none or wrong keys if your API misconfigures algorithm allowlists.
  • “401 means expired” — Often it’s aud, iss, clock skew, or a rotated signing key. exp is one field among many.

JWT structure (the 30-second version)

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.SIGNATURE
│        Header        │      Payload      │  Signature  │
PartWhat decode showsWhat decode cannot do
Headeralg, typ, kidProve algorithm wasn’t tampered
Payloadexp, sub, aud, custom claimsValidate signature
SignatureOpaque stringRecompute 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

  1. Reproduce minimally — One failing fetch from DevTools Network tab. Copy the Authorization: Bearer … value only; avoid dumping full HAR files into tickets.
  2. Decode locally — Paste into a JWT decode tool that runs in the browser. Confirm header alg is expected (not none unless you explicitly allow it in dev).
  3. Check the usual suspects — Table below covers 90% of “claims look fine” cases.
  4. 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.
  5. Rotate if leaked — If the token hit Slack, a screenshot, or a hosted decoder, treat it as compromised. Revoke refresh tokens / force re-auth.
SymptomCheck in payloadServer-side check
Random 401 after deployiss, aud still match new envJWKS URL, key rotation
Works locally, fails prodDifferent aud or cookie domainCORS + cookie SameSite
”Expired” but clock says OKexp in UTC vs localNTP skew on container
Admin flag in client onlyCustom claim not in signed schemaNever 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

ScenarioDecode locallyServer verify
”Why am I logged out?”Check exp, nbf, iatOptional
Authorization / RBACNever aloneAlways
Sharing in JIRARedact + use test token
Incident responseYes, to see scope of leakRevoke 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.

Try the jwt-decode tool

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.