JWT Tokens Explained: Structure, Security, and Common Mistakes
Understand how JSON Web Tokens work, what's inside them, and the security pitfalls that trip up developers in production.
JSON Web Tokens (JWTs) are everywhere — they power authentication in SPAs, mobile apps, and microservice architectures worldwide. Yet they're also one of the most misunderstood security primitives in web development. Getting them wrong can mean authentication bypass, privilege escalation, or full account takeover.
What is a JWT?
A JWT is a compact, URL-safe string that encodes a set of claims — assertions about a user or session. It looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
It's three Base64URL-encoded segments separated by dots:
- Header — algorithm and token type
- Payload — the claims (user data)
- Signature — cryptographic proof of integrity
Paste any JWT into our JWT Decoder to instantly inspect all three parts without touching any external server.
Anatomy of a JWT
Header
{
"alg": "HS256",
"typ": "JWT"
}
alg specifies the signing algorithm. Common values:
HS256— HMAC with SHA-256 (symmetric, single secret)RS256— RSA with SHA-256 (asymmetric, public/private key)ES256— ECDSA with SHA-256 (asymmetric, smaller keys)
Payload
{
"sub": "user_abc123",
"email": "alice@example.com",
"role": "admin",
"iat": 1711670400,
"exp": 1711756800
}
Standard registered claims:
| Claim | Meaning |
|---|---|
sub |
Subject (who the token is about) |
iss |
Issuer (who created the token) |
aud |
Audience (who should accept it) |
exp |
Expiration time (Unix timestamp) |
iat |
Issued at (Unix timestamp) |
nbf |
Not before (don't accept before this time) |
Signature
For HS256:
HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)
The signature proves the token hasn't been tampered with. The payload is NOT encrypted — it's just encoded. Anyone can read it.
Never store sensitive data like passwords or credit card numbers in a JWT payload.
HS256 vs. RS256: which to choose?
HS256 uses a single shared secret. Every service that needs to verify tokens must have the same secret. Simple for monoliths, but dangerous in multi-service architectures — if any service is compromised, an attacker can forge tokens.
RS256 uses asymmetric keys. The auth server signs with a private key; all other services verify with the public key. Compromising a consumer service doesn't give an attacker the ability to forge tokens. Prefer RS256 for any system with multiple services.
The alg: none vulnerability
One of the most infamous JWT attacks. Some older libraries accepted a token with "alg": "none" and no signature — treating it as valid. An attacker could craft:
{ "alg": "none" }
with any payload and bypass authentication entirely.
Fix: Always explicitly specify the allowed algorithms in your JWT library. Never accept none.
// ❌ Dangerous
jwt.verify(token, secret);
// ✅ Safe — only accept HS256
jwt.verify(token, secret, { algorithms: ["HS256"] });
Algorithm confusion attack
Another critical vulnerability: if your library auto-detects the algorithm from the header, an attacker can change RS256 to HS256 and sign the token using the public key as the HMAC secret (which is, by definition, public).
Fix: Always hardcode the expected algorithm. Never trust the alg header.
Token storage: where to keep JWTs
| Storage | XSS risk | CSRF risk | Notes |
|---|---|---|---|
localStorage |
High | None | Accessible to any JS on the page |
sessionStorage |
High | None | Cleared on tab close |
| HTTP-only cookie | None | Medium | Best for web apps; use SameSite=Strict |
| Memory (variable) | Low | None | Lost on refresh; for SPAs |
For web applications, HTTP-only cookies with SameSite=Strict is the most secure option. For native apps, secure storage APIs (Keychain, Keystore) are appropriate.
Expiration and refresh tokens
Short-lived access tokens (5–15 minutes) paired with long-lived refresh tokens is the standard pattern:
- User logs in → server issues access token (15 min) + refresh token (7 days, stored in DB)
- Client uses access token for API calls
- When access token expires, client sends refresh token → gets a new access token
- On logout, invalidate the refresh token in the database
This limits the damage window if an access token is stolen.
Revoking JWTs
JWTs are stateless — once issued, they're valid until expiration. This is a trade-off. Options to revoke early:
- Blocklist — store invalidated JTI (JWT ID) values in Redis. Check on each request.
- Short expiry — 5-minute tokens limit blast radius.
- Refresh token rotation — detect reuse of a rotated token as a sign of theft.
Debugging with the JWT Decoder
When debugging auth issues, use our JWT Decoder to:
- Inspect the full payload without writing code
- Check expiration timestamps in human-readable form
- Verify the algorithm being used
- Spot unexpected or missing claims
All decoding happens locally in your browser — tokens never leave your machine.
JWT security checklist
- Use RS256 or ES256 for multi-service architectures
- Explicitly specify allowed algorithms in your library
- Validate
exp,iss, andaudclaims on every request - Store tokens in HTTP-only cookies for web apps
- Use short access token lifetimes (15 min or less)
- Implement refresh token rotation with reuse detection
- Never put sensitive data in the payload
- Use HTTPS everywhere — a token in plaintext is as good as no auth
JWTs are powerful and convenient, but only as secure as their implementation. Understand the structure, know the attacks, and your auth layer will be solid.