NodeJSPassport.js

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

Bash
npm install passport passport-local passport-jwt express-session
The Passport model

Text
Route → passport.authenticate('strategy') → Strategy verify callback
                                                    ↓
                                          done(null, user)   → req.user = user → next()
                                          done(null, false)  → 401 / redirect
                                          done(err)          → 500
Passport is glue — it standardises how strategies attach identity to the request
Passport doesn't do the actual verification — that's inside each strategy's **verify callback**, which you write. Passport's job is to call it at the right time, handle the three outcomes (success / fail / error), and — when using sessions — serialize/deserialize the user to/from the session. Think of it as a router for auth: it knows *when* to call your code, not *what* your code does.
Local strategy (email + password)

config/passport.js

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
    }
  },
))
Return the same message for wrong email AND wrong password — no enumeration
The verify callback receives the submitted email and password. Whether the email doesn't exist or the password is wrong, return `done(null, false, { message: 'Invalid credentials' })` — the same message for both cases. Returning "email not found" on an unknown address lets attackers enumerate valid accounts. The [bcrypt compare](/nodejs/password-hashing) call handles the timing-safe comparison; don't add extra branches that change the work done.
Session serialization

JS
// 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)
  }
})
Serialize the id, deserialize by querying — keep the session small and fresh
`serializeUser` decides what goes *into* the session; deserialize pulls a fresh user record *from* the session token on each request. Store only the user **id** — a tiny value — so the session stays small and you always get up-to-date user data (e.g. a newly-suspended account will fail a DB lookup on the next request rather than serving stale data from the session). Never serialize the full user object with roles/permissions into the session itself.
Wiring it up with Express

JS
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

JS
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),
)
Pass `session: false` for JWT routes — the strategy is already stateless
When using the JWT strategy, calling `passport.authenticate('jwt', { session: false })` tells Passport not to serialize the result into a [session](/nodejs/sessions) — the JWT itself carries identity. For API routes authenticated with tokens this is correct: no session cookie is set, and no serialize/deserialize work happens. Omitting it causes Passport to try to serialize the user into the session, which adds unwanted state to a stateless flow.
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

Passport pays off when you need multiple strategies or OAuth providers
For a simple email+password API, hand-rolled [JWT middleware](/nodejs/jwt-implementation) is less ceremonious. Passport's value multiplies when you add a second strategy — Google login, GitHub, SAML — because they all fold into the same `passport.authenticate` call and the same `req.user` contract. The [next page](/nodejs/oauth) covers plugging OAuth providers directly into this model.
Next
Let users log in with Google, GitHub, and more: [OAuth 2.0 & Social Login](/nodejs/oauth).