Overview
A JSON Web Token (JWT) is a compact, signed token with three base64url parts: header (algorithm), payload (claims such as user ID, expiry, scopes), and signature. Any service with the signing key or public key can verify the token and trust its claims without calling a database or the issuer.
That statelessness is the main benefit and the main risk. JWTs scale well across microservices, but once issued they remain valid until they expire, so revocation requires short lifetimes, refresh tokens, or deny lists. JWTs are signed, not encrypted: anyone can read the payload, so never put secrets in it.
The wristband shows your name, access level, and expiry time, and has a hologram seal. Any guard can check the seal without calling the office. But if the office wants to revoke it early, guards will not know unless they check a revocation list.
When to use it
- Access tokens verified by many services without a shared session store.
- OIDC ID tokens that convey user identity.
- Short-lived, signed claims between systems (signed URLs, email links).
- Stateless APIs where database lookups per request are too costly.
Where it shows up in interviews
Recognize it when: many services must verify identity cheaply.
- Design auth for a microservice platform
- Design an API gateway
Recognize it when: logout, revocation, and refresh.
- Design secure mobile app login
- Design session management at scale
Where it is used in real software
Google, Microsoft, and Okta issue ID tokens as JWTs signed with RS256; apps fetch public keys from a JWKS endpoint.
Issuers publish multiple keys identified by kid, so they can rotate signing keys without breaking verification.
Early libraries accepted unsigned tokens when the header said alg: none; modern libraries require an explicit allowed algorithm.
Key terms
- Header
- { alg: RS256, typ: JWT, kid: key-id }.
- Claims
- sub (subject), iss (issuer), aud (audience), exp (expiry), iat (issued at), plus custom claims.
- HS256 vs RS256
- Shared-secret HMAC vs asymmetric RSA (or ES256) signatures.
- JWKS
- JSON Web Key Set: public keys published by the issuer.
- Refresh token
- Long-lived credential used to obtain new short-lived access tokens.
How it works, step by step
- 1Issuer creates claims
sub, iss, aud, exp (for example 15 minutes), scopes.
- 2Issuer signs
signature = sign(base64url(header) + '.' + base64url(payload), privateKey).
- 3Client sends the token
Authorization: Bearer <jwt>.
- 4Service verifies
Check the signature with the public key (by kid), then iss, aud, exp, and the allowed algorithm.
- 5Refresh
When the access token expires, exchange the refresh token for a new one; rotate the refresh token.
STEP 1Header: {"alg":"RS256","kid":"2026-09"} base64url encoded. It tells verifiers which key and algorithm to use.
Sessions vs JWT access tokens
Both authenticate requests after login
| Aspect | Server session + cookie | JWT access token |
|---|---|---|
| State | Stored server-side (Redis) | Self-contained in the token |
| Verification cost | Store lookup | Signature check (CPU only) |
| Revocation | Delete session: instant | Wait for expiry or use a deny list |
| Cross-service | Needs shared store | Any service with the public key |
| Best for | Browser web apps | APIs, microservices, mobile |
NOWAspect: State | Server session + cookie: Stored server-side (Redis) | JWT access token: Self-contained in the token
A common hybrid: browser talks to a backend with a session cookie; the backend exchanges it for short-lived JWTs to call internal services.
Implementation
import { SignJWT, jwtVerify, createRemoteJWKSet } from "jose"; // Issuer: sign a short-lived access tokenasync function issueAccessToken(userId: string, privateKey: CryptoKey) { return new SignJWT({ scope: "orders.read orders.write" }) .setProtectedHeader({ alg: "ES256", kid: "2026-09" }) .setSubject(userId) .setIssuer("https://auth.example.com") .setAudience("orders-api") .setIssuedAt() .setExpirationTime("15m") .sign(privateKey);} // Resource server: verify with the issuer's public keys (cached, rotated by kid)const jwks = createRemoteJWKSet(new URL("https://auth.example.com/.well-known/jwks.json")); async function authenticate(authorization?: string) { const token = authorization?.replace(/^Bearer /, ""); if (!token) throw new HttpError(401, "Missing token"); const { payload } = await jwtVerify(token, jwks, { issuer: "https://auth.example.com", audience: "orders-api", algorithms: ["ES256"], // never accept "none" or unexpected algorithms }); return { userId: payload.sub!, scopes: String(payload.scope).split(" ") };}Complexity and performance
Signature check, no I/O (JWKS cached).
Sent on every request; keep claims small.
Limits exposure.
Trade-offs
No lookup per request, but logout and account suspension take effect only at expiry unless you add a deny list (which reintroduces state).
HS256 is simpler but every verifier can also forge tokens; asymmetric signatures let services verify without being able to sign.
Variants and related techniques
Encrypted JWTs when claims must be confidential.
An alternative token format designed to avoid JWT's algorithm confusion issues.
Each refresh returns a new refresh token; reuse of an old one signals theft and revokes the chain.
Common mistakes
- Storing JWTs in localStorage.
Fix: Any XSS can steal them. Prefer HttpOnly Secure cookies for browsers, or keep tokens in memory.
- Long-lived access tokens.
Fix: Keep them short and use refresh tokens.
- Not checking aud and iss.
Fix: Otherwise a token issued for another service is accepted.
- Putting sensitive data in claims.
Fix: Payloads are only base64 encoded; anyone can read them.
Interview questions
How do you log out a user with JWTs?
Delete the refresh token server-side so no new access tokens can be issued, keep access tokens short-lived, and for immediate revocation maintain a small deny list of token IDs (jti) until they expire.
Why use asymmetric signing for JWTs in microservices?
Only the auth service holds the private key and can issue tokens. Other services verify with the public key and cannot forge tokens, limiting damage if one service is compromised.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Decode a JWT and explain every claim | Easy | Structure. |
| Design access + refresh token rotation for a mobile app | Medium | Lifecycle and theft detection. |
| Design revocation for 50M active tokens | Hard | Deny lists and TTLs. |