passport
Passport is Express-compatible authentication middleware for Node.js. Its sole job is authenticating requests, which it does through plug-in packages called strategies: over 480 exist, covering username/password, OAuth providers like Google and Facebook, SAML, OpenID and JWT. It deliberately does not mount routes, manage users, hash passwords or assume a database; you supply verify callbacks and session serialization yourself.
Still everywhere (8M weekly downloads) but effectively in maintenance mode: fine to keep in an existing Express app, hard to recommend for anything new. The strategy ecosystem was the selling point in 2015; in 2026 the same breadth exists in actively developed alternatives with far less wiring.
Use it if
- You maintain an existing Express app that already uses Passport and swapping auth systems is not worth the risk
- You need a niche identity provider whose only maintained Node integration is a Passport strategy
- You have a classic server-rendered Express app with session cookies and want the pattern with fifteen years of Stack Overflow answers behind it
- You are starting a new project: the core repo has had no push since August 2024, one release (0.7.0) since 2022, and about 400 open issues and PRs, while newer options like better-auth ship sign-up, sessions, 2FA and account management out of the box
- You are not in Express-style middleware land (Next.js route handlers, Hono, tRPC, serverless): Passport's req/res mutation model fights those frameworks
- You expect it to handle password hashing, user storage, CSRF or account flows: it does none of that, and every strategy adds its own separately-maintained package of varying health
- Callback-style APIs bother you: the done(err, user) verify pattern predates promises and produces verbose glue code in modern async codebases
Setup reality
npm install passport is just the start: you need one package per strategy (passport-local, passport-google-oauth20, ...), express-session with a production session store, hand-written serializeUser/deserializeUser, and your own verify callback with your own password hashing. Failure modes are famously opaque: forget the session middleware order and you get silent redirect loops with no error. req.logout became async in 0.6.0 and requires a callback, which broke a decade of tutorials that still rank first in search results.
Patterns
Username and password verificationlocal-strategy
const passport = require('passport');
const LocalStrategy = require('passport-local');
passport.use(new LocalStrategy(async (username, password, done) => {
try {
const user = await User.findOne({ username });
if (!user) return done(null, false, { message: 'No user' });
const ok = await bcrypt.compare(password, user.hash);
return ok ? done(null, user) : done(null, false, { message: 'Bad password' });
} catch (err) {
return done(err);
}
}));passport-local is a separate package and does zero hashing; done(null, false) means auth failed, done(err) means server error, mixing them up leaks 500s to users.
Store the user in the sessionsession-serialization
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
done(null, await User.findById(id));
} catch (err) {
done(err);
}
});deserializeUser runs a lookup on every request with a session; cache it or keep it cheap or your database eats one query per page load.
Wire Passport into Expressmiddleware-setup
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: sessionStore,
}));
app.use(passport.initialize());
app.use(passport.session());Order is everything: express-session must come before passport.session(), and the default MemoryStore leaks memory and drops sessions on restart, so use a real store in production.
Authenticate a login POSTlogin-route
app.post('/login',
passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/login',
})
);Needs body parsing middleware (express.urlencoded or express.json) before it, or the strategy silently never sees credentials and you just get the failure redirect.
Guard routes behind loginprotect-route
function ensureAuth(req, res, next) {
if (req.isAuthenticated()) return next();
res.redirect('/login');
}
app.get('/dashboard', ensureAuth, (req, res) => {
res.render('dashboard', { user: req.user });
});There is no built-in guard middleware; everyone writes this same function or pulls in connect-ensure-login.
Log the user outlogout
app.post('/logout', (req, res, next) => {
req.logout((err) => {
if (err) return next(err);
res.redirect('/');
});
});Since 0.6.0 req.logout is async and requires the callback; the old synchronous req.logout() from most tutorials throws.
Stateless API auth with JWTjwt-strategy
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
}, (payload, done) => done(null, { id: payload.sub })));
app.get('/api/me',
passport.authenticate('jwt', { session: false }),
(req, res) => res.json(req.user)
);Pass session: false for token APIs or Passport still tries to serialize into a session that APIs do not have.
Sign in with Googlegoogle-oauth
const GoogleStrategy = require('passport-google-oauth20');
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback',
}, (accessToken, refreshToken, profile, done) => {
User.findOrCreate({ googleId: profile.id }).then(
(user) => done(null, user),
(err) => done(err)
);
}));
app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => res.redirect('/')
);The callbackURL must exactly match what is registered in the Google console, including protocol and port; mismatch errors surface on Google's side, not yours.
Handle auth results yourself (JSON APIs)custom-callback
app.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) return next(err);
if (!user) return res.status(401).json({ error: info.message });
req.logIn(user, (err) => {
if (err) return next(err);
return res.json({ id: user.id });
});
})(req, res, next);
});With a custom callback Passport no longer establishes the session for you; forgetting the manual req.logIn call is the classic bug.
Show why login failedflash-failure-messages
app.post('/login',
passport.authenticate('local', {
failureRedirect: '/login',
failureMessage: true,
})
);
// in the /login handler:
// req.session.messages holds the strategy's failure messagesfailureMessage (0.6+) stores messages in req.session.messages; the older failureFlash option needs the abandoned connect-flash package.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| better-auth | npm | New TypeScript project that wants full auth (email, OAuth, 2FA, orgs) as a framework, not a middleware kit |
| next-auth | npm | Next.js app; Auth.js integrates with route handlers and edge runtimes where Passport does not fit |
| openid-client | npm | You want certified, low-level OpenID Connect flows and are comfortable wiring sessions yourself |