JWT, Explained
JSON Web Tokens are the de-facto authentication primitive of the modern web. The format is straightforward; the security pitfalls are not. This guide covers the structure, the claims that matter, and the four mistakes nearly every team makes at least once.
The structure
A JWT is a string with three dot-separated parts: header.payload.signature. Each part is base64url-encoded. The header and payload are JSON objects; the signature is a binary hash bound to the header+payload contents and a signing key.
Example (truncated, line-broken for readability):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9 . SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Decode the header, you get something like {"alg":"HS256","typ":"JWT"}. Decode the payload, you get the claims. The signature isn't human-readable.
The claims that matter
The payload is just JSON, so it can hold anything. But there are seven registered claims (RFC 7519) that every JWT library understands and most identity providers populate:
iss— issuer. Who minted the token. Always validate this.sub— subject. The user or principal the token is about.aud— audience. Who the token is intended for. Always validate this.exp— expiry. Unix timestamp; reject if now > exp.nbf— not before. Reject if now < nbf.iat— issued at. Used for freshness checks.jti— JWT ID. Unique per token, useful for revocation lists.
For application data, use unregistered claims (just JSON keys). Common conventions: email, name, roles. Don't put secrets in the payload — the payload is base64-encoded but not encrypted. Anyone with the token can read it.
Algorithms — and the algorithm trap
JWT supports many algorithms. The common ones:
- HS256, HS384, HS512 — HMAC with SHA. Symmetric. Same secret signs and verifies.
- RS256, RS384, RS512 — RSA-PSS or RSASSA-PKCS1. Asymmetric. Private key signs, public key verifies.
- ES256, ES384, ES512 — ECDSA. Asymmetric, smaller keys/signatures than RSA.
- EdDSA (Ed25519) — newer, faster, smaller. Use this for new systems if your library supports it.
- none — no signature. Never accept this.
The four pitfalls
1. Accepting alg: none
Some old JWT libraries accept tokens with {"alg":"none"} and no signature, treating them as valid. An attacker who controls the token can drop the signature and change the alg, and the server says "ok." Cure: explicitly whitelist allowed algorithms in your verification call. Never trust the alg field alone.
2. Algorithm confusion (RS256 → HS256)
If your service uses RS256 and exposes the public key (which is normal), an attacker can craft a token with {"alg":"HS256"} and sign it using the public key as if it were an HMAC secret. A naive verifier that uses the alg field to pick the verification function will use the public key as an HMAC secret — and accept it. Cure: lock the algorithm at verification time, don't read it from the token.
3. Not validating aud
If service A and service B trust the same identity provider, a token issued for A may be replayed against B if B doesn't validate that aud == B. The fix is one line of code, but it's frequently skipped because "we control both sides." Skipping it is how many SaaS products get cross-tenant token replay vulnerabilities.
4. Long-lived tokens with no revocation
JWTs are stateless by design — the server doesn't track them. That means if a token leaks, the only way to "revoke" it is to wait for exp. If your tokens last 30 days, leaked tokens last 30 days. Cure: keep access tokens short-lived (5-15 min) and use a revocable refresh-token flow for the longer session.
JustKit's role
The JustKit JWT decoder shows you the header and payload in plain text. It does not verify the signature — that requires the secret/key, which we don't have and shouldn't ask for. For verification, use your library's debug tooling (jsonwebtoken in Node, PyJWT in Python, etc.). Use the JustKit tool when you need to read what's in a token quickly without a CLI handy.