Security · 9 min read

JWT Tokens Demystified: Structure, Security, and Pitfalls

JWTs are simpler than they look and more dangerous than they appear. A practical walkthrough of structure, claims, signing algorithms, and the pitfalls.

By WebGenAI · · Updated

JWTs — JSON Web Tokens — are the backbone of modern authentication and authorization. They appear in API requests as `Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.signature_here`, get parsed thousands of times per second on busy services, and have caused some of the most notable security breaches of the last decade when implemented carelessly.

This article explains what's inside a JWT, why people use them, the security properties they actually offer (and the ones they don't), the algorithms involved, and the real-world pitfalls that keep showing up in postmortems. By the end you should be able to read any JWT, decide when to use one, and avoid the mistakes that get applications exploited.

Anatomy of a JWT

A JWT is three Base64URL-encoded segments joined by dots. The header is a small JSON object declaring the signing algorithm and token type. The payload is a JSON object containing the claims — facts about the user, the issuer, the expiry. The signature is a cryptographic signature over the header and payload combined.

Anyone with the token can read the header and payload — they're not encrypted, just encoded. The signature is what makes the token tamper-evident: change a byte in the header or payload and the signature stops matching. Servers verify the signature on every incoming token using a shared secret (for HMAC algorithms) or a public key (for RSA/ECDSA algorithms).

Standard claims

The JWT spec defines a short list of registered claim names. `iss` is the issuer (which service minted this token), `sub` is the subject (usually the user ID), `aud` is the audience (which service the token is intended for), `exp` is the expiration time as a Unix timestamp, `nbf` is the not-before time, `iat` is the issued-at time, and `jti` is a unique token ID.

You can add any custom claims you want — `roles`, `email`, `tenant_id`, whatever the application needs. Two warnings: don't put sensitive data in the payload (it's readable by anyone), and don't put much data there at all — every byte goes over the wire on every request. Aim for a few hundred bytes total.

Signing algorithms: HS256, RS256, ES256

HS256 uses HMAC with SHA-256 and a shared secret. The same secret signs and verifies. It's the simplest setup but means every party that needs to verify tokens must hold the signing secret. That's fine for a monolith, problematic when you want a separate authentication service.

RS256 uses RSA with SHA-256 — the signing service holds a private key, and every verifier holds the corresponding public key. The public key is safe to distribute freely. This is the right choice when many services need to verify tokens issued by a central auth service.

ES256 is the same idea as RS256 but with ECDSA over the P-256 curve. Signatures are much smaller (64 bytes vs 256 bytes for RSA), and verification is faster. Prefer ES256 over RS256 for new designs.

The `alg: none` disaster

Early JWT libraries had a critical bug: when the token's header declared `"alg": "none"`, they would skip signature verification entirely and accept the token. Attackers could forge any payload they wanted by simply setting `alg` to `none` and omitting the signature. This was patched in libraries years ago but still occasionally pops up.

Defense: always pass the allowed algorithms explicitly to your verify function. Never let the token's own header decide which algorithm to use. `jwt.verify(token, key, { algorithms: ['ES256'] })` rejects `alg: none`, `alg: HS256` (algorithm confusion), and anything else you didn't expect.

Algorithm confusion attacks

Some libraries used the same function to verify both HMAC and RSA-signed tokens, picking the verification method from the token's `alg` header. If a verifier was configured with an RSA public key but accepted an HMAC-signed token, the attacker could mint forged tokens by using the public key as the HMAC secret.

Defense: same as above. Lock down which algorithms are acceptable at the verifier level, and never let the token decide. Modern libraries do this by default; old ones might not.

Expiry, refresh tokens, and revocation

JWTs are designed to be self-contained, which means they can't be revoked by the server once issued — the server just trusts the signature and the embedded claims. That's a problem if a token is stolen or a user's permissions are downgraded.

The standard mitigation is short expiry. Access tokens live 5–15 minutes. When they expire, the client exchanges a longer-lived refresh token for a fresh access token. The refresh token is checked against a server-side database on every refresh, so it can be revoked instantly. The access token still works until it expires, but the window of risk is small.

For higher-security applications, maintain a server-side denylist of revoked token IDs (`jti`) and check it on every request. This trades the statelessness benefit of JWTs for the ability to revoke.

Where to store JWTs in a browser

Local storage is convenient but exposed to any XSS vulnerability — attacker JavaScript can read the token and exfiltrate it. Cookies with `HttpOnly` and `Secure` flags are safer because JavaScript can't read them, but they require CSRF protection (`SameSite=Strict` or anti-CSRF tokens).

The best practice in 2026 is `HttpOnly; Secure; SameSite=Lax` cookies for the refresh token, with access tokens kept in memory only. The access token never touches storage and is regenerated from the refresh token on page load.

Should you even use JWTs?

JWTs are a great fit for cross-service authentication where the signature lets services trust the token without consulting a central database, for short-lived API access tokens, and for OAuth/OIDC ID tokens.

JWTs are a bad fit for simple session management in a monolith — a random session ID stored in a database is simpler, smaller, more revocable, and avoids every JWT-specific footgun. Don't reach for JWT just because it's trendy.

Wrapping up

JWTs are a powerful, well-specified format that solves real problems in distributed authentication — but they require careful implementation. Always pin the allowed algorithms. Use short expiry plus refresh tokens. Store tokens safely in the browser. Don't put secrets in the payload. Don't use JWT when a session cookie would do.

If you need to inspect a JWT right now, our free JWT decoder displays the header, payload, and signature side by side, decodes nested timestamps to human-readable dates, and runs entirely in your browser. Paste, inspect, debug.