Passport is authentication middleware for Node.js. It is designed to serve a unique purpose: authenticate requests. It delegates all other details (user handling, sessions) to the application.
When to Use
- Node.js Express Authentication Middleware: Integrating authentication into Node.js Express, Koa, or NestJS backend services.
- Pluggable Multi-Strategy Authentication: Combining Local username/password, JWT bearer tokens, and OAuth2 strategies under one interface.
- Established Express Monoliths: Adding authentication to existing enterprise Express codebases with mature session infrastructures.
- Custom Authentication Protocols: Authoring tailored authentication strategies using Passport's clean middleware contract.
Quick Start
import passport from "passport";
import LocalStrategy from "passport-local";
// Configure Strategy passport.use( new LocalStrategy(async (username, password, done) => { const user = await User.findOne({ username }); if (!user) return done(null, false); if (!user.verifyPassword(password)) return done(null, false); return done(null, user); }), );
// Middleware in Route app.post( "/login", passport.authenticate("local", { successRedirect: "/", failureRedirect: "/login", }), );
## Core Concepts
### Pluggable Strategy Architecture
Passport delegates credential verification to specialized Strategy plugins (`passport-local`, `passport-jwt`, `passport-google-oauth20`):
```typescript
import passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import bcrypt from "bcrypt";
passport.use(
new LocalStrategy(
{ usernameField: "email" },
async (email, password, done) => {
try {
const user = await db.findUserByEmail(email);
if (!user) return done(null, false, { message: "Invalid credentials" });
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid)
return done(null, false, { message: "Invalid credentials" });
return done(null, user);
} catch (err) {
return done(err);
}
},
),
);
Session Serialization & Deserialization
Coordinates storing minimal user identifiers in session cookies and hydrating full user entities on subsequent requests:
// Serialize: Store only user ID in session store (Redis)
passport.serializeUser((user: any, done) => {
done(null, user.id);
});
// Deserialize: Fetch user record on each incoming request
passport.deserializeUser(async (id: string, done) => {
try {
const user = await db.findUserById(id);
done(null, user);
} catch (err) {
done(err);
}
});
Stateless JWT Strategy for APIs
Validates bearer tokens without maintaining server-side session state:
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET!,
},
async (payload, done) => {
return done(null, { id: payload.sub, role: payload.role });
},
),
);
Common Patterns
Modular JWT Strategy Configuration
Problem: Replicating authentication middleware logic across multiple microservices.
Solution: Configure standard Passport JWT extraction from authorization headers:
import passport from "passport";
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
};
passport.use(
new JwtStrategy(opts, async (jwtPayload, done) => {
try {
const user = await findUserById(jwtPayload.sub);
if (user) return done(null, user);
return done(null, false);
} catch (err) {
return done(err, false);
}
}),
);
Best Practices
Do:
- Store Only the User ID in
serializeUser: Keep session store payloads tiny; fetch updated permissions on deserialization. - Use Stateless JWT Strategy for REST APIs: Avoid heavy cookie-based session stores when servicing stateless mobile and frontend clients.
- Handle Async Errors Gracefully: Always wrap database lookups in try/catch blocks and pass exceptions to
done(err). - Combine with Express Session Stores (Redis): Use
connect-redisto share session state horizontally across Node.js replicas.
Don't:
- Store full user objects in sessions: Outdated permissions or changed passwords won't take effect until sessions expire.
- Use
passport.authenticate('local')without rate limiting: Protect authentication endpoints withexpress-rate-limitagainst brute-force attacks. - Forget to call
done(): Failing to invokedone()leaves HTTP requests hanging until client connection timeouts.
Troubleshooting
| Error | Cause | Solution |
|---|---|---|
TypeError: passport.initialize() is not a function |
Incorrect import syntax or calling initialize before middleware stack is ready. | Use app.use(passport.initialize()) after body parsers. |
Unauthorized: 401 on protected route |
Authorization header missing Bearer prefix or token expired. |
Inspect inbound request header: must be Authorization: Bearer <token>. |
Unknown authentication strategy |
Route attempting to authenticate with a strategy before calling passport.use(). |
Initialize and register all strategy instances before mounting routes. |