Secure Authentication
Authentication verifies user identity. Poor authentication implementation is a leading cause of security breaches. Let's explore best practices.
Password Security
1. Password Hashing with bcrypt
const bcrypt = require('bcrypt');
// NEVER store plain text passwords!
// BAD:
const user = { password: req.body.password }; // NEVER DO THIS
// GOOD: Hash passwords before storing
async function hashPassword(password) {
const saltRounds = 12; // Higher = more secure but slower
return bcrypt.hash(password, saltRounds);
}
async function createUser(email, password) {
const passwordHash = await hashPassword(password);
return db.user.create({
data: {
email,
passwordHash, // Store the hash, never the plain password
},
});
}
// Verify password
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
async function login(email, password) {
const user = await db.user.findUnique({ where: { email } });
if (!user) {
// Use same error to prevent user enumeration
throw new Error('Invalid credentials');
}
const valid = await verifyPassword(password, user.passwordHash);
if (!valid) {
throw new Error('Invalid credentials');
}
return user;
}
2. Password Requirements
import { z } from 'zod';
const passwordSchema = z.string()
.min(12, 'Password must be at least 12 characters')
.regex(/[A-Z]/, 'Password must contain uppercase letter')
.regex(/[a-z]/, 'Password must contain lowercase letter')
.regex(/[0-9]/, 'Password must contain number')
.regex(/[^A-Za-z0-9]/, 'Password must contain special character')
.refine(
(pwd) => !commonPasswords.includes(pwd.toLowerCase()),
'Password is too common'
);
// Check against breached passwords using Have I Been Pwned API
async function isPasswordBreached(password) {
const crypto = require('crypto');
const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const prefix = hash.slice(0, 5);
const suffix = hash.slice(5);
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const text = await response.text();
return text.includes(suffix);
}
Session Management
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');
const redisClient = redis.createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
name: 'sessionId', // Don't use default 'connect.sid'
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // Only HTTPS
httpOnly: true, // No JavaScript access
sameSite: 'strict', // CSRF protection
maxAge: 3600000, // 1 hour
},
}));
// Regenerate session on login to prevent fixation
app.post('/login', async (req, res) => {
const user = await authenticate(req.body);
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.loginTime = Date.now();
res.json({ success: true });
});
});
// Destroy session on logout
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
res.clearCookie('sessionId');
res.json({ success: true });
});
});
JWT Authentication
const jwt = require('jsonwebtoken');
// Generate tokens
function generateTokens(userId) {
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Verify access token
function verifyAccessToken(token) {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
if (payload.type !== 'access') throw new Error('Invalid token type');
return payload;
} catch (error) {
throw new Error('Invalid token');
}
}
// Middleware
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
req.user = verifyAccessToken(token);
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
// Refresh token endpoint
app.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Check if token is revoked (store in Redis/DB)
const isRevoked = await isTokenRevoked(refreshToken);
if (isRevoked) throw new Error('Token revoked');
const tokens = generateTokens(payload.userId);
res.json(tokens);
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Multi-Factor Authentication (MFA)
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate TOTP secret
async function enableMFA(userId) {
const secret = speakeasy.generateSecret({
name: `MyApp (${user.email})`,
issuer: 'MyApp',
});
// Store secret securely (encrypted)
await db.user.update({
where: { id: userId },
data: { mfaSecret: encrypt(secret.base32) },
});
// Generate QR code for authenticator app
const qrCode = await QRCode.toDataURL(secret.otpauth_url);
return { secret: secret.base32, qrCode };
}
// Verify TOTP code
function verifyMFACode(secret, code) {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token: code,
window: 1, // Allow 1 step before/after
});
}
// Login with MFA
async function loginWithMFA(email, password, mfaCode) {
const user = await authenticate(email, password);
if (user.mfaEnabled) {
const secret = decrypt(user.mfaSecret);
const valid = verifyMFACode(secret, mfaCode);
if (!valid) {
throw new Error('Invalid MFA code');
}
}
return generateTokens(user.id);
}
Rate Limiting
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
// Strict rate limiting for auth endpoints
const authLimiter = rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: { error: 'Too many login attempts, try again later' },
standardHeaders: true,
legacyHeaders: false,
});
app.post('/login', authLimiter, loginHandler);
app.post('/register', authLimiter, registerHandler);
app.post('/forgot-password', authLimiter, forgotPasswordHandler);
Security Checklist
- Hash passwords with bcrypt (cost factor 12+)
- Enforce strong password requirements
- Check passwords against breach databases
- Use secure session cookies (httpOnly, secure, sameSite)
- Implement short-lived access tokens + refresh tokens
- Offer and encourage MFA
- Rate limit authentication endpoints
- Log authentication events
- Use account lockout after failed attempts