Suggest an editImprove this articleRefine the answer for “How to implement JWT authentication in Express.js?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**JWT authentication in Express.js** signs a JSON payload with a secret key and sends the resulting token to the client. Middleware verifies the signature on each request using `jwt.verify`. No session storage needed. ```js const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '15m' }); // In middleware: req.user = jwt.verify(token, SECRET, { algorithms: ['HS256'] }); ``` **Key point:** a missing `expiresIn` creates a token that never expires. Always specify `algorithms` in verify to prevent the `alg: none` attack.Shown above the full answer for quick recall.Answer (EN)Image**JWT authentication in Express.js** uses signed JSON tokens to verify user identity without storing session state on the server. ## Theory ### TL;DR - JWT is like a tamper-evident stamp: the client carries it, the server checks the seal and expiration without any lookup - Core flow: POST /login with credentials, receive a signed token, send it in `Authorization: Bearer <token>` on every request - Use JWT for stateless APIs and horizontal scaling. Use sessions for server-rendered apps with sensitive mutable state - Always set `expiresIn`. Always pass `{ algorithms: ['HS256'] }` to `jwt.verify`. Store secrets in environment variables, never in code ### Quick Example ```js const jwt = require('jsonwebtoken'); const SECRET = process.env.JWT_SECRET; // Issue token on login app.post('/login', async (req, res) => { const user = await User.findOne({ email: req.body.email }); const valid = await bcrypt.compare(req.body.password, user.passwordHash); if (!valid) return res.status(401).json({ error: 'Invalid credentials' }); const token = jwt.sign({ id: user.id, email: user.email }, SECRET, { expiresIn: '15m' }); res.json({ token }); }); // Verify in middleware function authenticate(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ error: 'No token' }); try { req.user = jwt.verify(token, SECRET, { algorithms: ['HS256'] }); next(); } catch { res.status(401).json({ error: 'Invalid or expired token' }); } } ``` `jwt.sign` encodes the payload, signs it with your secret, and embeds the expiry. `jwt.verify` recomputes the signature and throws if anything doesn't match. No database call on either side. ### How JWT Works Internally A token has three dot-separated parts: header (algorithm name), payload (your claims: user id, role, expiry timestamps), and signature. The `jsonwebtoken` library uses Node's `crypto.createHmac` with your secret to produce the HS256 signature. Verification recomputes it and compares. If the payload was changed after signing, the signatures won't match and `verify` throws. The full request cycle: 1. Client POST /auth/login with email and password 2. Server finds the user, `bcrypt.compare` checks the password hash 3. Server calls `jwt.sign({id, email, role}, SECRET, {expiresIn: '15m'})` and returns the token 4. Client stores the token (httpOnly cookie beats localStorage for XSS resistance) 5. Every following request includes `Authorization: Bearer <token>` 6. The `authenticate` middleware calls `jwt.verify`, attaches `req.user`, calls `next()` The server never stores the token. That's the whole design. ### JWT vs Sessions Sessions store user state in memory or Redis, requiring a lookup on every request. JWT encodes the state inside the token and signs it. The server only needs the secret to verify. No shared store, no sticky sessions when you scale out. But tokens can't be revoked before expiry. If a user logs out, the token stays technically valid until it expires. Sessions kill that instantly. So: JWT for stateless APIs, mobile backends, microservices. Sessions for traditional server-rendered apps with sensitive, mutable state. ### When to Use - REST or GraphQL APIs consumed by mobile apps or SPAs - Horizontally scaled services where a shared session store adds complexity - Microservices passing verified user claims between services (RS256 fits better here) - Short-lived access tokens (15m) paired with refresh tokens for longer sessions Skip JWT if you need immediate token revocation without building a Redis blacklist, or if your app is server-rendered with heavy session logic. ### Common Mistakes **No expiration on the token.** `jwt.sign(payload, secret)` without `expiresIn` creates a token that lives forever. Always add `{ expiresIn: '15m' }` for access tokens. **Skipping `algorithms` in verify.** The default behavior allows the `alg: none` attack, where an attacker strips the signature and sets the algorithm header to `none`. A misconfigured server accepts it as valid. Fix: ```js // Wrong: algorithm confusion vulnerability jwt.verify(token, secret); // Right jwt.verify(token, secret, { algorithms: ['HS256'] }); ``` **Storing the token in localStorage.** XSS can steal it. Use httpOnly cookies: ```js res.cookie('token', token, { httpOnly: true, secure: true, sameSite: 'strict' }); ``` **Weak secrets.** `'shh'` or `'secret'` can be brute-forced offline if a token leaks. Generate a proper one: ```bash node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" ``` **No clock skew tolerance in distributed systems.** Server clock drift rejects valid tokens. Add `{ clockTolerance: 30 }` to verify options. ### Real-World Usage - MERN stacks: `passport-jwt` strategy wraps the verify step in Passport's convention - Next.js: `next-auth` handles JWT sessions via OAuth providers - NestJS: `@nestjs/jwt` module with `AuthGuard` decorators - Supabase and Auth0: both issue JWTs you verify in Express using their public keys (RS256) - Strapi: built-in JWT plugin for headless CMS APIs I've seen teams skip the `algorithms` option for years until a security audit caught it. It's one line and it matters. ### Follow-up Questions **Q:** Walk through the full login-to-protected-request flow. **A:** Client POST /login with credentials. Server bcrypt.compare the hash. On success, jwt.sign({id: user.id, email}) returns a Bearer token. Client attaches it to every Authorization header. Middleware jwt.verify decodes it, attaches req.user, calls next(). **Q:** How do you handle token revocation with stateless JWT? **A:** You can't revoke a stateless token directly. Short expiry (15m) limits the damage window. For hard revocation, embed a `jti` UUID in each token and check a Redis blacklist on every verify. On logout, write that jti to Redis with TTL equal to the token's remaining lifetime. **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, a public key verifies. RS256 works better for microservices because other services can verify tokens without ever knowing the private key. **Q:** What is the `alg: none` attack? **A:** An attacker strips the signature and sets the algorithm header to `none`. A misconfigured server accepts the token as valid without checking any signature. Fix: always pass `{ algorithms: ['HS256'] }` to jwt.verify. **Q:** How do you implement refresh token rotation securely? **A:** Issue a new refresh token on each use and blacklist the old one by its `jti` UUID in Redis. If the blacklisted token appears again, that signals a replay attack: invalidate all sessions for that user immediately. Store only the token hash in the database, not the raw value. ## Examples ### Basic Login and Protected Route ```js const express = require('express'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const app = express(); app.use(express.json()); const SECRET = process.env.JWT_SECRET; const users = [{ id: 1, email: 'alice@example.com', passwordHash: '$2a$10$...' }]; app.post('/auth/login', async (req, res) => { const { email, password } = req.body; const user = users.find(u => u.email === email); if (!user || !await bcrypt.compare(password, user.passwordHash)) { return res.status(401).json({ error: 'Invalid credentials' }); } // Token expires in 15 minutes const token = jwt.sign({ id: user.id, email }, SECRET, { expiresIn: '15m' }); res.json({ token }); }); function authenticate(req, res, next) { const header = req.headers.authorization; if (!header?.startsWith('Bearer ')) return res.status(401).json({ error: 'No token' }); try { // Explicit algorithm prevents alg:none attack req.user = jwt.verify(header.split(' ')[1], SECRET, { algorithms: ['HS256'] }); next(); } catch { res.status(401).json({ error: 'Invalid or expired token' }); } } app.get('/profile', authenticate, (req, res) => { // req.user = { id: 1, email: 'alice@example.com', iat: 1700000000, exp: 1700000900 } res.json({ user: req.user }); }); ``` Credentials are checked with bcrypt, the token is signed with a 15-minute expiry, and `authenticate` uses the `algorithms` option. Any protected route just adds `authenticate` as middleware. ### Role-Based Authorization ```js function authorize(...roles) { return (req, res, next) => { if (!roles.includes(req.user.role)) { return res.status(403).json({ error: 'Forbidden' }); } next(); }; } // Include role in the token payload at login const token = jwt.sign( { id: user.id, email: user.email, role: user.role }, // 'admin' | 'user' SECRET, { expiresIn: '15m' } ); // Only admins reach this handler app.delete('/users/:id', authenticate, authorize('admin'), async (req, res) => { await User.deleteById(req.params.id); res.status(204).send(); }); ``` `authorize` runs after `authenticate`, so `req.user` is already populated. The role comes from the JWT payload, not a separate database query. That's fast. But if a user's role changes in the database, the old token still carries the old role until it expires. Short expiry times matter here too. ### Refresh Token Pattern with httpOnly Cookie ```js const REFRESH_SECRET = process.env.REFRESH_SECRET; app.post('/auth/refresh', (req, res) => { const refreshToken = req.cookies.refreshToken; if (!refreshToken) return res.status(401).json({ error: 'No refresh token' }); try { const decoded = jwt.verify(refreshToken, REFRESH_SECRET, { algorithms: ['HS256'] }); // New short-lived access token const accessToken = jwt.sign( { id: decoded.id, email: decoded.email }, SECRET, { expiresIn: '15m' } ); // Rotate: issue a new refresh token, old one is discarded const newRefresh = jwt.sign( { id: decoded.id, email: decoded.email }, REFRESH_SECRET, { expiresIn: '7d' } ); res.cookie('refreshToken', newRefresh, { httpOnly: true, // not readable by JavaScript secure: true, // HTTPS only sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days in ms }); res.json({ accessToken }); } catch { res.status(403).json({ error: 'Invalid refresh token' }); } }); ``` Access token lives 15 minutes and goes in the response body. Refresh token lives 7 days in an httpOnly cookie. XSS can't read the cookie. CSRF can't use the access token because it's not in a cookie. Rotation on every refresh call limits the window if a refresh token is stolen.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.