Authentication Security
Rules
- Hash passwords with bcrypt (cost factor 12+) or argon2id — never store plaintext or use SHA/MD5
- Rate limit login endpoints: max 5 attempts per minute per IP, 10 per account per hour
- Implement exponential lockout: 1s, 2s, 4s, 8s... after failed attempts, hard lock at 10 failures for 15 min
- Password reset tokens: cryptographically random, single-use, expire in 1 hour max
- Return generic errors: "Invalid email or password" — never reveal which field is wrong
- Enforce minimum password length of 8 characters, check against breached password lists (HaveIBeenPwned API)
import bcrypt from "bcrypt";
const hash = await bcrypt.hash(password, 12);
const valid = await bcrypt.compare(password, storedHash);
if (!valid) {
await incrementFailedAttempts(userId);
return res.status(401).json({ error: "Invalid email or password" });
}
await resetFailedAttempts(userId);
async function checkLockout(userId: string) {
const attempts = await getFailedAttempts(userId);
if (attempts.count >= 10) {
const lockoutEnd = attempts.lastAttempt + 15 * 60 * 1000;
if (Date.now() < lockoutEnd) throw new Error("Account temporarily locked");
}
}
Avoid
- Storing passwords in plaintext, base64, or reversible encryption
- SHA-256 for passwords — it's fast, which means brute-forceable
- Revealing whether an email exists during login or password reset
- Password reset links that never expire or can be reused
- Allowing unlimited login attempts from a single IP