JWT

Backend Fundamentals · Backend Security · 8 min read

1. What problem does this solve?

ProblemWhy it mattersConsequence of not solvingReal‑world appearance
Stateless authenticationTraditional 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 authCookies 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 identityA 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 revocationSessions 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

ComponentPurposeTypical format
HeaderAlgorithm & token type{ "alg":"HS256","typ":"JWT" }
PayloadClaims (user id, scopes, iat, exp, etc.){ "sub":"123", "name":"Alice", "iat":1610000000, "exp":1610003600 }
SignatureHMAC or RSA signatureHMACSHA256(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

AspectJWTSession
Statelessness✔❌
Scalability✔❌ (shared store)
Revocation❌ (unless short‑lived or revocation list)✔
SizeLarger payload → more bandwidthSmall session ID
ComplexitySign/verify logicStore/lookup logic

Failure modes

  • Expired token – client must refresh.
  • Invalid signature – tampering detected.
  • Clock skew – iat/exp mis‑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

  1. User logs in → credentials verified.
  2. Auth service creates payload (sub, iat, exp, roles).
  3. Sign payload with secret or private key → token string.
  4. Send token to client (HTTP‑Only cookie or Authorization: Bearer).
  5. Client includes token on each request.
  6. Service verifies signature + checks exp, aud, etc.
  7. If valid → grant access; else → 401/403.
  8. 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

ConcernWhat to doWhy
ScalabilityShort 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.
ReliabilityUse 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.
ObservabilityLog token issuance, revocation events, failed verifications.Detect abuse, audit usage.
PerformanceKeep payload < 1 KB. Use fast libraries (jsonwebtoken v8+).Reduces bandwidth and CPU.
Error handlingReturn 401 for invalid/expired tokens; 403 for insufficient scopes.Clear client feedback.
Resource usageAvoid storing tokens in DB; rely on statelessness.Saves memory and I/O.
DeploymentDeploy signing keys to all nodes; use environment variables or secret stores.Consistency across services.
Failure recoveryImplement graceful fallback to session store if JWT verification fails temporarily.Avoid single point of failure.
RevocationUse short expiry + refresh token rotation; optionally maintain a revocation list (Redis).Allows immediate revocation of compromised tokens.

7. Common mistakes

MistakeWhy it’s bad
Storing secrets in codeExposes signing key; easy to leak.
Using HS256 with a weak keyAnyone with key can forge tokens.
Putting sensitive data in payloadPayload is Base64‑encoded, not encrypted.
Long‑lived access tokensCompromise window widens; no revocation.
Not validating exp/nbfTokens may be accepted after expiry.
Using same token for both access & refreshRefresh token can be used to get new access tokens indefinitely.
Ignoring clock skewClients with wrong time may reject valid tokens.
Not using HTTPSTokens can be intercepted.
Sending JWT in URL queryExposed 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 jti claim.

Trade‑offs

DecisionProsCons
Short access token + refreshTight security, low revocation costExtra round‑trip for refresh
Long access tokenFewer refresh callsHigher risk if stolen
Asymmetric signingNo shared secretSlightly slower verification
Symmetric signingFasterRequires 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 (jti blacklist).
  • Clock drift – Use NTP; allow small leeway (leeway option).

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

LevelQuestionKey answer points
BeginnerWhat is a JWT and what are its three parts?Header, Payload, Signature; Base64URL‑encoded; dot‑separated.
BeginnerWhy do we use the exp claim?Prevents indefinite validity; mitigates replay attacks; allows token rotation.
SeniorExplain 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.
SeniorCompare 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.
ScenarioA 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.

  1. Create a RefreshToken model in PostgreSQL with columns: id, user_id, token_hash, expires_at, revoked.
  2. Generate a random refresh token (e.g., 128‑bit hex) and store its hash.
  3. Expose /refresh endpoint that accepts the refresh token, verifies hash, checks expiry, rotates the token, and issues a new access token.
  4. Implement revocation: mark old refresh token as revoked.
  5. 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 exp to 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