Hash passwords with a costly function, choose between sessions and JWT with full awareness, and protect your routes with middleware.
Open this lesson in KodokonStoring a password means preparing for a future breach: design the hashing to survive theft of the database. A fast hash like SHA-256 can be brute-forced at billions of attempts per second on a GPU. You need a deliberately costly function - scrypt, provided by node:crypto, or bcrypt/argon2 - and a random per-user salt, which renders precomputed tables useless. The salt is not a secret: it's stored in plain text next to the hash.
import {
randomBytes,
scryptSync,
timingSafeEqual,
} from 'node:crypto';
export function hashPassword(password) {
const salt = randomBytes(16).toString('hex');
const hash = scryptSync(password, salt, 64)
.toString('hex');
return `${salt}:${hash}`;
}
export function verifyPassword(password, stored) {
const [salt, hash] = stored.split(':');
const candidate = scryptSync(password, salt, 64);
const expected = Buffer.from(hash, 'hex');
return timingSafeEqual(candidate, expected);
}Two models for keeping the user logged in. The session: an opaque identifier in an httpOnly cookie, with state on the server side. Immediate revocation, cookie invisible to JavaScript, but a shared store (Redis) becomes necessary as soon as you add a second instance. The JWT: the signed state embedded in the token, verifiable without any store - ideal between services - but irrevocable before expiry. The honest trade-off: sessions for a classic web application, short-lived JWTs for APIs consumed by third parties or for distributed architectures.
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET;
if (!SECRET) {
throw new Error('JWT_SECRET must be set');
}
export function signToken(user) {
return jwt.sign(
{ sub: user.id, email: user.email },
SECRET,
{ expiresIn: '15m' },
);
}The protection middleware factors out access control: it extracts the token from the Authorization: Bearer header, verifies it, attaches the decoded identity to req.user, or cuts things short with a 401 routed to the error middleware from the previous lesson. Apply it at the router level (router.use(requireAuth)) to protect an entire group of routes rather than route by route.
import jwt from 'jsonwebtoken';
import { HttpError } from './http-error.js';
const SECRET = process.env.JWT_SECRET;
export function requireAuth(req, res, next) {
const header = req.headers.authorization ?? '';
const [scheme, token] = header.split(' ');
if (scheme !== 'Bearer' || !token) {
return next(new HttpError(401, 'Missing token'));
}
try {
req.user = jwt.verify(token, SECRET);
next();
} catch {
next(new HttpError(401, 'Invalid or expired token'));
}
}timingSafeEqual for in password verification?