JWT
Backend Fundamentals · Backend Security · 8 min read
1. What problem does this solve?
| Problem | Why it matters | Consequence of not solving | Real‑world appearance |
|---|---|---|---|
| Stateless authentication | Traditional session stores (e.g., Redis, DB) require a lookup on every request. | Extra latency, higher memory usage, single‑point of failure. | Web apps that need horizontal scaling (e.g., a SaaS platform). |
| Cross‑domain / API auth | Cookies are limited to the same origin; APIs often need a token that can be sent from any client. | Clients must maintain session state; CORS headaches. | Mobile apps calling a backend, microservices communicating. |
| Decoupled identity | A single token can carry claims that multiple services understand without querying a central auth server. | Tight coupling, more network calls. | Microservice architecture where each service verifies the token locally. |
| Scalable revocation | Sessions can be invalidated by deleting a key, but JWTs are self‑contained. | Hard to revoke a single token; risk of stale credentials. | OAuth2 flows where refresh tokens are rotated. |
If you skip JWTs and rely solely on server‑side sessions, you’ll need a shared session store, which adds complexity and can become a bottleneck. JWTs let each service verify a token independently, which is essential for high‑throughput, distributed systems.
2. Mental model
Analogy: The “Digital Passport”
Imagine a traveler (the client) who wants to enter multiple countries (services). Instead of presenting a new visa at every border, they carry a passport that lists their identity and the countries they’re allowed to visit. Each border guard (service) can read the passport and decide if entry is granted, without asking the traveler’s home country.
Engineering model
- Token = passport.
- Header = passport type & algorithm.
- Payload = claims (identity, scopes, expiry).
- Signature = stamp that proves the passport was issued by a trusted authority.
The token is self‑contained: once issued, any service that knows the signing key can validate it without contacting the issuer.
3. Deep explanation
Internals
| Component | Purpose | Typical format |
|---|---|---|
| Header | Algorithm & token type | { "alg":"HS256","typ":"JWT" } |
| Payload | Claims (user id, scopes, iat, exp, etc.) | { "sub":"123", "name":"Alice", "iat":1610000000, "exp":1610003600 } |
| Signature | HMAC or RSA signature | HMACSHA256(base64UrlEncode(header)+'.'+base64UrlEncode(payload), secret) |
The three parts are Base64URL‑encoded and concatenated with dots.
Important terminology
- Claims – key/value pairs in the payload.
Registered claims:iss,sub,aud,exp,nbf,iat.
Public claims: any name that follows RFC 7519.
Private claims: custom data agreed upon by parties. - Signing key – secret (HS256) or private key (RS256).
- Verification – checking signature + optional claim validation.
- Refresh token – long‑lived token used to obtain a new access token.
- Token rotation – issuing a new refresh token each time one is used.
Trade‑offs
| Aspect | JWT | Session |
|---|---|---|
| Statelessness | ✔ | ❌ |
| Scalability | ✔ | ❌ (shared store) |
| Revocation | ❌ (unless short‑lived or revocation list) | ✔ |
| Size | Larger payload → more bandwidth | Small session ID |
| Complexity | Sign/verify logic | Store/lookup logic |
Failure modes
- Expired token – client must refresh.
- Invalid signature – tampering detected.
- Clock skew –
iat/expmis‑validated. - Replay attacks – same token reused; mitigated by short expiry or nonce.
Limitations
- No built‑in revocation – you must implement a blacklist or short expiry.
- Payload visibility – anyone can read claims; never put secrets in payload.
- Size constraints – keep payload lean; large tokens increase latency.
Where it fits
┌───────────────────────┐
│ Client (Browser/CLI) │
└───────┬───────────────┘
│
▼
┌───────────────────────┐
│ API Gateway / Auth │
│ (issues JWT) │
└───────┬───────────────┘
│
▼
┌───────────────────────┐
│ Microservice A │
│ (verifies JWT) │
└───────┬───────────────┘
│
▼
┌───────────────────────┐
│ Database / Redis │
└───────────────────────┘
4. How it works
- User logs in → credentials verified.
- Auth service creates payload (
sub,iat,exp,roles). - Sign payload with secret or private key → token string.
- Send token to client (HTTP‑Only cookie or
Authorization: Bearer). - Client includes token on each request.
- Service verifies signature + checks
exp,aud, etc. - If valid → grant access; else → 401/403.
- Optional: if token expired, client exchanges refresh token for new access token.
sequenceDiagram
participant U as User
participant A as Auth Service
participant S as API Service
participant DB as Database
U->>A: POST /login {email, pwd}
A->>DB: SELECT * FROM users WHERE email=?
DB-->>A: user record
A->>A: create JWT payload
A->>A: sign payload
A->>U: Set-Cookie: jwt=...; HttpOnly
U->>S: GET /protected
S->>S: read Authorization header
S->>S: verify signature
S->>S: check exp, roles
S-->>U: 200 OK
5. Practical implementation
Node.js / TypeScript example (FastAPI‑style)
// auth.ts
import jwt, { JwtPayload } from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
const SECRET = process.env.JWT_SECRET!; // e.g., 'supersecretkey'
export interface AuthPayload extends JwtPayload {
sub: string; // user id
roles: string[];
}
// Generate token
export function signToken(userId: string, roles: string[]): string {
const payload: AuthPayload = {
sub: userId,
roles,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 15 * 60, // 15 min
};
return jwt.sign(payload, SECRET, { algorithm: 'HS256' });
}
// Middleware to protect routes
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader) return res.status(401).json({ error: 'Missing token' });
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, SECRET) as AuthPayload;
// Attach user info to request for downstream handlers
(req as any).user = payload;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
Usage
import express from 'express';
import { authMiddleware, signToken } from './auth';
const app = express();
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// 1️⃣ Verify credentials (omitted)
const userId = '42';
const roles = ['user'];
const token = signToken(userId, roles);
res.cookie('jwt', token, { httpOnly: true, secure: true, sameSite: 'lax' });
res.json({ ok: true });
});
app.get('/profile', authMiddleware, (req, res) => {
const user = (req as any).user;
res.json({ id: user.sub, roles: user.roles });
});
app.listen(3000);
FastAPI (Python) quick snippet
# auth.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
import jwt
from datetime import datetime, timedelta
SECRET = "supersecretkey"
ALGORITHM = "HS256"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def create_access_token(data: dict, expires_minutes: int = 15):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=expires_minutes)
to_encode.update({"exp": expire, "iat": datetime.utcnow()})
return jwt.encode(to_encode, SECRET, algorithm=ALGORITHM)
def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
return payload
except jwt.PyJWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
app = FastAPI()
@app.post("/token")
def login():
# verify user
token = create_access_token({"sub": "42", "roles": ["user"]})
return {"access_token": token, "token_type": "bearer"}
@app.get("/profile")
def profile(user: dict = Depends(get_current_user)):
return {"id": user["sub"], "roles": user["roles"]}
6. Production considerations
| Concern | What to do | Why |
|---|---|---|
| Scalability | Short access token lifetime (5–15 min). Use refresh tokens stored in secure HTTP‑Only cookie or DB. | Limits window for compromised token; reduces load on revocation checks. |
| Reliability | Use a CDN or edge proxy to cache public keys if using asymmetric signing. | Avoids key fetch latency on every request. |
| Security | - Use RS256 or ES256 (public/private keys). - Store secrets in Vault/KMS. - Rotate keys regularly. | Mitigates brute‑force, key compromise. |
| Observability | Log token issuance, revocation events, failed verifications. | Detect abuse, audit usage. |
| Performance | Keep payload < 1 KB. Use fast libraries (jsonwebtoken v8+). | Reduces bandwidth and CPU. |
| Error handling | Return 401 for invalid/expired tokens; 403 for insufficient scopes. | Clear client feedback. |
| Resource usage | Avoid storing tokens in DB; rely on statelessness. | Saves memory and I/O. |
| Deployment | Deploy signing keys to all nodes; use environment variables or secret stores. | Consistency across services. |
| Failure recovery | Implement graceful fallback to session store if JWT verification fails temporarily. | Avoid single point of failure. |
| Revocation | Use short expiry + refresh token rotation; optionally maintain a revocation list (Redis). | Allows immediate revocation of compromised tokens. |
7. Common mistakes
| Mistake | Why it’s bad |
|---|---|
| Storing secrets in code | Exposes signing key; easy to leak. |
| Using HS256 with a weak key | Anyone with key can forge tokens. |
| Putting sensitive data in payload | Payload is Base64‑encoded, not encrypted. |
| Long‑lived access tokens | Compromise window widens; no revocation. |
Not validating exp/nbf | Tokens may be accepted after expiry. |
| Using same token for both access & refresh | Refresh token can be used to get new access tokens indefinitely. |
| Ignoring clock skew | Clients with wrong time may reject valid tokens. |
| Not using HTTPS | Tokens can be intercepted. |
| Sending JWT in URL query | Exposed in browser history & logs. |
8. Senior engineer perspective
Architecture decisions
- Signing algorithm – Prefer asymmetric (RS256) for multi‑service environments; allows key rotation without redeploying all services.
- Key management – Integrate with Vault/KMS; rotate keys on schedule.
- Token storage – Use HTTP‑Only secure cookies for web clients; bearer header for APIs.
- Refresh strategy – Rotate refresh tokens; store hash in DB to detect reuse.
- Revocation – Maintain a short‑lived blacklist in Redis; use
jticlaim.
Trade‑offs
| Decision | Pros | Cons |
|---|---|---|
| Short access token + refresh | Tight security, low revocation cost | Extra round‑trip for refresh |
| Long access token | Fewer refresh calls | Higher risk if stolen |
| Asymmetric signing | No shared secret | Slightly slower verification |
| Symmetric signing | Faster | Requires secret distribution |
Bottlenecks
- Key verification – CPU‑bound if many requests; mitigate with caching public keys.
- Refresh token DB lookups – Use Redis with TTL to avoid DB hits.
Failure scenarios
- Key compromise – Rotate keys, revoke all tokens (
jtiblacklist). - Clock drift – Use NTP; allow small leeway (
leewayoption).
Monitoring
- Auth success/failure rates – Detect brute‑force attempts.
- Token age distribution – Spot unusually old tokens.
- Revocation list hits – Gauge revocation usage.
Cost/performance
- CPU – JWT verification is lightweight; still consider load testing.
- Bandwidth – Keep token size minimal; use compression if needed.
When NOT to use JWT
- Highly sensitive data that must never be exposed even in Base64.
- Systems requiring immediate revocation without short expiry.
- Low‑traffic, single‑server apps where a session store is simpler.
9. Interview questions
| Level | Question | Key answer points |
|---|---|---|
| Beginner | What is a JWT and what are its three parts? | Header, Payload, Signature; Base64URL‑encoded; dot‑separated. |
| Beginner | Why do we use the exp claim? | Prevents indefinite validity; mitigates replay attacks; allows token rotation. |
| Senior | Explain how you would rotate JWT signing keys in a production system. | Generate new key pair, publish new public key, keep old key for a grace period, update services, revoke old tokens via jti or blacklist. |
| Senior | Compare JWTs with traditional session cookies in terms of scalability and revocation. | JWTs are stateless → no shared store; revocation harder → short expiry or blacklist; sessions require shared store → easier revocation but less scalable. |
| Scenario | A client receives a 401 after a successful login. The token was just issued. What could be wrong? | Clock skew causing exp/nbf to fail; wrong signing algorithm; token sent in wrong header; token tampered; server using different secret. |
10. Hands‑on task
Goal: Add a refresh‑token flow to the existing Node.js auth example.
- Create a
RefreshTokenmodel in PostgreSQL with columns:id,user_id,token_hash,expires_at,revoked. - Generate a random refresh token (e.g., 128‑bit hex) and store its hash.
- Expose
/refreshendpoint that accepts the refresh token, verifies hash, checks expiry, rotates the token, and issues a new access token. - Implement revocation: mark old refresh token as revoked.
- Add unit tests to ensure rotation and revocation work.
Use crypto.randomBytes, bcrypt for hashing, and pg for DB access. Commit the changes and run the test suite.
11. Quick revision
- JWT = JSON Web Token: header.payload.signature.
- Header: algorithm (
alg) + type (typ). - Payload: claims (
sub,exp,iat,nbf,aud,iss, custom). - Signature: HMAC or RSA; proves authenticity.
- Stateless auth → no session store needed.
- Short access token + refresh token rotation = best practice.
- Use
expto limit token lifetime. - Never put secrets in payload.
- Verify signature + claims on every request.
- Store signing keys securely (Vault/KMS).
- Use HTTP‑Only, Secure cookies for web clients.
- Monitor auth failures, token age, revocation hits.
12. References
- RFC 7519 – JSON Web Token (JWT)
- jsonwebtoken npm package
- FastAPI Security – OAuth2
- OWASP JWT Cheat Sheet
- AWS Cognito JWT guide