Authentication in Express boils down to a recurring pattern: a route handler that verifies “who is this user?” before letting them touch protected stuff. We have two big choices for how to wire that up — Passport.js (batteries-included) or roll-our-own middleware.
In simple language: Passport is like buying an IKEA shelf — pick a strategy, snap it together. Custom middleware is woodworking from scratch. Both work, the tradeoff is flexibility vs control.
The auth flow at a glance
Option 1: Passport.js
Passport is a middleware layer that abstracts auth into “strategies”. One strategy for local username/password, one for Google OAuth, one for JWT, etc. We pick what we need.
npm install passport passport-local passport-jwt
import passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
passport.use(new LocalStrategy(
{ usernameField: "email" },
async (email, password, done) => {
const user = await db.users.findByEmail(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return done(null, false, { message: "Invalid credentials" });
}
return done(null, user);
}
));
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
}, async (payload, done) => {
const user = await db.users.findById(payload.sub);
return user ? done(null, user) : done(null, false);
}));
app.post("/login",
passport.authenticate("local", { session: false }),
(req, res) => {
const token = jwt.sign({ sub: req.user.id }, process.env.JWT_SECRET, { expiresIn: "15m" });
res.json({ token });
}
);
app.get("/me", passport.authenticate("jwt", { session: false }), (req, res) => {
res.json({ user: req.user });
});
The win: 500+ strategies for OAuth providers (Google, GitHub, Facebook). The cost: a learning curve, and the API feels dated.
Option 2: Custom middleware
For most APIs we control end-to-end, a 20-line middleware beats pulling in Passport. Honest take: I reach for this 80% of the time.
import jwt from "jsonwebtoken";
export function requireAuth(req, res, next) {
const header = req.headers.authorization;
if (!header?.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing token" });
}
try {
const payload = jwt.verify(header.slice(7), process.env.JWT_SECRET);
req.user = { id: payload.sub, role: payload.role };
next();
} catch {
res.status(401).json({ error: "Invalid or expired token" });
}
}
app.get("/me", requireAuth, (req, res) => {
res.json({ userId: req.user.id });
});
Want role-based access? Compose another middleware.
const requireRole = (role) => (req, res, next) =>
req.user?.role === role ? next() : res.status(403).end();
app.delete("/users/:id", requireAuth, requireRole("admin"), deleteUser);
Refresh tokens
Short-lived access tokens (15 min) limit damage if one leaks. But forcing users to log in every 15 minutes is hell. Refresh tokens fix this — a long-lived (7-30 day) token that only does one thing: get new access tokens.
app.post("/login", async (req, res) => {
const user = await verifyCredentials(req.body);
const accessToken = jwt.sign({ sub: user.id }, ACCESS_SECRET, { expiresIn: "15m" });
const refreshToken = jwt.sign({ sub: user.id }, REFRESH_SECRET, { expiresIn: "30d" });
// store refresh token hash in DB so we can revoke it
await db.refreshTokens.insert({ userId: user.id, tokenHash: sha256(refreshToken) });
res.json({ accessToken, refreshToken });
});
app.post("/refresh", async (req, res) => {
const { refreshToken } = req.body;
try {
const payload = jwt.verify(refreshToken, REFRESH_SECRET);
const stored = await db.refreshTokens.findByHash(sha256(refreshToken));
if (!stored) return res.status(401).end(); // revoked
const accessToken = jwt.sign({ sub: payload.sub }, ACCESS_SECRET, { expiresIn: "15m" });
res.json({ accessToken });
} catch {
res.status(401).end();
}
});
Key idea: refresh tokens live in the DB so we can revoke them on logout. Access tokens are stateless and trusted as long as the signature checks out.
Which to pick?
- Building an API for your own clients? Custom middleware.
- Need Google/GitHub/SSO login? Passport, no contest.
- Mixing both? Totally fine — Passport for OAuth, custom JWT middleware for your API routes.