The most consequential misunderstanding about JSON Web Tokens is that they are encrypted. They are not. A JWT is signed, which is a different guarantee entirely, and confusing the two leads directly to leaking data you assumed was hidden.
Three segments, separated by dots
The header says which algorithm signed the token and, often, which key was used. The payload holds the claims — who the token is about, who issued it, when it expires, and whatever else the issuing system chose to include. The signature is computed over the first two segments using a secret or private key.
The header and payload are Base64url encoded, not encrypted. Anyone holding the token can decode them instantly, without any key, which is exactly what a decoder does. Encoding is for safe transport, not secrecy.
A real token, decoded
Decode the first segment of a typical token and you get a small JSON object such as {"alg":"HS256","typ":"JWT"}. It says the token is a JWT, signed with HMAC-SHA256 using a shared secret. Tokens signed with a key pair say RS256 or ES256 instead, and often carry a kid naming which key was used, so the receiver can pick the right one when keys are rotated.
The second segment is the payload, for example {"sub":"1234567890","name":"Ada","iat":1767225600,"exp":1767229200}. The short names are registered claims, defined by the standard so every system reads them the same way. Everything else is a private claim the issuer chose to add, and that is where email addresses, roles and internal IDs tend to leak.
iss(issuer): who created the token, usually the URL of the login service.sub(subject): who the token is about, usually a user ID.aud(audience): which service the token is meant for. A receiver should reject a token addressed to someone else.exp,nbfandiat: when it expires, when it becomes valid and when it was issued, as Unix timestamps in seconds.jti(JWT ID): a unique ID for the token, which is what makes a deny list of revoked tokens possible.
What the signature proves
The signature proves the token has not been altered since it was issued, and that it was issued by someone holding the key. That is all. It does not hide the contents, it does not prove the token is still valid, and it does not prove the token has not been stolen and replayed by someone else.
Which is why alg: none is dangerous. It declares the token unsigned, and any system that accepts such a token is trusting data an attacker can freely rewrite. Reject it explicitly rather than relying on a library's defaults.
Expiry, and what it doesn't cover
exp is a Unix timestamp in seconds, and a token past it must be rejected. nbf does the reverse, marking a token as not yet usable. Both are worth checking explicitly rather than assuming the library did it.
Neither covers revocation. A token issued for a week remains cryptographically valid for a week even if the user logs out, changes their password, or is deleted. If you need to revoke early, you need server-side state — a deny list or short-lived tokens with refresh — because the token itself cannot tell you.
In short
Never put anything sensitive in a JWT payload; assume it is public. Check expiry yourself, reject alg: none explicitly, and remember that a valid signature says nothing about whether the token should still be honoured.
JWT Decoder
Decode a JSON Web Token, read its claims, check expiry, and verify HS, RS or ES signatures — without the token ever leaving your browser.
Keep reading
Why your link preview isn't showing, and how to fix it
A shared link with no card is nearly always one of five things. How previews are fetched, which tags matter, and why Slack works when WhatsApp won't.
How to read a regular expression without panicking
Regex looks like line noise until you know the four things it is made of. A practical guide to reading any pattern, plus the mistakes that waste the most time.
How to read messy JSON, and fix the errors it throws
Deeply nested API responses are hard to hold in your head. How to navigate JSON structurally, and what the three most common parse errors actually mean.