Passport.js (Express Authentication)
Overview
Passport.js is the de-facto standard authentication middleware for Express.js. It uses a strategy pattern with 500+ available strategies for different auth mechanisms.
Version: passport@0.7.x, passport-jwt@4.x
Use case: Express.js applications, custom auth flows, pure APIs
Key Benefit: Maximum flexibility, works with any Express app, battle-tested in production.
When to Use This Skill
✅ Use Passport.js when:
-
Building Express.js REST APIs
-
Need JWT-based authentication for APIs
-
Want 500+ auth strategies available
-
Require custom authentication logic
-
Building microservices or pure backend APIs
-
Not using Next.js
❌ Use Auth.js instead when:
-
Building Next.js applications
-
Want minimal configuration
-
Need quick OAuth setup
-
Want serverless/Edge support
Quick Start
Installation
npm install passport passport-jwt jsonwebtoken npm install -D @types/passport @types/passport-jwt @types/jsonwebtoken
JWT Strategy Setup
// src/strategies/jwt.strategy.ts import passport from 'passport'; import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'; import { prisma } from '../lib/prisma';
const options = { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: process.env.JWT_SECRET!, };
passport.use(new JwtStrategy(options, async (payload, done) => { try { const user = await prisma.user.findUnique({ where: { id: payload.sub } });
if (!user) {
return done(null, false);
}
return done(null, user);
} catch (error) { return done(error, false); } }));
export default passport;
Express Integration
Complete Setup
// src/app.ts import express from 'express'; import passport from 'passport'; import jwt from 'jsonwebtoken'; import { hash, verify } from 'argon2'; import './strategies/jwt.strategy'; import { prisma } from './lib/prisma';
const app = express(); app.use(express.json()); app.use(passport.initialize());
// Register app.post('/auth/register', async (req, res) => { const { email, password, name } = req.body;
const hashedPassword = await hash(password); const user = await prisma.user.create({ data: { email, password: hashedPassword, name }, });
res.status(201).json({ id: user.id, email: user.email }); });
// Login app.post('/auth/login', async (req, res) => { const { email, password } = req.body;
const user = await prisma.user.findUnique({ where: { email } }); if (!user || !user.password) { return res.status(401).json({ message: 'Invalid credentials' }); }
const valid = await verify(user.password, password); if (!valid) { return res.status(401).json({ message: 'Invalid credentials' }); }
const accessToken = jwt.sign( { sub: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET!, { expiresIn: '15m' } );
const refreshToken = jwt.sign( { sub: user.id }, process.env.JWT_REFRESH_SECRET!, { expiresIn: '7d' } );
res.json({ accessToken, refreshToken }); });
// Protected route app.get('/api/profile', passport.authenticate('jwt', { session: false }), (req, res) => { res.json(req.user); } );
// Refresh token app.post('/auth/refresh', async (req, res) => { const { refreshToken } = req.body;
try { const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as { sub: string }; const user = await prisma.user.findUnique({ where: { id: payload.sub } });
if (!user) {
return res.status(401).json({ message: 'Invalid token' });
}
const accessToken = jwt.sign(
{ sub: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
res.json({ accessToken });
} catch { res.status(401).json({ message: 'Invalid token' }); } });
app.listen(3000);
Middleware Helpers
Role-Based Authorization
// src/middleware/auth.middleware.ts import { Request, Response, NextFunction } from 'express'; import passport from 'passport';
// Authenticate and attach user export const authenticate = passport.authenticate('jwt', { session: false });
// Check specific role export function requireRole(...roles: string[]) { return (req: Request, res: Response, next: NextFunction) => { const user = req.user as { role: string } | undefined;
if (!user || !roles.includes(user.role)) {
return res.status(403).json({ message: 'Forbidden' });
}
next();
}; }
// Usage: app.get('/admin/users', authenticate, requireRole('admin'), (req, res) => { /* ... */ } );
Optional Authentication
export function optionalAuth(req: Request, res: Response, next: NextFunction) { passport.authenticate('jwt', { session: false }, (err, user) => { if (user) req.user = user; next(); })(req, res, next); }
// Route works with or without auth app.get('/api/posts', optionalAuth, (req, res) => { const userId = (req.user as { id: string })?.id; // Show public posts, or personalized if logged in });
Integration with tRPC (Express)
// src/server/context.ts import { CreateExpressContextOptions } from '@trpc/server/adapters/express'; import { prisma } from '../lib/prisma';
export const createContext = ({ req }: CreateExpressContextOptions) => ({ user: req.user, // Populated by Passport middleware prisma, });
export type Context = ReturnType<typeof createContext>;
// src/server/index.ts import { createExpressMiddleware } from '@trpc/server/adapters/express'; import passport from 'passport';
// Apply Passport to all /trpc routes app.use('/trpc', passport.authenticate('jwt', { session: false, failWithError: false }), createExpressMiddleware({ router: appRouter, createContext, }) );
JWT Token Structure
// Access token payload interface AccessTokenPayload { sub: string; // User ID email: string; role: string; iat: number; // Issued at exp: number; // Expiration }
// Generate tokens function generateTokens(user: User) { const accessToken = jwt.sign( { sub: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET!, { expiresIn: '15m' } );
const refreshToken = jwt.sign( { sub: user.id }, process.env.JWT_REFRESH_SECRET!, { expiresIn: '7d' } );
return { accessToken, refreshToken }; }
Type Definitions
// src/types/express.d.ts import { User } from '@prisma/client';
declare global { namespace Express { interface User { id: string; email: string; role: string; } } }
Environment Variables
JWT_SECRET=your-access-token-secret-min-32-chars JWT_REFRESH_SECRET=your-refresh-token-secret-min-32-chars
Rules
Do ✅
-
Use session: false for stateless JWT auth
-
Hash passwords with argon2 or bcrypt
-
Use short-lived access tokens (15min)
-
Use refresh tokens for long sessions
-
Store refresh tokens securely (httpOnly cookie)
-
Validate JWT on every request
Avoid ❌
-
Storing sensitive data in JWT payload
-
Using long-lived access tokens
-
Storing tokens in localStorage (XSS risk)
-
Using weak secrets
-
Skipping password hashing
Common Strategies
Strategy Package Use Case
JWT passport-jwt
API authentication
Local passport-local
Username/password
OAuth2 passport-oauth2
Generic OAuth
Google passport-google-oauth20
Google login
GitHub passport-github2
GitHub login
Auth.js vs Passport.js Decision Tree
Scenario Recommendation
Next.js App Router Auth.js
Express.js API Passport.js
Quick OAuth setup Auth.js
Custom auth logic Passport.js
Serverless/Edge Auth.js
Multiple strategies Passport.js
Pure JWT API Passport.js
Troubleshooting
"401 on every request": → Check Authorization header format: "Bearer <token>" → Verify JWT_SECRET matches between sign and verify → Check token expiration
"req.user is undefined": → Ensure passport.initialize() is called → Check authenticate middleware is applied → Verify strategy name matches
"CORS issues with auth": → Set credentials: true in CORS config → Include origin whitelist → Allow Authorization header
"Token not refreshing": → Check refresh token secret is different → Verify refresh token hasn't expired → Store refresh token in httpOnly cookie
File Structure
src/ ├── strategies/ │ └── jwt.strategy.ts # JWT strategy config ├── middleware/ │ └── auth.middleware.ts # Auth helpers ├── routes/ │ └── auth.routes.ts # Auth endpoints └── types/ └── express.d.ts # Type extensions
References
-
https://passportjs.org — Official documentation
-
https://www.npmjs.com/package/passport-jwt — JWT strategy
-
http://www.passportjs.org/packages/ — All strategies