Passport.js
Every auth mechanism — local username/password, Google OAuth, GitHub, JWT — has a slightly different flow. Passport.js unifies them behind a single middleware model: you configure strategies, call passport.authenticate('strategy-name') on a route, and Passport handles the protocol details. When a strategy succeeds it calls done(null, user); Passport attaches the user to req.user and calls next(). When it fails it returns a 401 (or redirects). This page covers the local strategy for password login, the session integration, and how OAuth strategies plug into the same model.
Install
npm install passport passport-local passport-jwt express-session
The Passport model
Route → passport.authenticate('strategy') → Strategy verify callback
↓
done(null, user) → req.user = user → next()
done(null, false) → 401 / redirect
done(err) → 500Local strategy (email + password)
config/passport.js
import passport from 'passport'
import { Strategy as LocalStrategy } from 'passport-local'
import bcrypt from 'bcrypt'
passport.use(new LocalStrategy(
{ usernameField: 'email' }, // default field name is 'username'
async (email, password, done) => {
try {
const user = await db.users.findByEmail(email)
if (!user) return done(null, false, { message: 'Invalid credentials' })
const ok = await bcrypt.compare(password, user.passwordHash)
if (!ok) return done(null, false, { message: 'Invalid credentials' })
return done(null, user) // success
} catch (err) {
return done(err) // unexpected error → 500
}
},
))Session serialization
// Store only the user id in the session (not the whole user object):
passport.serializeUser((user, done) => done(null, user.id))
// Re-hydrate from the id on each request:
passport.deserializeUser(async (id, done) => {
try {
const user = await db.users.findById(id)
done(null, user)
} catch (err) {
done(err)
}
})Wiring it up with Express
import session from 'express-session'
// Order matters: session → passport.initialize → passport.session
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }))
app.use(passport.initialize())
app.use(passport.session()) // reads the session, calls deserializeUser
// Login route:
app.post('/login', passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/login?error=1',
// Or return JSON:
// failureFlash: false,
}))
// Logout:
app.post('/logout', (req, res) => {
req.logout(() => res.redirect('/login'))
})
// Protect a route with a helper:
function requireAuth(req, res, next) {
if (req.isAuthenticated()) return next()
res.status(401).json({ error: 'Not authenticated' })
}
app.get('/profile', requireAuth, (req, res) => res.json(req.user))JWT strategy
npm install passport-jwt
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
passport.use(new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
algorithms: ['HS256'], // pin the algorithm
},
async (payload, done) => {
try {
const user = await db.users.findById(payload.sub)
if (!user) return done(null, false)
return done(null, user)
} catch (err) {
return done(err)
}
},
))
// Use it stateless (no session needed for JWT routes):
app.get('/api/me', passport.authenticate('jwt', { session: false }), (req, res) =>
res.json(req.user),
)Passport vs hand-rolled auth
Passport | Hand-rolled middleware | |
|---|---|---|
Boilerplate | More setup initially | Minimal for one strategy |
Many strategies | Plug in any passport-* package | Re-implement each flow |
OAuth/social login | Excellent — passport-google, -github, etc. | Significant work |
Testing | Requires mocking passport | Direct unit-testable functions |