Passport‑JWT: Build a Stateless Auth Layer in Express Without Sessions
Learn how to set up Passport‑JWT in Express for stateless authentication. The guide covers strategy configuration, token extraction, expiration handling, common pitfalls, and a practical checklist to ensure a secure, scalable API.
29 Jul 2025, 20:54 UTC

Why Stateless JWTs with Passport?
When you expose an API that needs to scale across multiple instances, storing session data in memory or a shared store can become a bottleneck. Passport’s passport-jwt strategy solves this by treating the JWT as the sole source of truth: the token is signed, sent by the client, and verified on every request. If the token is valid and not expired, the request is authorized. This approach eliminates server‑side session storage and simplifies horizontal scaling.
Setting Up the Strategy
Below is a minimal Express app that demonstrates how to wire up the JWT strategy. All code snippets assume you have a Node.js environment (v18+) and the following packages installed:
expresspassportpassport-jwtjsonwebtoken
# npm install express passport passport-jwt jsonwebtoken
Now create app.js:
const express = require('express');
const passport = require('passport');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const jwt = require('jsonwebtoken');
const app = express();
// 1. Configure the JWT strategy
const jwtOptions = {
// Extract JWT from the Authorization header using the Bearer scheme
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// Use a strong, unpredictable secret. In production store this in a secure vault.
secretOrKey: process.env.JWT_SECRET || 'REPLACE_WITH_RANDOM_STRING',
// Tell Passport not to create a session (stateless)
session: false,
};
passport.use(
new JwtStrategy(jwtOptions, (payload, done) => {
// payload is the decoded JWT payload
// Map it to a user object or reject the request
if (!payload || !payload.id) {
return done(null, false); // No user ID found → unauthorized
}
// In a real app, you might look up the user in a DB here
const user = { id: payload.id, name: payload.name || 'Anonymous' };
return done(null, user);
})
);
app.use(passport.initialize());
// 2. Public route – no auth required
app.get('/public', (req, res) => {
res.json({ message: 'This is public data' });
});
// 3. Protected route – requires a valid JWT
app.get(
'/protected',
passport.authenticate('jwt', { session: false }),
(req, res) => {
// If authentication succeeded, req.user is set by the strategy
res.json({ message: 'Protected data', user: req.user });
}
);
// 4. Helper to generate a token (for demo purposes only)
app.get('/login', (req, res) => {
const token = jwt.sign({ id: 123, name: 'Alice' }, jwtOptions.secretOrKey, {
expiresIn: '1h',
});
res.json({ token });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server listening on port ${PORT}`));
Run the server with node app.js. The /login endpoint is a convenience for generating a token; in production you would issue a token after verifying credentials.
Testing the Flow
- Request
GET /loginto obtain a token. - Make a request to
/protectedwith the header:Authorization: Bearer <token> - If the token is valid and not expired, you receive a 200 response containing the user payload. If the token is missing, malformed, or expired, Passport returns a 401 Unauthorized.
Token Extraction Pitfalls
The jwtFromRequest option determines where Passport looks for the token. Common mistakes include:
- Using
ExtractJwt.fromAuthHeaderAsBearerToken()when clients send the token in a cookie. The request will silently fail. - Using the wrong header prefix (e.g.,
Tokeninstead ofBearer) and not configuring a custom extractor.
To extract from a cookie named access_token:
jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.fromAuthHeaderAsBearerToken(),
(req) => req.cookies && req.cookies.access_token,
]),
Make sure you have cookie-parser middleware installed and applied before Passport.
Expiration & Revocation
Passport automatically checks the exp claim in the JWT. If the token is expired, the strategy rejects it before the verify callback runs. However, JWTs are inherently immutable; you cannot revoke a single token unless you rotate the signing key or maintain a revocation list. Common patterns:
- Short‑lived access tokens (e.g., 15 min) plus a refresh token stored server‑side.
- Maintain a blacklist of revoked token IDs (the
jticlaim) and check it in the verify callback.
Common Mistakes to Avoid
- Hard‑coded secrets: Use environment variables or a secrets manager. A weak secret lets attackers forge tokens.
- Not setting
session: false: Passport defaults to session mode, which conflicts with stateless design. - Ignoring errors: By default, Passport sends a 401, but you might want a custom error handler. Use
failureMessageor provide your ownfailureRedirectif needed. - Exposing the signing secret in client‑side code or logs.
Limitations & When to Use Sessions
Passport‑JWT is ideal for pure APIs or microservices where each request is independent. If you need server‑side session features (e.g., CSRF protection, server‑side logout, or storing additional session data), consider using passport-local with express‑session or a hybrid approach: keep JWT for API access and a session cookie for web flows.
Practical Checklist
| Item | Check |
|---|---|
| Secret is at least 256 bits | ✓ |
| JWT is sent via Authorization: Bearer or secure cookie | ✓ |
Strategy configured with session: false | ✓ |
| Token expiration enforced (exp claim) | ✓ |
| Error handling customized if needed | ✓ |
Conclusion
Passport‑JWT lets you build a lightweight, stateless authentication layer that scales effortlessly. By carefully configuring the extractor, using a strong secret, and understanding the limits of token revocation, you can secure your API while keeping the server stateless.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.