Passport.js with TypeScript — Authentication Strategies Guide 2026
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-sessionLocal 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 appCommon Mistakes
- Forgetting
{ session: false }in JWT strategy — Passport tries to serialize to session if omitted - Not augmenting
Express.User— leavesreq.userasunknown, defeating TypeScript benefits - Calling
passport.initialize()before routes but afterpassport.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.authenticatewithout 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.Requestwith proper User type to get full TypeScript safety onreq.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: truein session-based strategies to surface authentication errors inreq.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.Userin TypeScript to get typedreq.useracross the application - Session-based auth requires
express-sessionand serialization; JWT-based auth does not need sessions - Custom strategies extend
passport-strategybase class and callthis.success,this.fail, orthis.error - Separate
passport.initialize()(always) andpassport.session()(only for session-based auth)
Advertisement
Related reading
Clock Skew Breaking Tokens — When Servers Disagree on What Time It Is6 min readNode.js API Best Practices 2026 — Build Production-Ready REST APIs5 min readPrisma ORM Guide 2026 — Type-Safe Database Access with PostgreSQL5 min readAPI Security Guide 2026 — OWASP Top 10, JWT, CORS, and Rate Limiting5 min readAPI-First Development in 2026 — Design, Mock, Validate, Then Build6 min readbetter-auth — The Open-Source Auth Library That Replaces NextAuth6 min read