Node.js cookbook · Intermediate
Add JWT authentication to an Express API
Protect Express routes with bearer tokens and expose the authenticated user to handlers.
Approach
Verify tokens with a maintained library (jose) using a pinned algorithm, issuer and audience — ideally against your identity provider's JWKS. Put the verified claims on req (typed via declaration merging) and return 401 with WWW-Authenticate: Bearer on failure. Express 5 forwards rejected promises from async middleware to the error handler automatically.
Solution
import express, { type NextFunction, type Request, type Response } from "express";
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
declare global {
namespace Express {
interface Request { user?: { id: string; roles: string[] } }
}
}
const JWKS = createRemoteJWKSet(new URL("https://auth.example.com/.well-known/jwks.json"));
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization ?? "";
if (!header.startsWith("Bearer ")) {
return res.status(401).set("WWW-Authenticate", "Bearer").json({ title: "Missing token" });
}
try {
const { payload } = await jwtVerify(header.slice(7), JWKS, {
issuer: "https://auth.example.com/",
audience: "orders-api",
algorithms: ["RS256"],
});
req.user = { id: String(payload.sub), roles: (payload as JWTPayload & { roles?: string[] }).roles ?? [] };
next();
} catch {
res.status(401).set("WWW-Authenticate", 'Bearer error="invalid_token"').json({ title: "Invalid token" });
}
}
export const requireRole = (role: string) => (req: Request, res: Response, next: NextFunction) =>
req.user?.roles.includes(role) ? next() : res.status(403).json({ title: "Forbidden" });
const app = express();
app.get("/api/orders", requireAuth, async (req, res) => {
res.json(await orders.forUser(req.user!.id));
});
app.delete("/api/admin/users/:id", requireAuth, requireRole("admin"), async (req, res) => { /* ... */ });
Pitfalls
jwt.decode()instead of verify.- Not pinning algorithms/audience.
- Long-lived tokens without refresh/revocation.
Variations
- Passport.js strategies for many providers.
- Fastify:
@fastify/jwtwithonRequesthooks.
Further reading: jose library
Reference: https://github.com/panva/jose
More Node.js recipes
- Add health and readiness endpoints to a Node.js service
- Cache expensive lookups in Redis from Node.js
- Call external APIs with timeouts and retries in Node.js
- Centralize error handling in Express 5
- Hash passwords and implement login safely in Node.js
- Implement cursor pagination with PostgreSQL in Node.js
- Offload CPU-heavy work with worker threads
- Process background jobs with BullMQ