What Is a JWT?
A JSON Web Token (JWT) is a compact, URL-safe token format defined by RFC 7519. A JWT consists of three Base64URL-encoded parts separated by dots:
header.payload.signature
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTc0MDAwMDAwMH0.abc123
- Header — algorithm and token type:
{"alg":"HS256","typ":"JWT"} - Payload — claims (user data, expiration):
{"sub":"user_123","exp":1740000000} - Signature — HMAC or RSA signature over header + payload
JWTs are used heavily for stateless authentication — the server signs a token at login, the client stores and sends it with every request, and the server verifies the signature without consulting a database.
Critical JWT Vulnerability: The "none" Algorithm
The most dangerous JWT misconfiguration is accepting tokens signed with the alg: "none" algorithm. Some early JWT libraries would accept unsigned tokens if the header specified "alg": "none", allowing attackers to forge tokens for any user.
// Attacker-crafted payload (no real signature)
{"alg":"none","typ":"JWT"}.{"sub":"admin","role":"superuser"}.
Fix: Always explicitly specify the allowed algorithms in your JWT verification library. Never accept "none".
// Node.js — jsonwebtoken library
jwt.verify(token, secret, { algorithms: ["HS256"] });
Algorithm Confusion: RS256 vs HS256
Another classic attack: a server uses RS256 (asymmetric, public/private key pair). The public key is, by definition, public. An attacker takes that public key and creates a token signed with HS256 using the public key as the HMAC secret. If the server doesn't validate the algorithm, it may verify the HMAC against its known "public key" — and accept a forged token.
Fix: Always whitelist the exact algorithm(s) your system uses. Never allow callers to specify the algorithm.
Where to Store JWTs in the Browser
This is one of the most debated topics in frontend security:
| Storage | XSS risk | CSRF risk | Recommendation |
|---|---|---|---|
localStorage | High — any JS can read it | None | Avoid for auth tokens |
sessionStorage | High — same as above | None | Avoid for auth tokens |
| httpOnly cookie | None — JS cannot access | Medium | Recommended |
| Memory (JS var) | Low — lost on refresh | None | Good for short sessions |
The consensus for most applications: store JWTs in httpOnly, Secure, SameSite=Strict cookies. This prevents XSS from stealing tokens while CSRF protection comes from SameSite and/or CSRF tokens.
Always Set Token Expiration
A JWT without an expiration claim (exp) is valid forever — if stolen, it gives permanent access. Always set a short expiration for access tokens:
// Access token: 15 minutes
jwt.sign({ sub: userId }, secret, { expiresIn: "15m" });
// Refresh token: 7-30 days (stored securely, used to get new access tokens)
jwt.sign({ sub: userId }, refreshSecret, { expiresIn: "7d" });
The access/refresh token pattern keeps access tokens short-lived (limiting the damage of a stolen token) while maintaining usable sessions via refresh tokens.
Never Put Sensitive Data in the Payload
A JWT payload is Base64URL-encoded — not encrypted. Anyone who holds the token can decode and read the payload. Never include:
- Passwords or password hashes
- Credit card numbers or PII
- Internal system details an attacker could exploit
- API keys or secrets
Keep payloads lean: user ID, roles, expiration. If you need confidential claims, use JWE (JSON Web Encryption) which adds an encryption layer on top of JWT.
JWT Revocation — The Stateless Tradeoff
JWTs are stateless by design: the server doesn't track which tokens are active. This creates a revocation problem. If a user logs out or you need to invalidate a token (account compromise, role change), you can't simply "delete" it — it remains valid until expiry.
Common revocation strategies:
- Short expiration — 15-minute access tokens limit the revocation window to 15 minutes maximum.
- Token blacklist — Maintain a Redis set of revoked token IDs (
jticlaim). Check on every request. Reintroduces statefulness but solves revocation. - Token versioning — Store a
token_versionfield per user. Increment it on logout/compromise. Verify the token'sverclaim matches the current version.
Validate All Claims
Signature verification is not enough. Always validate:
exp— is the token expired?nbf— is the token valid yet (not before)?iss— is it from the expected issuer?aud— is it intended for your service?
Good JWT libraries handle this automatically if you configure them correctly. Always read your library's documentation to understand which claims are validated by default.
Key Management
For HS256 (symmetric), the secret key must be at least 256 bits of random data. For RS256/ES256 (asymmetric), use a minimum 2048-bit RSA or 256-bit EC key. Store secrets in environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault) — never hardcode them or commit them to version control.
Rotate keys periodically. When rotating, support a brief overlap period where both old and new keys are accepted, then retire the old key.
Use Copynix to Debug JWTs
During development, use the JWT Decoder to inspect token headers and payloads instantly in your browser — no data sent to any server. The JWT Builder lets you construct and sign tokens for testing your API's verification logic.
Summary
JWT security comes down to a handful of critical practices: whitelist algorithms, set short expirations, store tokens in httpOnly cookies, keep payloads free of sensitive data, and validate all standard claims. Get these right, and JWTs are a solid foundation for stateless authentication.
