What Actually Leaks When You Paste a JWT Into an Online Decoder
At one in the morning, debugging a 401 from a staging gateway that was not behaving like staging, I copied the bearer token out of a request header and pasted it into the first JWT decoder that Google offered me. It decoded. I got my answer, fixed the bug, and only afterwards worked out that the token was not from staging at all — I had grabbed it from the wrong browser tab. It was a production admin session with about forty minutes left on the clock, and I had just handed it to a website I had never heard of, whose privacy policy I had not read, which may or may not have logged it.
Nothing came of it. That is not the same as it having been fine. What the incident taught me is that most of the advice on this topic points at the wrong risk: people worry about whether the decoder can read their token, when the decoder reading the token is the least interesting thing that can happen.
A JWT Is Signed, Not Encrypted
The first two segments of a JWT are Base64url-encoded JSON, in the clear. There is no secret involved in reading them. Anyone holding the token — a proxy log, a browser extension, a support ticket, a screenshot — can read every claim inside it without any tool at all:
# Python: decode the payload of a token on stdin
echo "$TOKEN" | python3 -c 'import sys,json,base64
p=sys.stdin.read().strip().split(".")[1]
print(json.dumps(json.loads(base64.urlsafe_b64decode(p+"="*(-len(p)%4))),indent=2))'
// Node, one line
node -e 'console.log(JSON.parse(Buffer.from(process.argv[1].split(".")[1],"base64url")))' "$TOKEN"
// PHP, one line
php -r 'var_dump(json_decode(base64_decode(strtr(explode(".", $argv[1])[1], "-_", "+/"))));' "$TOKEN"
Each of those prints the same thing:
{
"sub": "1234567890",
"name": "Ada Lovelace",
"role": "admin",
"exp": 1755255600
}
The padding arithmetic in the Python version — "="*(-len(p)%4) — exists because JWT uses Base64url without padding, one of the small differences between Base64 variants that breaks naive decoders. Beyond that, decoding is not a capability. It is atob.
So "is it safe to let a website decode my token" is the wrong question. The website learns nothing from decoding that it did not already learn the instant you pasted the string.
The Token Itself Is the Credential
A JWT is a bearer token: whoever holds it is treated as the user it names, until it expires. There is normally no revocation list, because avoiding a database lookup is the entire point of the design. Check what you would actually be giving away:
date -u -d @1755255600 # exp claim as a UTC timestamp
If that time is in the future, the token is live. Anyone with a copy can replay it against your API for the remaining window with the role: admin claim intact. That is the leak — not the decoding, the copying. It applies equally to pasting a token into a chat channel, attaching it to a bug report, or leaving it in a terminal that gets screen-shared. Or dropping it into a link: a URL parameter is about the least private surface there is, which is why what goes into a prefilled AI prompt link has to be limited to server-side constants and published page metadata.
Which means the sensible default is boring: treat a production token exactly like a production password. Do not paste it anywhere you would not paste a password, and if you already did, act as though it is compromised, because you have no way to prove otherwise.
You Cannot Tell Whether a Decoder Runs Locally by Reading Its Homepage
Every online decoder claims your data never leaves your browser. Some are telling the truth. The claim is unfalsifiable from the marketing copy, and the source is usually a bundle you are not going to read at one in the morning.
There is a twenty-second test that settles it:
- Open the page, then open DevTools on the Network tab and clear it.
- Paste a throwaway token and decode.
- Look for any XHR or fetch request. A local decoder makes none.
- For certainty, tick Offline in DevTools (or in the Network conditions panel) and decode again. A tool that works with the network disabled cannot be uploading anything.
The offline step is the one that matters, because it does not depend on you noticing a beacon in a busy request list. It is also how I would want anyone to check our own JWT decoder and encoder rather than taking my word for it: the decode path is atob on the two segments, and signing uses crypto.subtle in the browser, so the page keeps working with the network switched off.
Generate the throwaway token for that test rather than using a real one. Any HS256 token with a nonsense secret will do — the tool does not care whether the signature is meaningful when you are only decoding.
The Secret Is a Different Category of Mistake Entirely
Decoding needs nothing. Verifying an HS256 signature needs the shared secret, and this is where a debugging session turns into an incident:
| What you paste | Who can use it | Blast radius | Reversible? |
|---|---|---|---|
| The token | Anyone holding it | One session, until exp |
Yes, if you can invalidate the session |
| An RS256 public key | Anyone; it is public by design | None | Not applicable |
| The HS256 shared secret | Anyone holding it | Every token for every user, forged at will, indefinitely | Only by rotating the secret and invalidating all tokens |
An HS256 secret is a signing key. Someone with it does not need to steal a token: they can mint one that says role: admin for any sub they like, and your API will verify it happily because the signature is genuine. That is the same shared-secret trade-off that makes HMAC signing unsuitable once more than one service needs to verify tokens — and the reason RS256 exists, where verifiers only ever hold a public key and the private signing key stays in one place.
Practical rule: paste tokens into a local tool if you must, and never paste a signing secret into anything with an address bar. Verify signatures in your own runtime, in a test, with the secret coming from your existing configuration.
If You Have Already Pasted One
Triage in this order, because the first two answers decide everything else.
Was it a secret or a token? A leaked HS256 secret is a key rotation, right now, plus invalidating every issued token. There is no smaller version of that response.
Is the token still live? Decode it locally and check exp. If it has passed, you are done — record it, move on. If not, revoke the session if your stack can (a denylist, a bumped token_version claim, a forced re-login for that user) and treat the window between now and exp as an exposure period.
What was in the claims? A token carrying role: admin, a tenant identifier, or long-lived scopes deserves a look at access logs for the affected subject. A token for a read-only account with three minutes left does not.
If your answer to "can we revoke a single token" is no, that is worth fixing on its own terms — the mechanics and the standard mistakes are in the JWT deep dive.
Keep the Token in Your Terminal
The one-liners above take five seconds and leak nothing, and they work on a plane. Keep one in your shell history or as a jwtd function, use a decoder you have personally watched work offline when you want the claims laid out nicely, and hold the line on secrets absolutely: a token is one session's problem, a signing key is everyone's. My one in the morning mistake cost nothing in the end, but I would not have known if it had — and that uncertainty is the actual price of pasting a credential into a stranger's website.