Passport Strategies

Passport (0.7.0) is not an authentication system; it is an adapter. Each strategy is a package that knows one way of proving identity — a password (passport-local 1.0.0), a bearer token (passport-jwt 4.0.1), a Google account (passport-google-oauth20 2.0.0) — and all expose the same passport.authenticate('name') middleware, so swapping local login for single sign-on changes one line.

passport-local with sessionsJavaScript
import passport from 'passport';               // npm i passport@0.7.0 passport-local@1.0.0
import { Strategy as LocalStrategy } from 'passport-local';
passport.use(new LocalStrategy({ usernameField: 'email' }, async (email, pw, done) => {
  const user = users.find((u) => u.email === email);
  if (!user || !await argon2.verify(user.hash, pw)) {
    return done(null, false, { message: 'Wrong password' });   // done(err, user, info)
  }
  return done(null, user);
}));
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser((id, done) => done(null, users.find((u) => u.id === id) ?? false));
app.use(session(SESSION_OPTS));                // must come before passport.session()
app.use(passport.initialize());
app.use(passport.session());                   // reads req.session.passport.user
app.post('/login', passport.authenticate('local', { failWithError: true }),
  (req, res) => res.json({ ok: true, user: req.user.email, roles: req.user.roles }),
  (err, req, res, next) => res.status(401).json({ error: err.message }));   // 401 branch

done(null, false) means "not authenticated" while done(err) means "something broke" — mixing them up turns a wrong password into a 500. serializeUser decides what goes in the session, an ID and never the whole user, and deserializeUser runs on every request, so make it a cached or indexed lookup.

Output of 42
$ curl -c jar -d '{"email":"ada@example.com","password":"correct horse battery"}' .../login
{"ok":true,"user":"ada@example.com","roles":["author"]}
$ curl -i -d '{"email":"ada@example.com","password":"hunter2"}' .../login
HTTP/1.1 401 Unauthorized   {"error":"Unauthorized"}

That generic message is failWithError doing its job: the strategy's info.message is deliberately not leaked. When you do want it, use the custom-callback form, which hands you (err, user, info) — then res.status(401).json({ error: info?.message }) answers {"error":"Wrong password"}. Passport earns its keep when you need several strategies at once; for a single local login it is more indirection than argon2.verify plus req.session.userId.