What is actually inside a JWT
Three base64 segments, one signature, and a lot of confusion about which part proves what. A short tour, and why decoding is not verifying.
A JSON Web Token looks like an opaque blob of secure-seeming characters, and it is neither opaque nor, by itself, secure. It is three pieces of base64 joined by dots, two of which anyone can read. Understanding which part does what clears up most of the confusion around them.
Three segments
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← header
.
eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWRhIn0 ← payload
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gF ← signatureThe first two are base64url-encoded JSON. Decode them and you get readable objects. The third is a cryptographic signature over the first two, and it is the only part that requires a key.
The header
Says how the token was signed. Almost always something like {"alg":"HS256","typ":"JWT"}. It is metadata for the verifier, not for you.
The payload
The claims — who the token is about, when it expires, what it permits. Some names are standardised:
sub— subject, usually a user idiss— issuer, who minted itaud— audience, who it is intended forexp— expiry, as a Unix timestamp in secondsiat— issued atnbf— not valid before
The time claims are seconds, not milliseconds — a frequent source of tokens that appear to expire in 1970 or in the year 56000. Our timestamp converter detects which unit you have pasted.
What the signature actually proves
The signature is computed over the header and payload using a secret (for HMAC algorithms like HS256) or a private key (for RSA and ECDSA ones like RS256). Change a single character of the payload and the signature no longer matches.
So the signature proves integrity and origin: this token was issued by someone holding the key, and has not been altered since. It proves nothing about confidentiality, because there is none.
Decoding is not verifying
This is the distinction that matters most, and the one most often collapsed.
Decoding is base64 in reverse. It needs no key, works in any browser, and tells you what the token claims. Any decoder — including ours — does only this.
Verifying recomputes the signature with your key and checks it matches, then checks the token has not expired and was intended for you. It requires the key, so it belongs on your server and nowhere else.
A token whose claims look right may still be forged. Anyone can craft a JWT saying {"role":"admin"}; what they cannot do is sign it without your key. Trusting a decoded payload without verifying the signature is the classic JWT vulnerability.
The alg:none problem
Early JWT libraries honoured a header saying "alg":"none", which declares the token unsigned — and dutifully accepted it. An attacker could take a valid token, rewrite the payload, set the algorithm to none, drop the signature, and be admitted.
Modern libraries reject this, but the lesson generalises: never let the token tell you how to validate it. Decide server-side which algorithm you accept and refuse everything else.
Practical notes
- Keep them short-lived. A JWT cannot be revoked without extra machinery, because verification is offline by design. Minutes to hours, not weeks.
- Mind the size. Every claim travels on every request. Tokens stuffed with permissions get expensive.
- Watch where you store them. In localStorage, any XSS can read the token. An httpOnly cookie is generally safer, with CSRF handled separately.
- Do not paste live tokens anywhere. Including into decoders. A JWT is a credential; treat a real one like a password.
Our JWT decoder reads the header and claims in your browser, renders the time claims as real dates, and says clearly that it does not verify anything — because no page on the open web should be asking for your signing key.