Passport.js with TypeScript — Authentication Strategies Guide 2026

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Introduction

Why Passport.js

Passport.js is the most widely adopted authentication middleware for Node.js with over 500 strategies for local auth, JWT, and every major social provider. It integrates cleanly with Express via middleware and handles the plumbing of strategy selection, credential verification, and session serialization.

Use Passport when you need multiple auth strategies in one app (e.g., local + Google + JWT) without gluing them together manually.

Installation

npm install passport passport-local passport-jwt passport-google-oauth20 express-session
npm install --save-dev @types/passport @types/passport-local @types/passport-jwt @types/passport-google-oauth20 @types/express-session

Local Strategy (Username + Password)

import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import bcrypt from 'bcrypt';
 
passport.use(
  new LocalStrategy(
    { usernameField: 'email', passwordField: 'password' },
    async (email, password, done) => {
      try {
        const user = await db.users.findOne({ where: { email } });
        if (!user) return done(null, false, { message: 'User not found' });
 
        const valid = await bcrypt.compare(password, user.passwordHash);
        if (!valid) return done(null, false, { message: 'Incorrect password' });
 
        return done(null, user);
      } catch (err) {
        return done(err);
      }
    }
  )
);
 
// Session serialization
passport.serializeUser((user: any, done) => done(null, user.id));
passport.deserializeUser(async (id: number, done) => {
  try {
    const user = await db.users.findById(id);
    done(null, user);
  } catch (err) {
    done(err);
  }
});

JWT Strategy (Stateless APIs)

import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
 
passport.use(
  new JwtStrategy(
    {
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey:    process.env.JWT_SECRET!,
      algorithms:     ['HS256'],
    },
    async (payload: { userId: number; email: string }, done) => {
      try {
        const user = await db.users.findById(payload.userId);
        if (!user) return done(null, false);
        return done(null, user);
      } catch (err) {
        return done(err);
      }
    }
  )
);
 
// Reusable middleware
export const jwtAuth = passport.authenticate('jwt', { session: false });
 
// Usage
router.get('/profile', jwtAuth, (req, res) => {
  res.json({ user: req.user });
});

Google OAuth Strategy

import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
 
passport.use(
  new GoogleStrategy(
    {
      clientID:     process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      callbackURL:  '/auth/google/callback',
      scope:        ['openid', 'email', 'profile'],
    },
    async (accessToken, refreshToken, profile, done) => {
      try {
        const email = profile.emails?.[0]?.value;
        if (!email) return done(new Error('No email from Google'));
 
        const user = await db.users.upsert({
          where:  { googleId: profile.id },
          create: { googleId: profile.id, email, name: profile.displayName },
          update: { name: profile.displayName },
        });
        return done(null, user);
      } catch (err) {
        return done(err as Error);
      }
    }
  )
);
 
// Routes
router.get('/auth/google',
  passport.authenticate('google', { scope: ['openid', 'email', 'profile'] })
);
 
router.get('/auth/google/callback',
  passport.authenticate('google', { session: false, failureRedirect: '/login?error=1' }),
  (req, res) => {
    const user = req.user as any;
    const token = generateJWT({ userId: user.id, email: user.email });
    res.redirect(`/dashboard?token=${token}`);
  }
);

Express App Integration

import express from 'express';
import session from 'express-session';
import passport from 'passport';
 
const app = express();
app.use(express.json());
 
// Sessions (for local strategy)
app.use(session({
  secret:            process.env.SESSION_SECRET!,
  resave:            false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure:   process.env.NODE_ENV === 'production',
    maxAge:   24 * 60 * 60 * 1000,
  },
}));
 
app.use(passport.initialize());
app.use(passport.session());
 
// Login route
app.post('/login',
  passport.authenticate('local', { session: true, failureMessage: true }),
  (req, res) => res.json({ user: req.user })
);
 
// Logout
app.post('/logout', (req, res, next) => {
  req.logout((err) => {
    if (err) return next(err);
    req.session.destroy(() => res.json({ ok: true }));
  });
});

Custom Strategy

import { Strategy } from 'passport-strategy';
import { Request } from 'express';
 
class ApiKeyStrategy extends Strategy {
  name = 'api-key';
 
  authenticate(req: Request) {
    const key = req.headers['x-api-key'] as string;
    if (!key) return this.fail({ message: 'Missing API key' }, 401);
 
    db.apiKeys.findOne({ where: { key, active: true } })
      .then(record => {
        if (!record) return this.fail({ message: 'Invalid API key' }, 401);
        this.success(record.user);
      })
      .catch(err => this.error(err));
  }
}
 
passport.use(new ApiKeyStrategy());
export const apiKeyAuth = passport.authenticate('api-key', { session: false });

TypeScript Request Augmentation

// types/express.d.ts
import { User } from '../models/User';
 
declare global {
  namespace Express {
    interface User {
      id: number;
      email: string;
      role: string;
    }
  }
}
// Now req.user is typed throughout your app

Common Mistakes

  • Forgetting { session: false } in JWT strategy — Passport tries to serialize to session if omitted
  • Not augmenting Express.User — leaves req.user as unknown, defeating TypeScript benefits
  • Calling passport.initialize() before routes but after passport.session() — order matters: initialize first, then session
  • Returning done(null, false) without a message in the verify callback — errors are silent without messages
  • Using passport.authenticate without error handling on the callback — unhandled 401s can crash middleware chains

Best Practices

  • Use { session: false } for all API routes and JWT-based auth — sessions are for web apps only
  • Augment Express.Request with proper User type to get full TypeScript safety on req.user
  • Separate strategy configuration into individual files — one file per strategy for maintainability
  • Test each strategy in isolation with mock users before integrating with the database
  • Use failureMessage: true in session-based strategies to surface authentication errors in req.session.messages

Key Takeaways

  • Passport.js supports 500+ strategies; you configure each strategy once and use it as Express middleware
  • Local strategy handles traditional email/password login with bcrypt comparison
  • JWT strategy enables stateless API authentication — use ExtractJwt.fromAuthHeaderAsBearerToken()
  • Social strategies (Google, GitHub) upsert users in the database and issue your own session or JWT
  • Always augment Express.User in TypeScript to get typed req.user across the application
  • Session-based auth requires express-session and serialization; JWT-based auth does not need sessions
  • Custom strategies extend passport-strategy base class and call this.success, this.fail, or this.error
  • Separate passport.initialize() (always) and passport.session() (only for session-based auth)

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading