Building a Reusable Passport Strategy for Stateless JWT Authentication
Centralize JWT validation in a reusable Passport strategy to eliminate duplicated auth code across Node.js services. Includes a complete stateless HS256 strategy, registration pattern, and operational checklist for revocation, key rotation, and performance.
16 Jan 2026, 22:51 UTC

The Problem: Duplicated Token Logic Across Services
When a Node.js stack grows past a single service, authentication code tends to sprawl. Each microservice or API gateway ends up with its own JWT verification middleware, often copy-pasted and diverging over time. A change to the token format, a new claim requirement, or a rotation of signing keys becomes a multi-repository refactor.
Passport.js solves this by isolating authentication into strategies—self-contained modules that implement a single `authenticate(req, options)` method. A custom strategy lets you centralize token validation, claim parsing, and user lookup in one place, then reuse it across every Express or Fastify service that mounts Passport.
Why a Custom Strategy Beats Ad-Hoc Middleware
Ad-hoc middleware works for a single app, but it couples token parsing to the HTTP layer. A Passport strategy, by contrast, receives the request object and returns a user instance (or `false` / error) through a callback. This contract makes the strategy testable in isolation, swappable between frameworks, and composable with Passport's built-in features like `session: false` for stateless APIs.
The trade-off is a slightly steeper initial setup: you must implement the `Strategy` constructor, the `authenticate` method, and handle the `done(err, user, info)` callback correctly. Once written, however, the strategy becomes a versioned npm package you can audit, update, and distribute independently.
Worked Example: A Stateless JWT Strategy
Below is a minimal, production-ready strategy that verifies a HS256-signed JWT, extracts the `sub` claim, and fetches the user from a repository. It assumes you already have a `UserRepository` with a `findById(id)` method.
// lib/jwt-strategy.js
const { Strategy } = require('passport-strategy');
const jwt = require('jsonwebtoken');
const { UserRepository } = require('../repositories');
class JwtStrategy extends Strategy {
constructor(options, verify) {
super();
this.name = 'jwt';
this.options = options || {};
this.verify = verify || this.defaultVerify;
}
authenticate(req, options) {
const token = this.extractToken(req);
if (!token) return this.fail({ message: 'Missing token' }, 401);
jwt.verify(token, this.options.secretOrKey, this.options.verifyOptions, (err, payload) => {
if (err) return this.fail({ message: 'Invalid token' }, 401);
this.verify(payload, (verifyErr, user, info) => {
if (verifyErr) return this.error(verifyErr);
if (!user) return this.fail(info || { message: 'User not found' }, 401);
this.success(user, info);
});
});
}
extractToken(req) {
const auth = req.headers.authorization;
if (auth && auth.startsWith('Bearer ')) return auth.slice(7);
return req.cookies?.access_token;
}
async defaultVerify(payload, done) {
try {
const user = await UserRepository.findById(payload.sub);
done(null, user || false, user ? undefined : { message: 'User not found' });
} catch (e) {
done(e);
}
}
}
module.exports = JwtStrategy;
Register it once in your shared auth module:
// config/passport.js
const passport = require('passport');
const JwtStrategy = require('../lib/jwt-strategy');
passport.use(new JwtStrategy({
secretOrKey: process.env.JWT_SECRET,
verifyOptions: { algorithms: ['HS256'] }
}));
module.exports = passport;
Any service then imports this config and mounts the middleware:
// services/orders/app.js
const express = require('express');
const passport = require('@myorg/auth-config/passport');
const app = express();
authenticateJwt = passport.authenticate('jwt', { session: false });
app.get('/orders', authenticateJwt, (req, res) => {
// req.user is the hydrated user instance
res.json(req.user.orders);
});
Limitations and Operational Concerns
- Token revocation: Stateless JWTs cannot be invalidated before expiry without an external store (Redis blocklist, database flag). The strategy above does not implement revocation; add a `jti` claim check against a blocklist if you need immediate logout.
- Key rotation: The `secretOrKey` can be a function that selects a key by `kid` header. For RS256, replace `secretOrKey` with a JWKS fetcher (e.g., `jwks-rsa`).
- Performance: `jsonwebtoken.verify` is synchronous and CPU-bound. Under heavy load, consider offloading verification to a worker thread or using a native binding like `node-jose`.
- Input validation: The strategy trusts the `Authorization` header format. Harden `extractToken` against header injection by rejecting malformed values early.
Verification Checklist Before Shipping
- Run
npm list passportand confirm version>=0.7.0(current major as of 2026). - Execute
npm auditto surface any transitive vulnerabilities in `jsonwebtoken` or `passport-strategy`. - Write a unit test that feeds a valid signed token and asserts
req.usermatches the repository fixture. - Add an integration test that sends an expired token and expects a 401 with
{ message: 'Invalid token' }. - Load-test the endpoint with
autocannon -c 100 -d 10 http://localhost:3000/ordersand verify p99 latency stays within your SLA.
Closing: Ship the Strategy as an Internal Package
Extract the strategy, its tests, and the Passport registration into a private npm package (e.g., `@myorg/passport-jwt-strategy`). Version it independently, publish to your registry, and depend on it from every service. When the security team mandates RS256 or a new claim, you update one package, bump the version, and roll out via your normal deployment pipeline—no copy-paste, no drift.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.