Passport JWT Strategy
Passport is Express’s most popular authentication middleware, and the passport-jwt strategy adapts it for stateless, token-based APIs. Instead of writing your own header-parsing and jwt.verify middleware by hand, you configure a strategy once — telling it where to find the token and how to turn its payload into a user — and then guard any route with a single call to passport.authenticate('jwt'). This page wires up passport-jwt with ExtractJwt, writes the verify callback that resolves the authenticated user, and secures routes cleanly. It assumes you already issue JWTs on login; if not, start with the JWT authentication page.
How the JWT strategy works
The strategy does three things on every protected request: it extracts the raw token from the request, verifies its signature and expiry against your secret, and then hands the decoded payload to a verify callback you provide. Your callback decides whether the user behind those claims still exists and is allowed in. If it returns a user, Passport attaches it to req.user; if it returns false, the request is rejected with a 401. Because the strategy verifies the signature for you, the token reaching your callback is always cryptographically valid — your job is only to resolve identity.
Installing the packages
npm install passport passport-jwt
passport is the core middleware; passport-jwt provides the Strategy and the ExtractJwt helpers. Keep your signing secret in the environment so it is never committed.
# .env
JWT_SECRET=replace-with-a-long-random-string
Configuring the strategy
Create the strategy with two parts: an options object that controls extraction and verification, and the verify callback. ExtractJwt.fromAuthHeaderAsBearerToken() pulls the token from Authorization: Bearer <token>, and secretOrKey is the same secret you signed with. The callback receives the decoded payload and a done function — call done(null, user) on success, done(null, false) to reject, and done(err) for an unexpected failure.
const passport = require("passport");
const { Strategy: JwtStrategy, ExtractJwt } = require("passport-jwt");
const options = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
algorithms: ["HS256"],
};
passport.use(
new JwtStrategy(options, async (payload, done) => {
try {
const user = await db.users.findById(payload.sub);
if (!user) {
return done(null, false);
}
return done(null, { id: user.id, email: user.email, role: user.role });
} catch (err) {
return done(err, false);
}
})
);
module.exports = passport;
Tip: The verify callback runs on every protected request. Looking the user up by
payload.subhere lets you instantly reject deleted or banned accounts — something a pure stateless check cannot do — at the cost of one lookup per request. If you want full statelessness, skip the database call and trust the claims directly.
Initializing Passport in your app
Require the configured strategy module, then register passport.initialize() as application-level middleware. With the JWT strategy you do not need sessions, so omit passport.session() and set session: false when authenticating — this keeps every request fully stateless.
const express = require("express");
const passport = require("./auth/jwt-strategy"); // module above
const app = express();
app.use(express.json());
app.use(passport.initialize());
Securing routes
Guard a route by inserting passport.authenticate('jwt', { session: false }) as middleware. On success Passport populates req.user from your verify callback and calls the next handler; on failure it short-circuits with a 401 Unauthorized before your handler ever runs.
const router = express.Router();
const authenticate = passport.authenticate("jwt", { session: false });
router.get("/me", authenticate, (req, res) => {
res.json(req.user);
});
app.use("/api", router);
Apply it at the router level to protect an entire prefix at once:
const api = express.Router();
api.use(passport.authenticate("jwt", { session: false }));
api.get("/profile", (req, res) => res.json({ user: req.user }));
api.get("/orders", async (req, res) => {
const orders = await db.orders.findByUser(req.user.id);
res.json(orders);
});
app.use("/api", api);
Output:
$ curl -H "Authorization: Bearer eyJhbGci..." http://localhost:3000/api/me
{"id":"u_123","email":"[email protected]","role":"user"}
$ curl http://localhost:3000/api/me
Unauthorized
Customizing the failure response
By default Passport sends a bare 401 Unauthorized with the text body shown above. For a JSON API you usually want a structured error. Pass a custom callback to authenticate to take full control of both success and failure — note that with a custom callback you must invoke the function it returns with (req, res, next).
function requireJwt(req, res, next) {
passport.authenticate("jwt", { session: false }, (err, user, info) => {
if (err) return next(err);
if (!user) {
const reason = info && info.name === "TokenExpiredError"
? "Token expired"
: "Invalid or missing token";
return res.status(401).json({ error: reason });
}
req.user = user;
next();
})(req, res, next);
}
module.exports = requireJwt;
Output:
$ curl http://localhost:3000/api/me
{"error":"Invalid or missing token"}
Extractor options
ExtractJwt ships several extractors, and you can combine them with fromExtractors([...]), which returns the first token found. Prefer the Authorization header for APIs; cookie extraction is handy for browser apps but requires CSRF protection.
| Extractor | Reads token from |
|---|---|
fromAuthHeaderAsBearerToken() | Authorization: Bearer <token> |
fromAuthHeaderWithScheme(scheme) | Authorization: <scheme> <token> |
fromHeader(name) | A custom header such as x-access-token |
fromBodyField(name) | A field in the JSON/form body |
fromUrlQueryParameter(name) | A query-string parameter |
fromExtractors([...]) | The first match among several extractors |
Best Practices
- Always pass
{ session: false }with the JWT strategy so Passport never tries to serialize a session you don’t use. - Pin
algorithmsin the strategy options to the algorithm you signed with, and never includenone, to block algorithm-confusion attacks. - Resolve the user from
payload.subin the verify callback so deleted or banned accounts are rejected immediately. - Return
done(null, false)for an unknown user and reservedone(err)for genuine errors like a failed database query. - Use a custom
authenticatecallback to emit structured JSON errors and to distinguish expired tokens from invalid ones. - Apply authentication at the router level for protected prefixes instead of repeating it on every route.
- Keep the signing secret in an environment variable and serve every endpoint over HTTPS.