If you've built or consumed a modern API, there's a very good chance JWT came up. JSON Web Tokens are the standard mechanism for stateless authentication in web applications, used everywhere from mobile apps to microservices. But a lot of developers use them without really understanding what's inside — and that leads to security mistakes. Let's break them down properly.
What Is a JWT?
A JWT is a compact, URL-safe string that encodes a JSON payload along with a cryptographic signature. The server creates a token when a user logs in and sends it to the client. The client sends this token with every subsequent request. The server can verify the token's authenticity without making a database lookup — that's the key advantage.
A JWT looks like this:
Three parts separated by dots: header.payload.signature
Part 1: The Header
The header is a base64url-encoded JSON object that specifies the token type and signing algorithm:
{
"alg": "HS256",
"typ": "JWT"
}
Common algorithms:
- HS256 — HMAC with SHA-256. Uses a shared secret key. Simple but both parties must share the secret.
- RS256 — RSA with SHA-256. Uses a public/private key pair. The server signs with the private key; anyone can verify with the public key.
- ES256 — ECDSA with SHA-256. Like RS256 but uses elliptic curves — smaller tokens, faster verification.
Part 2: The Payload (Claims)
The payload contains the data you want to transmit, called claims. Claims are just JSON key-value pairs. JWT defines a set of registered claim names:
{
"iss": "https://auth.creesol.com",
"sub": "user_42",
"aud": "https://api.creesol.com",
"exp": 1716393600,
"iat": 1716307200,
"nbf": 1716307200,
"jti": "a3f8b2c1-d4e5-6789-0abc-def012345678",
"name": "Alice",
"email": "alice@example.com",
"roles": ["admin"]
}
iss— Issuer: who created the tokensub— Subject: who the token is about (usually a user ID)aud— Audience: who the token is intended forexp— Expiration: Unix timestamp after which the token is invalidiat— Issued At: when the token was creatednbf— Not Before: token is not valid before this timejti— JWT ID: unique identifier, used to prevent replay attacks
You can add any custom claims you want alongside the registered ones. Keep the payload small — it's transmitted on every request.
Part 3: The Signature
The signature proves the token hasn't been tampered with. For HS256, it's computed as:
HMAC-SHA256(
base64url(header) + "." + base64url(payload),
secret_key
)
If an attacker modifies any part of the header or payload and resubmits the token, the signature won't match and the server will reject it. The key point: the signature provides integrity, not confidentiality. Anyone can decode the header and payload — they're just base64url encoded, not encrypted. Never put sensitive data in a JWT payload unless you're using JWE (JSON Web Encryption).
The Authentication Flow
- User submits credentials (username + password) to
POST /auth/login - Server verifies credentials against the database
- Server creates a JWT signed with its secret key, sets a short expiration (e.g., 15 minutes)
- Server also issues a refresh token (opaque, long-lived, stored in database)
- Client stores the access JWT in memory (not localStorage — more on this below)
- Client sends
Authorization: Bearer <token>header on each API request - Server verifies the signature and checks
exp— no database lookup needed - When the JWT expires, client uses the refresh token to get a new JWT
Security Pitfalls to Avoid
1. Don't store JWTs in localStorage
localStorage is accessible to any JavaScript on the page. If your site has an XSS vulnerability, an attacker can steal every token. Store access tokens in memory (a JavaScript variable) and refresh tokens in an HttpOnly cookie, which JavaScript cannot read.
2. Validate the algorithm
Early JWT libraries had a vulnerability where setting "alg": "none" in the header would bypass signature verification. Always explicitly specify which algorithms you accept and reject none.
// Node.js — explicitly allow only HS256
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
3. Check all claims
Verify exp, iss, and aud on every request. A valid signature on an expired or wrong-audience token should still be rejected.
4. Keep tokens short-lived
Since JWTs are stateless, you can't invalidate them from the server (without maintaining a blocklist, which defeats the purpose). Short expiration times (5–15 minutes) limit the window of damage if a token is stolen.
5. Use RS256 for distributed systems
If multiple services need to verify tokens, RS256 is better: only your auth service holds the private key, while any service can verify using the public key. No shared secret to leak across services.
Need to inspect a JWT, decode its payload, or verify its structure?
Try the Free JWT Decoder
CREESOL