Suggest an editImprove this articleRefine the answer for “What is JWT and how does it work?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**JWT (JSON Web Token)** is a compact, signed token that carries user claims as JSON, letting servers verify identity without a database lookup. ```javascript const token = jwt.sign({ userId: 1 }, secret, { expiresIn: '15m' }); jwt.verify(token, secret); // { userId: 1, iat: ..., exp: ... } ``` **Key point:** JWT is signed, not encrypted. Anyone can decode the payload, but only the holder of the secret can produce a valid signature.Shown above the full answer for quick recall.Answer (EN)Image**JWT (JSON Web Token)** is a compact, self-contained token that encodes user claims in JSON, signs them cryptographically, and lets servers verify identity without querying a database. ## Theory ### TL;DR - JWT works like a sealed passport: the data is visible, but tampering breaks the cryptographic seal - Stateless authentication means no DB lookup on each request, unlike session-based auth - Three Base64Url-encoded parts joined by dots: `header.payload.signature` - The payload is readable by anyone. JWT is signed, not encrypted - Use JWT for APIs and microservices; use sessions when you need instant revocation ### Quick Example ```javascript const jwt = require('jsonwebtoken'); // Sign a token at login const token = jwt.sign( { userId: 123, role: 'admin' }, process.env.JWT_SECRET, { expiresIn: '15m' } ); // eyJhbGciOiJIUzI1NiJ9... // Verify on every protected request jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => { if (err) return res.status(401).json({ error: 'Unauthorized' }); // decoded = { userId: 123, role: 'admin', iat: ..., exp: ... } }); ``` `sign` creates the token. `verify` recomputes the signature and compares it byte-for-byte against the third segment. Change anything in the payload, and the signatures no longer match. ### JWT Structure A JWT looks like this: `eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyM30.SflKxwRJSMeKKF2QT4fw` Three parts, two dots. Each part is Base64Url-encoded, a URL-safe variant of Base64 that drops `+` and `=` characters so the token survives inside HTTP headers without corruption. **Header** declares the signing algorithm: ```json { "alg": "HS256", "typ": "JWT" } ``` **Payload** carries claims about the user: ```json { "sub": "123", "role": "admin", "exp": 1716239022, "iat": 1716235422 } ``` Standard claims: `sub` (user ID), `iss` (issuer), `aud` (audience), `exp` (expiration time), `iat` (issued at), `nbf` (not valid before). You can add any custom fields alongside them. **Signature** is the security layer: ``` HMACSHA256(base64Url(header) + "." + base64Url(payload), secret) ``` The server recomputes this on every request. A match means the token is valid and unmodified. A mismatch means tampering. ### How the Signature Works When you call `jwt.sign()`, Node's crypto module runs HMAC-SHA256 over the encoded header and payload using your secret key. The result becomes the third segment of the token string. On `jwt.verify()`, the same HMAC computation runs again over the received `header.payload`. The library compares the result against the token's third segment using constant-time comparison, which prevents timing attacks. If the `exp` claim is past the current Unix time, verification fails regardless of signature validity. This is why you can paste any JWT into jwt.io and read the payload. The signature does not hide data. It only proves the data has not been altered since the server issued the token. ### Key Difference from Sessions With sessions, the server keeps state: user ID, roles, and related data live in memory or a database, and every request triggers a lookup. With JWT, the server stores nothing. The token carries all the data, and verification is a pure crypto computation. This scales well horizontally since any server instance can verify any token using the shared secret. But the trade-off is revocation. A token stays valid until `exp`, even after logout. Sessions let you delete that state immediately. I've seen teams build token blacklists in server memory for logout, then lose the entire list on every deployment restart. Short expiry plus server-side refresh tokens is the only approach that holds up in production. ### When to Use - API-only backend or microservices: JWT (stateless, any instance verifies) - Single server with a session store: cookies and sessions (simpler revocation) - Mobile apps or SPAs: JWT (easy to attach to HTTP headers) - OAuth2 and OIDC flows: JWT (standardized claims, used by Auth0, Okta, AWS Cognito) - Long sessions with revocation needs: short JWT access tokens plus server-side refresh tokens ### Common Mistakes **Putting sensitive data in the payload.** The payload is just Base64. Anyone can paste the token into jwt.io and read every field. Store only an opaque user ID and roles. Fetch sensitive data from the database after verification. Wrong: ```javascript jwt.sign({ sub: 'user', creditCard: '4111...' }, secret); ``` Fix: ```javascript jwt.sign({ sub: 'user123', role: 'admin' }, secret, { expiresIn: '15m' }); ``` **No expiration or a weak secret.** ```javascript // Wrong: token lives forever, secret is brute-forceable jwt.sign(payload, 'secret'); // Correct: short expiry, 256-bit random secret jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m' }); // Generate with: crypto.randomBytes(32).toString('hex') ``` **Missing algorithm restriction.** The JWT spec allows `alg: none`, which removes the signature entirely. An attacker strips the signature, modifies the payload, and sets `alg` to `none`. ```javascript // Wrong: accepts any algorithm, including "none" jwt.verify(token, secret); // Correct: explicit whitelist jwt.verify(token, secret, { algorithms: ['HS256'] }); ``` **Storing JWT in localStorage.** Any XSS script can read `localStorage` and steal the token. Use `httpOnly` cookies, or keep tokens short-lived with a refresh flow. **Assuming JWT is revocable without extra infrastructure.** A short `expiresIn` plus refresh tokens stored in Redis is the right model. The access token expires fast. The refresh token can be deleted on logout or user ban. ### Real-world Usage - Express + `passport-jwt`: extracts and verifies `Authorization: Bearer <token>` in middleware - React/Next.js: `fetch` or `axios` attaches the token to every API request header - Auth0, Okta, AWS Cognito: issue JWTs after login, verified via a JWKS endpoint - GraphQL + Apollo: `context` function extracts and decodes JWT for resolvers - HS256 vs RS256: HS256 is symmetric (shared secret, faster); RS256 is asymmetric (private key signs, public key verifies, better for multi-service setups) ### Follow-up Questions **Q:** Walk through a JWT from login to API call. **A:** User submits credentials, server signs a payload with the secret and returns the token. Client attaches it as `Authorization: Bearer <token>` on every subsequent request. Middleware verifies the signature and extracts claims for authorization logic. **Q:** How do you handle JWT revocation? **A:** Short access token expiry (15 minutes) combined with server-side refresh tokens in Redis. On logout, delete the refresh token. For extra coverage, include a `jti` claim and check a blacklist on sensitive operations. **Q:** What is the difference between HS256 and RS256? **A:** HS256 is symmetric: the same secret signs and verifies. RS256 is asymmetric: a private key signs, and any service with the public key can verify. RS256 fits multi-service architectures where sharing a secret across services is a security risk. **Q:** Why Base64Url and not regular Base64? **A:** Standard Base64 uses `+`, `/`, and `=` which corrupt HTTP headers and URLs. Base64Url swaps them for URL-safe alternatives, so the token travels cleanly as a header value or query param. **Q (Senior):** Design zero-downtime JWT revocation for 1 million users. **A:** Short access tokens (5 minutes) plus 24-hour refresh tokens in a Redis cluster with TTL matching the refresh expiry. Rotate refresh tokens on every use: old one deleted, new pair issued. For key rotation, serve public keys via a JWKS endpoint with a short cache TTL on consuming services. Monitor signature verification latency as Redis scales under load. ## Examples ### Login and Protected Route ```javascript const express = require('express'); const jwt = require('jsonwebtoken'); const app = express(); app.post('/login', express.json(), (req, res) => { // Replace with a real database check if (req.body.email !== 'user@example.com' || req.body.password !== 'pass') { return res.status(401).json({ error: 'Invalid credentials' }); } const token = jwt.sign( { id: 1, email: req.body.email }, process.env.JWT_SECRET, { expiresIn: '15m' } ); res.json({ token }); }); app.get('/profile', (req, res) => { const token = req.headers.authorization?.split(' ')[1]; // Bearer <token> try { const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }); res.json({ user: decoded }); } catch (err) { res.status(401).json({ error: 'Unauthorized' }); } }); ``` `/login` returns `{ token: "eyJ..." }`. The client stores it and sends `Authorization: Bearer eyJ...` on every request. `/profile` verifies the signature, then returns the decoded user object. Note the explicit `algorithms` option, it's the fix for the `alg: none` attack. ### Refresh Token Rotation When access tokens expire, clients need a way to get new ones without forcing the user to log in again. Refresh token rotation handles this and limits the damage from token theft. ```javascript const refreshTokens = new Map(); // Use Redis in production app.post('/refresh', express.json(), (req, res) => { const { refreshToken } = req.body; if (!refreshTokens.has(refreshToken)) { return res.status(403).json({ error: 'Invalid refresh token' }); } try { const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET); // Rotate: delete the old token, issue a new pair refreshTokens.delete(refreshToken); const newAccess = jwt.sign( { id: decoded.id }, process.env.JWT_SECRET, { expiresIn: '15m' } ); const newRefresh = jwt.sign( { id: decoded.id }, process.env.REFRESH_SECRET, { expiresIn: '7d' } ); refreshTokens.set(newRefresh, true); res.json({ accessToken: newAccess, refreshToken: newRefresh }); } catch { res.status(403).json({ error: 'Refresh failed' }); } }); ``` If an attacker steals a refresh token, they can use it once. But the next time the legitimate user rotates, the stolen token is deleted from the store and fails on the next attempt. This pattern comes directly from Auth0 and Okta's recommended flows.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.