DEV Community

Benjamin
Benjamin

Posted on Originally published at jwt-base64-inspector.vercel.app

Your JWT payload is public. Read it before an attacker does.

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}
Enter fullscreen mode Exit fullscreen mode

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):

  1. Never put secrets in the payload — it's readable by design.
  2. Always set exp and check it on every request.
  3. Pin the algorithm — never trust alg from the token.
  4. Validate aud and iss; most JWT libraries default to loose matching.
  5. Use RS256 (or better) for anything shared between services.
  6. 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 (2)

Collapse
 
030dev profile image
Nick van Dort

Good reminder that signed doesn’t mean encrypted. I’d add that aud and iss validation become especially important once tokens start crossing service boundaries — a perfectly valid signature doesn’t mean the token was actually intended for your API. Algorithm pinning is another small check with a surprisingly large security impact.

Collapse
 
developer_tech profile image
Benjamin

Nailed it. Signed means "nobody tampered with it", it says nothing about whether it was ever meant for your API. aud is the check that only bites in production. Pinning the algorithm is the closest thing to a free security win in the whole spec.