JWTs are everywhere. Every time you log in to a web app, an API issues you a JSON Web Token, and that token carries your identity, your permissions, and often a few personal details in its payload. The format itself was stable for a decade, then a flurry of activity in 2024-2025 — RFC 8725 (JWT best current practices) became a hard requirement for new specs, the “JWTs are bad” debate reignited over session sizing, and several large providers moved to opaque tokens for browser use. The result: more developers than ever are inspecting tokens by hand. When something goes wrong — your session expires at the wrong time, an API rejects a token you thought was valid, a teammate says “what’s in the JWT?” — the first thing most developers do is paste the token into an online decoder.
The problem: most of those decoders send your token to a server. The token itself is not a secret (it’s just base64-encoded JSON), but the contents of the payload often are: a user ID, an email, sometimes a session ID, sometimes a list of permissions. If your service uses JWTs to talk to other internal services, the payload might even include internal service names or feature flags.
A token pasted into a third-party decoder is a small information leak. Most of the time it doesn’t matter. But the principle matters: you should be able to inspect a token without trusting someone else’s infrastructure.
What a JWT actually looks like
A JWT is three base64url-encoded strings, separated by dots (for more on how Base64 works, see our Base64 encoding explained guide):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGljZSIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The three parts are the header, payload, and signature. Decode the first two and you get JSON:
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"sub": "12345",
"name": "Alice",
"iat": 1516239022
}
The signature is a cryptographic hash that proves the token was issued by someone who knows the secret. Anyone can decode the payload — that’s not the security boundary. The security boundary is whether you can verify the signature.
The verification vs decoding distinction
This is where a lot of developers go wrong. Decoding is not verifying. A decoder just base64-decodes the JSON so you can read it. Anyone can do that, and it doesn’t prove the token is authentic. Verification is the act of checking the signature against a known secret to confirm the token wasn’t tampered with.
- To debug a token’s contents (what claims does it have, when does it expire), decoding is enough.
- To authenticate a user, you need verification, and it has to happen on a server you trust.
Most online decoder tools only decode. They display the header and payload but never ask for your secret — because they’re not verifying, they’re just showing you what’s in the token.
How to decode locally
You don’t need a third-party tool for this. A JWT is just three base64url strings. Open a terminal:
echo 'eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGljZSIsImlhdCI6MTUxNjIzOTAyMn0' | tr '_-' '/+' | base64 -d
That’s the payload. The header is the first segment, same trick.
If you’d rather not memorise the tr '_-' '/+' dance, the JWT decoder on DevSpeedTools does the same thing in the browser. No network request, no upload, no log. Open DevTools → Network, paste a token, and confirm yourself that nothing leaves your machine.
When you actually need verification
If you’re trying to debug an authentication issue and you need to know whether the token is valid (not just what it contains), you need to verify the signature. The verification has to happen with the same secret or public key your authentication server uses. That means it has to happen on infrastructure you control.
A few options for verification that don’t involve pasting your secret into a website:
- In Node.js:
jsonwebtoken.verify(token, process.env.JWT_SECRET). - In Python:
jwt.decode(token, key, algorithms=['HS256']). - In Go:
token, err := jwt.Parse(tokenString, keyFunc). - CLI:
jwt-cli(Rust) orpyjwtwith a one-liner.
If you do find yourself needing an online verifier for some reason, look for one that explicitly says it runs in the browser (the source is usually visible). The honest tools say so. The rest you should assume upload your token to a server.
The “invalid signature” error
A common reason developers end up on a decoder is the error invalid signature. That message is doing two things:
- Telling you the token was signed with a different secret than the one you’re using to verify.
- Telling you the token may have been tampered with.
Or — and this is the one most people miss — the token may be expired. Check the exp claim in the payload. If the current Unix timestamp is past exp, the token is invalid. Some libraries raise invalid signature for expired tokens, which is misleading.
The JWT decoder shows the exp value alongside the rest of the payload so you can spot expired tokens at a glance. The same goes for nbf (not before), iat (issued at), and aud (audience) — all useful for debugging.
The TL;DR
JWT decoders are useful. They are also a place where developers get casual about pasting production tokens. A safer default:
- Use a local or in-browser decoder for quick inspection. No upload, no leak.
- Use a CLI or your own code for verification. Never paste a secret into a website.
- Check the standard claims first —
expexpired is the cause of most “my token suddenly stopped working” issues.
The token is not a secret. The contents of the token often are. Treat it accordingly.