JWTs Explained: What's Actually Inside That Token
August 3, 2026
112 reads
If you've ever pasted a JWT into a decoder and been surprised that you could just... read it, that's not a bug. That's the entire design. Here's what's actually going on.
Three parts, separated by dots
A JWT (JSON Web Token) is just three base64url-encoded chunks joined by periods: header.payload.signature. Each part has one job:
- Header: says which algorithm was used to sign the token (usually
HS256orRS256). - Payload: the actual data: who this token represents, what they're allowed to do, when it expires. This is called the set of "claims."
- Signature: a cryptographic proof that the header and payload haven't been tampered with since the server issued them.
Paste any JWT into JWT Decoder and you'll see exactly this: header and payload as readable JSON, signature as an opaque string.
A token, decoded
Here is what a typical payload looks like once decoded:
{
"sub": "user_1842",
"role": "editor",
"iat": 1790000000,
"exp": 1790003600
}
Anyone can read this. It says the token is for user user_1842, with the role editor, issued at one time and expiring an hour later (exp minus iat is 3,600 seconds). The server's job is to check that nobody has edited those values.
The standard claims worth knowing
A few claim names are standardized, so you'll see them in almost every token:
sub: the subject, usually a user IDiss: the issuer, who created the tokenaud: the audience, who the token is meant forexp: the expiry time, after which the token must be rejectednbf: "not before", a time before which the token isn't valid yetiat: when the token was issuedjti: a unique token ID, useful for tracking or revoking
Times are in Unix seconds. If a token is being rejected unexpectedly, check exp, nbf and aud first.
The part that surprises people: it's encoded, not encrypted
Base64 is not encryption. It's just a reversible text encoding, the same way you could describe a number in binary or hex. Anyone who gets hold of a JWT, including a user inspecting their own browser's network tab, can decode the payload and read every claim in it. No secret key required. If you want to see how that encoding works, our guide to Base64 explains it.
This is by design: a JWT is meant to be inspectable, so any service that receives it can read the claims directly instead of calling back to a database. What the signature protects is integrity, not confidentiality: it proves the payload hasn't been altered, but it does nothing to hide what's in it.
The practical consequence: never put a password, API key, or anything actually secret into a JWT payload. If it's sensitive, it doesn't belong in the token. A user ID, a role, or an expiry timestamp is exactly what payloads are for.
What the signature actually stops
The signature is generated using a secret key (for HS256) or a private key (for RS256) that only the issuing server holds. If someone edits the payload, say changing "role": "user" to "role": "admin", the signature no longer matches, and any server correctly verifying the token will reject it.
This is why the verification step matters as much as the token itself. A JWT that's never checked against its signature is just an unverified claim someone is making about themselves. The security comes entirely from the receiving server actually validating it before trusting anything inside.
HS256 or RS256?
The two algorithms you'll meet most differ in how the key works:
- HS256 uses one shared secret to both sign and verify. It's simple and fast, and fine when the same service issues and checks tokens. The risk is that every service able to verify a token can also forge one.
- RS256 uses a private key to sign and a public key to verify. Only the issuer can create tokens, but anyone with the public key can check them. This suits setups where many services need to verify tokens issued by one authority.
Mistakes that cause real vulnerabilities
Most JWT security problems come from a handful of mistakes:
- Not verifying the signature at all, or decoding a token and trusting it.
- Accepting any algorithm the token claims. A token's header can say
"alg": "none", meaning "unsigned". Always tell your library which algorithms you accept, and never allownone. - Weak secrets. An
HS256secret that is short or guessable can be brute-forced offline. Use a long, random value; our Password Generator can create one. - Long-lived tokens. A token that is valid for months can't easily be taken back if it's stolen.
- Skipping
audandisschecks, which lets a token meant for one service be replayed on another.
Where to store a token in the browser
There's no perfect answer. A token in localStorage can be read by any script on the page, so a cross-site scripting bug exposes it. A token in an HttpOnly cookie can't be read by scripts, but then you need to defend against cross-site request forgery. Many teams keep the access token short-lived and in memory, and the refresh token in an HttpOnly cookie.
Access tokens, refresh tokens and revocation
Because a JWT is self-contained, a server can't easily "cancel" one before it expires. The usual answer is to keep access tokens very short-lived (minutes) and pair them with a longer-lived refresh token that can be revoked. When a user logs out or a token is compromised, you revoke the refresh token and the access token simply expires soon after.
Building and inspecting tokens
- Use JWT Decoder to paste in any token and instantly see its header, payload, and signature broken out. It's handy for debugging why an API call is being rejected, or checking an
exp(expiry) claim without writing any code. - Use JWT Encoder to build a token with a custom payload and secret, which is useful for testing an API locally before wiring up real authentication.
- Use Hash Generator to explore how hashing works, the building block behind signatures.
Two habits worth keeping
- Always check
exp. Most authentication bugs involving JWTs come down to expired tokens being accepted, or tokens with no expiry at all. Set one, and check it server-side on every request. - Verify signatures server-side, always. Never trust a claim in a payload just because the token "looks valid." Decoding is not the same as verifying, and only signature verification actually confirms the token wasn't forged or altered.
Related tools
Related tools: