How to Decode & Debug a JWT Token Safely
Table of Contents
Last year a developer posted a JWT into a public debugging forum asking why it was expired. Within the thread, three separate people pointed out that the token's payload contained the user's email, their role (`admin: true`), and enough session detail to impersonate them on the site in question. The token was still valid for another six hours. The developer deleted the post, rotated the secret, and learned the most important JWT lesson the hard way: a JWT's payload is readable by anyone who holds the token. Decoding isn't hacking — it's just Base64url, and every decoder on the internet will do it for free.
That incident captures the two things you need to understand about JSON Web Tokens: their anatomy is simple enough that you can (and should) decode them yourself while debugging, and their security model is subtle enough that pasting them into random websites is genuinely dangerous. This guide covers both.
Anatomy: Header, Payload, Signature
A JWT is three Base64url-encoded segments joined by dots: `xxxxx.yyyyy.zzzzz`. Each segment has a job, and understanding the jobs is what makes debugging possible.
The header (first segment) is JSON describing the token itself — typically the type (`"typ": "JWT"`) and the signing algorithm (`"alg": "HS256"` or `"alg": "RS256"`). It's metadata, not sensitive, but it matters enormously during debugging: if your server expects RS256 and the token says HS256, verification fails, and the header tells you why in seconds.
The payload (second segment) is the actual content — a JSON object of claims: statements about the user and the token. Things like who the user is (`sub`), when the token expires (`exp`), when it was issued (`iat`), and any custom data your app stuffed in there (roles, permissions, tenant ID). This is the segment you'll decode most often, and it's the one people forget is public.
The signature (third segment) is the security. The server takes the encoded header, a dot, the encoded payload, and its secret key, runs them through the header's algorithm, and that's the signature. When a token arrives, the server recomputes the signature and compares. Match means the token is genuine and unmodified; mismatch means reject. You can't verify the signature without the secret — but you can decode the first two segments without it, which is all debugging usually needs.
Reading the Payload: The Claims That Matter
Decode a typical payload and you'll see a mix of standard (registered) claims and custom ones. The registered claims are the ones that cause 90% of debugging sessions, so learn them by sight:
- `exp` (expiration time): Unix timestamp after which the token must not be accepted. The single most common reason tokens get rejected. Note it's in seconds, not milliseconds — a classic trap when your code generates it with a millisecond clock.
- `iat` (issued at): Unix timestamp of when the token was created. Useful for debugging "this token should be valid" mysteries — compare it against `exp` to see the intended lifetime.
- `nbf` (not before): the token isn't valid before this time. Clock skew between servers makes this one bite: if the issuing server's clock is 30 seconds ahead of the verifying server, a fresh token can be rejected as "not yet valid."
- `iss` (issuer) and `aud` (audience): who created the token and who it's for. Multi-service architectures reject tokens with the wrong audience constantly — the token is genuine, it's just not for your service.
- `sub` (subject): usually the user ID. The claim your application code actually cares about.
Then come the custom claims — `role`, `permissions`, `tenant_id`, whatever your app needs. Two rules for custom claims: keep them small (every byte rides along on every request, usually in a header with size limits around 8 KB), and never put anything in them you wouldn't show the user, because the user can decode the token as easily as you can.
Why Your Token Got Rejected: The Debugging Checklist
When a token fails verification, work through this list in order — it resolves the vast majority of cases:
1. Is it expired? Decode the payload, read `exp`, convert it to a human date. Tokens with 15-minute lifetimes expire during long debugging sessions, and tokens generated by a script last Tuesday are long dead. This is the cause roughly half the time.
2. Is the signature actually valid? A token that decodes fine but fails verification usually has a signature problem: wrong secret, wrong algorithm, or the token was modified (even one changed character breaks it). If you control the server, re-sign a fresh token with the known secret and compare — if the fresh one verifies and the old one doesn't, the old token's signature was computed differently.
3. Check `aud` and `iss`. In microservice setups, grabbing a token minted for service A and sending it to service B fails audience validation. The token isn't broken; it's just at the wrong door.
4. Look for clock skew. If `nbf` is in the future by seconds or `exp` seems wrong by a consistent offset, the servers disagree about the time. Most libraries allow a small leeway (30–60 seconds) for exactly this reason — make sure yours is configured.
5. Check how you're transmitting it. Tokens get mangled in transit more often than people expect: URL-encoding turning characters into `%XX` sequences, copy-paste dropping characters, loggers truncating long strings. Compare the exact string your client sends against what the server receives — character for character.
Decode first, hypothesize second. The JSON Web Token (JWT) Decoder & Payload Inspector shows you the header and payload with the claims labeled and the timestamps converted to readable dates, which turns most of these checks into a ten-second glance.
The Security Rule: Client-Side Only, Never Paste Secrets
Here's the rule that the forum poster learned the expensive way: never paste a real token into a website you don't fully trust. Decoding a JWT requires no server — it's pure Base64url decoding plus JSON parsing, which means any legitimate decoder can run entirely in your browser. If a decoder site sends your token to its server (check the network tab — you'd be surprised how many do), that site now holds a live credential, and you have no idea what it does with it.
The safe approach has two parts. First, use decoders that process everything client-side — Toolenix tools run 100% in your browser, so a token you paste never leaves your machine. Second, treat tokens like passwords in your habits: don't paste them into chat windows, don't commit them to git, don't screenshot them into Slack. A token with `admin: true` and six hours of validity is a credential, full stop.
One more subtle point: don't put secrets in the payload, even if the token is signed. Signing proves the token wasn't tampered with; it does not hide the contents. I've seen API keys and internal URLs sitting in JWT payloads, readable by anyone with browser devtools. If the user shouldn't see it, it doesn't belong in the token — keep it server-side and reference it by ID.
Short Lifetimes and Refresh Tokens: The Bigger Picture
If access tokens should be short-lived (15 minutes is the standard advice), how do users stay logged in for weeks? The answer is the refresh token: a second, longer-lived credential (days to months) stored securely — in an HttpOnly cookie or secure storage, never in localStorage next to the access token — whose only job is requesting fresh access tokens from the auth server.
The split exists to limit blast radius. Access tokens fly around constantly — in headers on every API call, visible in devtools, logged in proxies — so they're designed to be disposable: if one leaks, it dies within minutes. Refresh tokens are used rarely (once per access-token lifetime) and guarded carefully; if one leaks, the auth server can revoke it, since refresh tokens are tracked server-side unlike stateless access tokens. When your access token expires mid-session and the app silently gets a new one without asking you to log in again, that's the refresh flow working as designed.
One practical consequence for debugging: when a user reports being "logged out randomly," check both tokens. An expired access token with a working refresh flow is invisible to the user; a failed refresh (revoked token, rotated secret, clock skew on the auth server) is what actually kicks people out. Decode the access token to confirm which half of the system is at fault before you start changing session code.
Try it free — no sign-up
Run the numbers yourself in seconds. Everything happens in your browser.
Open the JSON Web Token (JWT) Decoder & Payload InspectorFrequently Asked Questions
Can anyone decode my JWT?
Yes. The header and payload are only Base64url-encoded, not encrypted — anyone holding the token can read them with any decoder, no key required. Only the signature requires the secret to verify. Design your payloads accordingly: no passwords, no API keys, nothing the user shouldn't see.
Why is my valid-looking token being rejected as expired?
Usually one of three things: it genuinely expired (check `exp` against the current time — remember it's in seconds, not milliseconds), the issuing and verifying servers disagree about the time (clock skew), or your code generated `exp` with a millisecond timestamp, making it look like a date 50,000 years in the future or otherwise wrong. Decode the payload and read the actual timestamp first.
What's the difference between HS256 and RS256?
HS256 uses a single shared secret for both signing and verifying — simpler, but every verifier must hold the secret. RS256 uses a public/private key pair: the issuer signs with the private key, and anyone with the public key can verify without being able to forge tokens. RS256 is the better choice when multiple services verify tokens issued by one auth server.
How long should a JWT live?
Short — 15 minutes is the common default for access tokens, paired with a longer-lived refresh token (days to weeks) that mints new ones. Long-lived access tokens are convenient and dangerous: a stolen token works until it expires, and there's no way to revoke a stateless JWT before then except maintaining a denylist, which defeats the purpose of statelessness.
Is it safe to decode JWTs in an online tool?
Only if the tool processes everything in your browser without sending the token to a server. Decoding is a purely local operation, so there's no legitimate reason for a decoder to upload your token. The JSON Web Token (JWT) Decoder & Payload Inspector runs entirely client-side — but as a habit, prefer local tools for anything carrying live credentials, and never paste tokens into sites you haven't verified.
Why does decoding work without the secret key?
Because decoding and verifying are different operations. Decoding just reverses the Base64url encoding on the header and payload — no key involved, by design, so that any party can read the claims. Verification recomputes the signature using the secret and compares it to the token's signature — that does need the key, and it's the step that proves the token is genuine. A decoder shows you the contents; only the server holding the secret can confirm they're trustworthy.