Passport.js is a Node.js authentication middleware — passport.use(new LocalStrategy({ usernameField: "email" }, async (email, password, done) => { ... })) defines a strategy. passport.authenticate("local") is Express middleware that runs the strategy. passport.serializeUser((user, done) => done(null, user.id)) and passport.deserializeUser(async (id, done) => ...) handle session persistence. new JwtStrategy({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey }, verify) validates JWT tokens. new GitHubStrategy({ clientID, clientSecret, callbackURL }, verify) enables GitHub OAuth. req.user is set after successful authentication — augment the Express.User interface for TypeScript. passport.initialize() and passport.session() are Express middleware added before routes. req.isAuthenticated() checks session. req.logout(callback) clears the session. Claude Code generates Passport.js username/password login, JWT API auth, GitHub OAuth flows, and protected route middleware.
CLAUDE.md for Passport.js
## Passport.js Stack
- Version: passport >= 0.7, passport-local >= 1.0, passport-jwt >= 4.0, passport-github2 >= 0.1
- Init: app.use(passport.initialize()); app.use(passport.session()) — session needed for cookie auth
- Local: passport.use(new LocalStrategy({ usernameField: "email" }, async (email, pw, done) => { ... }))
- JWT: passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: JWT_SECRET }, verify))
- Session: passport.serializeUser((user, done) => done(null, user.id)); passport.deserializeUser(async (id, done) => ...)
- TypeScript: declare global { namespace Express { interface User { id: string; role: string } } }
- requireAuth: (req, res, next) => req.isAuthenticated() ? next() : res.status(401).json({ error: "Unauthorized" })
Strategy Configuration
// lib/auth/passport.ts — strategy definitions
import passport from "passport"
import { Strategy as LocalStrategy } from "passport-local"
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt"
import { Strategy as GitHubStrategy } from "passport-github2"
import { db } from "@/lib/db"
import { verifyPassword } from "@/lib/auth/passwords"
import { signAccessToken } from "@/lib/auth/jwt"
// TypeScript augmentation
declare global {
namespace Express {
interface User {
id: string
email: string
name: string
role: "user" | "admin"
avatarUrl: string | null
githubId: string | null
}
}
}
// ── Local Strategy (email + password) ──────────────────────────────────────
passport.use(
new LocalStrategy(
{ usernameField: "email", passwordField: "password" },
async (email, password, done) => {
try {
const user = await db.user.findUnique({
where: { email: email.toLowerCase().trim() },
select: {
id: true,
email: true,
name: true,
role: true,
avatarUrl: true,
githubId: true,
passwordHash: true,
emailVerified: true,
},
})
if (!user) {
return done(null, false, { message: "Invalid email or password" })
}
if (!user.passwordHash) {
return done(null, false, { message: "Account uses social login" })
}
if (!user.emailVerified) {
return done(null, false, { message: "Please verify your email" })
}
const isValid = await verifyPassword(password, user.passwordHash)
if (!isValid) {
return done(null, false, { message: "Invalid email or password" })
}
const { passwordHash, emailVerified, ...safeUser } = user
return done(null, safeUser)
} catch (err) {
return done(err)
}
},
),
)
// ── JWT Strategy (Authorization: Bearer <token>) ───────────────────────────
passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET!,
algorithms: ["HS256"],
},
async (payload: { userId: string }, done) => {
try {
const user = await db.user.findUnique({
where: { id: payload.userId },
select: {
id: true,
email: true,
name: true,
role: true,
avatarUrl: true,
githubId: true,
},
})
if (!user) return done(null, false)
return done(null, user)
} catch (err) {
return done(err)
}
},
),
)
// ── GitHub OAuth Strategy ──────────────────────────────────────────────────
passport.use(
new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
callbackURL: `${process.env.APP_URL}/auth/github/callback`,
scope: ["user:email"],
},
async (accessToken, refreshToken, profile, done) => {
try {
const email = profile.emails?.[0]?.value
if (!email) {
return done(null, false, { message: "No email from GitHub" })
}
// Upsert user — link GitHub account
const user = await db.user.upsert({
where: { githubId: profile.id },
create: {
email,
name: profile.displayName ?? profile.username ?? "GitHub User",
githubId: profile.id,
avatarUrl: profile.photos?.[0]?.value ?? null,
emailVerified: true,
role: "user",
},
update: {
avatarUrl: profile.photos?.[0]?.value ?? undefined,
},
select: { id: true, email: true, name: true, role: true, avatarUrl: true, githubId: true },
})
return done(null, user)
} catch (err) {
return done(err)
}
},
),
)
// ── Session serialization ──────────────────────────────────────────────────
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser(async (id: string, done) => {
try {
const user = await db.user.findUnique({
where: { id },
select: { id: true, email: true, name: true, role: true, avatarUrl: true, githubId: true },
})
done(null, user ?? false)
} catch (err) {
done(err)
}
})
export default passport
Express Routes
// routes/auth.ts — authentication routes
import { Router, Request, Response } from "express"
import passport from "@/lib/auth/passport"
import { signAccessToken, signRefreshToken } from "@/lib/auth/jwt"
import { hashPassword } from "@/lib/auth/passwords"
import { db } from "@/lib/db"
import crypto from "crypto"
import { z } from "zod"
export const authRouter = Router()
// POST /auth/login — local strategy
authRouter.post("/login", (req, res, next) => {
passport.authenticate("local", { session: true }, async (err: Error | null, user: Express.User | false, info: { message: string } | undefined) => {
if (err) return next(err)
if (!user) return res.status(401).json({ error: info?.message ?? "Login failed" })
req.logIn(user, async (loginErr) => {
if (loginErr) return next(loginErr)
// For API clients: also return a JWT
const token = await signAccessToken({ userId: user.id, role: user.role, sessionId: req.sessionID ?? "" })
res.json({
user: { id: user.id, name: user.name, email: user.email, role: user.role },
token,
})
})
})(req, res, next)
})
// POST /auth/register
authRouter.post("/register", async (req, res, next) => {
try {
const parsed = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(1).max(100),
}).safeParse(req.body)
if (!parsed.success) {
return res.status(400).json({ error: "Invalid input" })
}
const { email, password, name } = parsed.data
const existing = await db.user.findUnique({ where: { email } })
if (existing) {
return res.status(409).json({ error: "Email already registered" })
}
const passwordHash = await hashPassword(password)
const user = await db.user.create({
data: { email, name, passwordHash, role: "user", emailVerified: false },
select: { id: true, email: true, name: true, role: true },
})
res.status(201).json({ user })
} catch (err) {
next(err)
}
})
// GET /auth/github — start OAuth
authRouter.get("/github", passport.authenticate("github", { scope: ["user:email"] }))
// GET /auth/github/callback
authRouter.get(
"/github/callback",
passport.authenticate("github", { failureRedirect: "/sign-in?error=github_failed" }),
(req, res) => {
res.redirect("/dashboard")
},
)
// POST /auth/logout
authRouter.post("/logout", (req, res, next) => {
req.logout((err) => {
if (err) return next(err)
req.session.destroy(() => {
res.clearCookie("connect.sid")
res.json({ ok: true })
})
})
})
// GET /auth/me — current user (session or JWT)
authRouter.get(
"/me",
(req, res, next) => {
// Try session first, fall back to JWT
if (req.isAuthenticated()) return next()
passport.authenticate("jwt", { session: false })(req, res, next)
},
(req, res) => {
if (!req.user) return res.status(401).json({ error: "Unauthorized" })
res.json({ user: req.user })
},
)
Middleware
// middleware/auth.ts — requireAuth and requireRole
import { Request, Response, NextFunction } from "express"
import passport from "@/lib/auth/passport"
// Session auth (cookie-based)
export function requireAuth(req: Request, res: Response, next: NextFunction) {
if (req.isAuthenticated()) return next()
if (req.path.startsWith("/api/")) {
return res.status(401).json({ error: "Unauthorized" })
}
res.redirect(`/sign-in?redirect=${encodeURIComponent(req.path)}`)
}
// JWT auth (Bearer token)
export const requireJwt = passport.authenticate("jwt", { session: false })
// Role guard
export function requireRole(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).json({ error: "Unauthorized" })
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: "Forbidden" })
}
next()
}
}
// Usage:
// router.get("/admin", requireAuth, requireRole("admin"), handler)
// router.get("/api/data", requireJwt, handler)
For the Better Auth alternative when a modern, full-stack TypeScript auth library with framework adapters for Next.js, Remix, Hono, and SvelteKit out of the box is preferred — Better Auth handles sessions, OAuth, API keys, and organizations without Express-specific boilerplate, see the Better Auth guide. For the Lucia Auth alternative when database-backed sessions with per-device session management, explicit session invalidation, and clean TypeScript types without global namespace augmentation are preferred — Lucia’s adapter pattern works across databases while Passport.js relies on database-agnostic strategy callbacks, see the Lucia guide. The Claude Skills 360 bundle includes Passport.js skill sets covering local strategy, JWT, and OAuth. Start with the free tier to try authentication middleware generation.