Express 5 + Passport JWT strategy: working versions (tested 2026-10-15)
Protect Express 5 routes with passport-jwt (bearer tokens signed by jsonwebtoken), stateless sessions and role checks.
passed3 passed · 0 failed2026-10-15 11:53 UTC- Status
- passed
- Last passed
- 2026-10-15 11:53 UTC
- Tests
- 3 / 3
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38730348195
The code
Exactly the files that passed. Your agent can pull them with get_recipe("express-passport-jwt") (install), or see them on GitHub.
{
"name": "express-passport-jwt",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"express": "5.3.0",
"passport": "0.7.0",
"passport-jwt": "4.0.1",
"jsonwebtoken": "9.0.3"
},
"devDependencies": {
"tsx": "4.23.15",
"@types/node": "24.19.2",
"supertest": "7.3.1",
"@types/supertest": "7.2.1",
"@types/express": "5.0.6",
"@types/passport": "1.0.17",
"@types/passport-jwt": "4.0.1",
"@types/jsonwebtoken": "9.0.10"
}
}
import express, { type NextFunction, type Request, type Response } from "express";
import passport from "passport";
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
import jwt from "jsonwebtoken";
type User = { id: string; role: "admin" | "user" };
export function createApp(secret: string) {
passport.use(new JwtStrategy(
{ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: secret, algorithms: ["HS256"] },
(payload: { sub: string; role: User["role"] }, done) => done(null, { id: payload.sub, role: payload.role }),
));
const app = express();
app.use(express.json());
app.use(passport.initialize());
const auth = passport.authenticate("jwt", { session: false });
const requireRole = (role: User["role"]) => (req: Request, res: Response, next: NextFunction) =>
(req.user as User).role === role ? next() : res.status(403).json({ error: "forbidden" });
app.post("/login", (req, res) => {
if (req.body?.password !== "secret") return res.status(401).json({ error: "invalid_credentials" });
const role = req.body.username === "root" ? "admin" : "user";
res.json({ token: jwt.sign({ role }, secret, { subject: req.body.username, expiresIn: "1h", algorithm: "HS256" }) });
});
app.get("/me", auth, (req, res) => { res.json(req.user); });
app.get("/admin", auth, requireRole("admin"), (_req, res) => { res.json({ ok: true }); });
return app;
}
import { test } from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import jwt from "jsonwebtoken";
import { createApp } from "../src/app.ts";
const app = createApp("test-secret");
const login = async (username: string) => (await request(app).post("/login").send({ username, password: "secret" })).body.token;
test("token unlocks /me", async () => {
const res = await request(app).get("/me").set("authorization", `Bearer ${await login("ada")}`);
assert.deepEqual(res.body, { id: "ada", role: "user" });
});
test("role check", async () => {
assert.equal((await request(app).get("/admin").set("authorization", `Bearer ${await login("ada")}`)).status, 403);
assert.equal((await request(app).get("/admin").set("authorization", `Bearer ${await login("root")}`)).status, 200);
});
test("rejects missing, forged and expired tokens", async () => {
assert.equal((await request(app).get("/me")).status, 401);
const forged = jwt.sign({ role: "admin" }, "other", { subject: "x" });
assert.equal((await request(app).get("/me").set("authorization", `Bearer ${forged}`)).status, 401);
const expired = jwt.sign({ role: "user", exp: Math.floor(Date.now() / 1000) - 10 }, "test-secret", { subject: "x" });
assert.equal((await request(app).get("/me").set("authorization", `Bearer ${expired}`)).status, 401);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Express 5 + jsonwebtoken auth middleware
Issue HS256 JWTs on login and protect routes with a bearer-token middleware.
Express 5 + express-session login flow, e2e-style over real HTTP with a cookie jar
Session login/logout with express-session (regenerate on login, httpOnly cookies), tested end to end against a real listening server using fetch-cookie, no browser or Playwright.
Express 5 + Helmet + express-rate-limit hardening
Add security headers with Helmet and per-IP rate limiting with express-rate-limit to an Express 5 API.
Express 5 + Multer 2 file uploads with limits
Accept multipart file uploads in Express 5 with Multer memory storage, size limits and a MIME type filter.
All Express recipesAll passport recipesAll passport-jwt recipesAll jsonwebtoken recipes