A JSON Web Token looks like random noise, but it's just three pieces of Base64Url-encoded text joined by dots. You can read the first two without any key at all. This guide shows what's inside a JWT, how to decode one in a few seconds, and the one mistake people make most often: treating a decoded token as a trusted one.
What a JWT looks like
Here's a short example token, split onto three lines so you can see the parts:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSIsImlhdCI6MTc2MDAwMDAwMCwiZXhwIjoxNzYwMDAzNjAwfQ
.lk8sRfkhPK6DSJBJ9VIZ_wILrtBHF885jTyVYUxyyfMEvery JWT has the same shape, header.payload.signature:
| Part | Contains | Readable without a key? |
|---|---|---|
| Header | The signing algorithm (alg) and token type (typ) | Yes |
| Payload | The claims: who the token is about, when it expires, roles and so on | Yes |
| Signature | A hash of the header and payload, made with a secret or private key | No, and you can't reverse it |
The first two parts are encoded, not encrypted. Anyone holding the token can read them.
Decode a JWT in your browser
The fastest way is to paste the token into the JWT decoder. It splits the token, decodes the header and payload into formatted JSON, and turns timestamp claims like exp and iat into readable dates. Decoding happens in your browser, so the token isn't uploaded anywhere.
- Copy the full token, usually from an
Authorization: Bearer …header, a cookie, or your browser's dev tools. - Paste it into the decoder's input.
- Read the header and payload on the right. Click any section to copy it.
Decode a JWT by hand
Since each part is Base64Url, you can decode it with standard tools. Base64Url is ordinary Base64 with - and _ in place of + and /, and with the trailing = padding removed.
In the browser console or Node.js:
function decodeJwtPart(part) {
const base64 = part.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
return JSON.parse(atob(padded));
}
const [header, payload] = token.split('.');
console.log(decodeJwtPart(header)); // { alg: "HS256", typ: "JWT" }
console.log(decodeJwtPart(payload)); // { sub: "1234567890", name: "Ada", iat: ..., exp: ... }In a terminal:
echo 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSJ9' | tr '_-' '/+' | base64 -dIf base64 -d complains about invalid input, add one or two = characters to the end until the length is a multiple of four. You can also paste a single part into the Base64 decoder.
The claims you'll see most often
The payload is a JSON object of claims. A few are standardized in RFC 7519:
| Claim | Name | Meaning |
|---|---|---|
iss | Issuer | Who created the token, for example your auth server's URL |
sub | Subject | Who the token is about, usually a user ID |
aud | Audience | Which service the token is meant for |
exp | Expiration time | After this moment the token must be rejected |
nbf | Not before | Before this moment the token must be rejected |
iat | Issued at | When the token was created |
jti | JWT ID | A unique ID, useful for revoking single tokens |
Anything else, like role, email or scope, is a custom claim your app defines.
exp, nbf and iat are Unix timestamps in seconds, not milliseconds. If you're debugging an "expired token" error, convert exp with the timestamp converter and compare it with the current time, keeping time zones and clock skew between servers in mind.
Decoding is not verifying
This is the important part. Decoding only tells you what a token claims. It doesn't tell you whether those claims are true, because anyone can write a payload that says "role": "admin" and encode it.
Trust comes from the signature. Your server recomputes it from the header, the payload and its key:
- HS256 / HS384 / HS512 use one shared secret (HMAC). The same key signs and verifies.
- RS256 / ES256 and friends use a key pair. The auth server signs with its private key, and anyone can verify with the public key, often published at a JWKS URL.
When you verify on a server, use a maintained library and pin the algorithms you expect. Never accept "alg": "none", and check exp, aud and iss along with the signature. A decoder like ToolDock's is for reading and debugging tokens. It won't tell you a token is valid, and it shouldn't.
Is it safe to paste a token into a decoder?
A JWT is a credential. Whoever holds a live access token can usually act as that user until it expires. So:
- Prefer decoders that run entirely in the browser (ToolDock's decoder does), or decode locally with the snippet above.
- Use expired tokens or tokens from a test environment when you can.
- Don't put secrets in the payload. Since anyone can decode it, a JWT is the wrong place for passwords, API keys or personal data you wouldn't show the user.
Quick reference
- A JWT is
header.payload.signature, each part Base64Url-encoded. - The header and payload are readable by anyone. Only the signature needs a key.
- Time claims are Unix seconds.
- Decoding is for debugging. Your server must verify the signature before trusting anything.
Got a token to inspect? Open the JWT decoder and paste it in.