Claude Code for Passport.js: Node.js Authentication Middleware — Claude Skills 360 Blog
Blog / Backend / Claude Code for Passport.js: Node.js Authentication Middleware
Backend

Claude Code for Passport.js: Node.js Authentication Middleware

Published: May 10, 2027
Read time: 7 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free