Sessions vs JWTs — and the Patterns That Actually Work
Authentication Basics
Hash passwords, issue tokens, protect routes. Know when to use sessions, when to use JWTs, and when to outsource the whole thing.
What you'll learn
- Hash passwords correctly
- Implement a simple JWT flow
- Choose between sessions and JWTs
Auth has many shapes — and many sharp edges. Pick a pattern, use a library.
Step 1 — Hash Passwords
Never store plaintext. Use a slow, salted KDF:
npm install bcrypt import bcrypt from "bcrypt";
const hash = await bcrypt.hash("user-password", 12);
// store `hash` in DB
const ok = await bcrypt.compare("user-password", hash);
// ok === true 12 is the cost factor — higher = slower = more secure. Tune so
hashing takes ~100ms on your hardware.
argon2 is a slightly more modern alternative — both fine.
Step 2 — Issue a Token on Login
After verifying the password, give the client a token they’ll send on every request.
Option A: JWT (Stateless)
npm install jsonwebtoken import jwt from "jsonwebtoken";
const SECRET = process.env.JWT_SECRET;
function issueToken(user) {
return jwt.sign({ sub: user.id, email: user.email }, SECRET, { expiresIn: "1h" });
}
function verifyToken(token) {
return jwt.verify(token, SECRET);
} Pros: stateless, scales horizontally without shared session storage. Cons: can’t be revoked easily, mistakes have big blast radius (stolen tokens valid until expiry).
Option B: Sessions (Stateful)
import express from "express";
import session from "express-session";
import RedisStore from "connect-redis";
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET,
cookie: { httpOnly: true, secure: true, sameSite: "lax", maxAge: 24 * 3600 * 1000 },
}));
// after login:
req.session.userId = user.id; Pros: revocation is trivial (delete the session), simpler mental model. Cons: needs a session store (Redis, DB).
Step 3 — Protect Routes
function requireAuth(req, res, next) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return res.status(401).json({ error: "missing token" });
try {
req.user = verifyToken(token);
next();
} catch {
res.status(401).json({ error: "invalid token" });
}
}
app.get("/api/me", requireAuth, (req, res) => res.json(req.user)); Just Outsource It
For real apps, consider:
- Auth0, Clerk, Supabase Auth, Auth.js (NextAuth)
- They handle: OAuth (Google/GitHub), MFA, session rotation, email verification, password resets
- You handle: less
Rolling your own auth is a project that never ends well. Use a service unless you have very specific needs.
Rate Limiting →