Auth Patterns & Secure Coding
JWT Security
// JWT best practices
// 1. Use RS256 (asymmetric) for multi-service environments
// 2. Short expiry for access tokens (15 min)
// 3. Store refresh token in HttpOnly cookie, access token in memory
// 4. Validate: signature, expiry, issuer, audience
// 5. Include jti (JWT ID) for revocation
import jwt from 'jsonwebtoken';
function generateTokens(userId: string) {
const accessToken = jwt.sign(
{ sub: userId, type: 'access' },
process.env.JWT_SECRET!,
{ expiresIn: '15m', issuer: 'myapp', audience: 'myapp-api' }
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh', jti: crypto.randomUUID() },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
function verifyToken(token: string) {
return jwt.verify(token, process.env.JWT_SECRET!, {
issuer: 'myapp',
audience: 'myapp-api',
});
}Password Hashing
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12; // balance security vs speed (10-14 typical)
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// Argon2 (preferred, winner of Password Hashing Competition)
import argon2 from 'argon2';
const hash = await argon2.hash(password);
const valid = await argon2.verify(hash, password);
// Timing-safe comparison (prevent timing attacks)
import crypto from 'crypto';
function safeCompare(a: string, b: string): boolean {
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}Rate Limiting & DoS Prevention
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
// General API rate limit
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
message: { error: 'Too many requests, please try again later.' },
standardHeaders: true, // X-RateLimit-* headers
legacyHeaders: false,
});
// Stricter limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 5, // 5 attempts per minute
skipSuccessfulRequests: true,
});
app.use('/api/', apiLimiter);
app.post('/auth/login', authLimiter, loginHandler);
// Additional protections
app.use(express.json({ limit: '10kb' })); // prevent large payload attacks
app.use(helmet()); // security headers
app.disable('x-powered-by'); // don't advertise ExpressSecure Development Checklist
Scan dependencies for vulnerabilities (npm audit, Snyk, Dependabot)
Never log sensitive data (passwords, tokens, PII, credit card numbers)
Validate and sanitize ALL inputs at the API boundary
Use HTTPS for all communications (internal services too)
Secret rotation — rotate DB passwords, API keys periodically
Principle of least privilege — services/users get only what they need
Security code reviews — look for OWASP top 10 in PRs
Penetration testing — at least annually for production systems
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free