Here's the sentence most developers learn a little too late: a JWT's header and payload are not encrypted. They're Base64. Anyone who can see your token can read every field inside it — your user ID, your role, your custom claims. The signature protects integrity, not confidentiality.
So before you store a secret in a token, let's be clear about what's actually readable. This is the same token your auth server hands out, decoded:
{"alg":"HS256","typ":"JWT"} <- header: the algorithm
{"sub":"1234567890","name":"Ada Lovelace", <- payload: fully readable
"iat":1516239022,"exp":1799999999}
Both parts are plain JSON behind a Base64 wrapper. A kid with atob() and five minutes reads them. If your payload holds a password, an email, or a role that grants privileges — you just shipped a credential leak that your "encrypted" token vibes will never catch.
The claims that matter (and what they're for):
-
sub— who the token belongs to -
iat— when it was issued -
exp— when it dies (tokens without this never expire — a flaw) -
iss/aud— who issued it and who it's for (your API) -
jti— a unique ID, your best tool for revocation
HS256 vs RS256 — pick carefully.
HS256 signs with one shared secret. Whoever holds the secret can both verify and forge tokens. RS256 uses a public/private key pair — your server verifies with the public key, and only the private key can sign. For anything that crosses service boundaries, RS256. And if your library lets a token dictate its own algorithm, you've opened the classic alg confusion attack — lock the algorithm you accept, always.
The 12-point hardening checklist (abridged to the ones that matter):
- Never put secrets in the payload — it's readable by design.
- Always set
expand check it on every request. - Pin the algorithm — never trust
algfrom the token. - Validate
audandiss; most JWT libraries default to loose matching. - Use RS256 (or better) for anything shared between services.
- Use short-lived access tokens + refresh tokens, not one token that lives for months.
That last one is the difference between a breach and an incident: a token that can be revoked and expires in 15 minutes is a credential. A token that lives for 90 days is a skeleton key.
I keep a full copy of this guide, with the complete 12-point checklist and every standard claim explained, at https://jwt-base64-inspector.vercel.app/guides/jwt-security. And if you want to see what your own tokens actually contain, decode one client-side — it never leaves your browser: https://jwt-base64-inspector.vercel.app
One honest caveat: this is the guide I wish I'd read before my first auth system, not after. Reading it after a production incident costs a lot more attention.
Which of the six checks above is the one most teams skip? I'd bet on aud validation — it's the quiet one that libraries don't nag you about.
Top comments (0)