Broken Authentication Code Review Guide
Table of Contents
Introduction
Broken Authentication consistently ranks in the OWASP Top 10 as one of the most critical web application vulnerabilities. When authentication is implemented incorrectly, attackers can compromise passwords, session tokens, or exploit implementation flaws to assume the identities of legitimate users.
Authentication vulnerabilities don't require complex exploits—they often arise from simple mistakes like weak password requirements, predictable session tokens, or missing rate limiting. The impact is severe: complete account takeover and access to all user data.
Authentication vs Authorization
Authentication verifies WHO you are (identity). Authorization determines WHAT you can do (permissions). Broken authentication allows attackers to become someone else entirely, which then bypasses all authorization controls for that identity.
Authentication Attack Surface
- • Weak password policies
- • Insecure password storage
- • Default credentials
- • Credential stuffing
- • Session fixation
- • Session hijacking
- • Insufficient expiration
- • Insecure session IDs
- • Weak reset tokens
- • Insecure questions
- • Token leakage
- • No rate limiting
Impact: Broken authentication leads to account takeover, unauthorized access to sensitive data, identity theft, and compliance violations.
Which vulnerability class typically has the highest impact?
Password Security
Password security encompasses both how passwords are validated during creation and how they are stored. Weak policies and improper storage are among the most common authentication flaws.
Vulnerable Password Patterns
1// VULNERABLE: No password requirements
2app.post('/register', (req, res) => {
3 const { username, password } = req.body;
4 // No validation! User could set password to "1"
5 createUser(username, password);
6});
7
8// VULNERABLE: Weak password policy (easily bypassed)
9function validatePassword(password) {
10 return password.length >= 6; // Too short!
11}
12
13// VULNERABLE: Storing plaintext passwords!
14async function createUser(username, password) {
15 await db.query(
16 'INSERT INTO users (username, password) VALUES (?, ?)',
17 [username, password] // NEVER store plaintext!
18 );
19}
20
21// VULNERABLE: Using weak hashing
22const crypto = require('crypto');
23function hashPassword(password) {
24 return crypto.createHash('md5').update(password).digest('hex');
25 // MD5 is broken! Can be cracked in seconds
26}
27
28// VULNERABLE: Hashing without salt
29function hashPassword(password) {
30 return crypto.createHash('sha256').update(password).digest('hex');
31 // Same password = same hash (rainbow table vulnerable)
32}Secure Password Implementation
1const bcrypt = require('bcrypt');
2
3// SECURE: Strong password validation
4function validatePassword(password) {
5 const errors = [];
6
7 if (password.length < 12) {
8 errors.push('Password must be at least 12 characters');
9 }
10
11 // Check against common passwords
12 if (COMMON_PASSWORDS.includes(password.toLowerCase())) {
13 errors.push('Password is too common');
14 }
15
16 // Check for breached passwords (haveibeenpwned API)
17 // await checkBreachedPassword(password);
18
19 return errors;
20}
21
22// SECURE: Using bcrypt with proper cost factor
23const BCRYPT_ROUNDS = 12; // Adjust based on server performance
24
25async function hashPassword(password) {
26 return bcrypt.hash(password, BCRYPT_ROUNDS);
27}
28
29async function verifyPassword(password, hash) {
30 return bcrypt.compare(password, hash);
31}
32
33// SECURE: Using Argon2 (winner of Password Hashing Competition)
34const argon2 = require('argon2');
35
36async function hashPassword(password) {
37 return argon2.hash(password, {
38 type: argon2.argon2id, // Recommended variant
39 memoryCost: 65536, // 64MB
40 timeCost: 3, // Iterations
41 parallelism: 4, // Threads
42 });
43}
44
45async function verifyPassword(password, hash) {
46 return argon2.verify(hash, password);
47}Password Hashing Algorithms
| Algorithm | Security | Use Case |
|---|---|---|
| MD5 | ❌ Broken | Never use for passwords |
| SHA-1 | ❌ Broken | Never use for passwords |
| SHA-256 | ⚠️ Fast | Not designed for passwords |
| bcrypt | ✅ Good | Standard choice, well-tested |
| scrypt | ✅ Good | Memory-hard, good for passwords |
| Argon2id | ✅ Best | Recommended, PHC winner |
Why is SHA-256 not recommended for password hashing?
Session Management
Session management determines how authenticated state is maintained between requests. Flaws in session handling can allow attackers to hijack legitimate user sessions or fixate sessions to known values.
Session Attack Types
1. Attacker gets session ID from server
2. Attacker sends victim a link with that session ID
3. Victim logs in, session becomes authenticated
4. Attacker uses same session ID to access account
1. Victim logs in and gets session ID
2. Attacker steals session ID (XSS, network sniffing)
3. Attacker replays session ID
4. Attacker has full access to victim's session
Session Vulnerabilities
1// VULNERABLE: Predictable session IDs
2function generateSessionId() {
3 return Date.now().toString(); // Predictable!
4}
5
6// VULNERABLE: Sequential session IDs
7let sessionCounter = 0;
8function generateSessionId() {
9 return (++sessionCounter).toString(); // Guessable!
10}
11
12// VULNERABLE: Session fixation (no regeneration on login)
13app.post('/login', (req, res) => {
14 const { username, password } = req.body;
15 if (authenticate(username, password)) {
16 // Session ID remains the same after login!
17 // Attacker could have set it beforehand
18 req.session.user = username;
19 res.redirect('/dashboard');
20 }
21});
22
23// VULNERABLE: Sessions never expire
24app.use(session({
25 secret: 'secret',
26 cookie: {} // No maxAge = session cookie (but never expires server-side!)
27}));
28
29// VULNERABLE: Session ID in URL
30app.get('/dashboard', (req, res) => {
31 const sessionId = req.query.sid; // Session in URL = leaked via Referer!
32 // ...
33});
34
35// VULNERABLE: Missing secure cookie flags
36app.use(session({
37 secret: 'secret',
38 cookie: {
39 // Missing: httpOnly, secure, sameSite
40 }
41}));Secure Session Implementation
1const session = require('express-session');
2const crypto = require('crypto');
3
4// SECURE: Cryptographically random session IDs
5// (express-session does this by default with proper config)
6
7// SECURE: Session configuration
8app.use(session({
9 secret: process.env.SESSION_SECRET, // Long, random secret
10 name: '__Host-sessionId', // Secure prefix
11 resave: false,
12 saveUninitialized: false,
13 cookie: {
14 httpOnly: true, // Prevent XSS access
15 secure: true, // HTTPS only
16 sameSite: 'strict', // CSRF protection
17 maxAge: 3600000, // 1 hour
18 path: '/',
19 },
20 store: new RedisStore({ client: redisClient }), // Server-side storage
21}));
22
23// SECURE: Regenerate session on authentication change
24app.post('/login', async (req, res) => {
25 const { username, password } = req.body;
26
27 if (await authenticate(username, password)) {
28 // Regenerate session to prevent fixation
29 req.session.regenerate((err) => {
30 if (err) return res.status(500).send('Error');
31
32 req.session.user = username;
33 req.session.loginTime = Date.now();
34 req.session.save((err) => {
35 if (err) return res.status(500).send('Error');
36 res.redirect('/dashboard');
37 });
38 });
39 }
40});
41
42// SECURE: Destroy session on logout
43app.post('/logout', (req, res) => {
44 req.session.destroy((err) => {
45 res.clearCookie('__Host-sessionId');
46 res.redirect('/login');
47 });
48});
49
50// SECURE: Implement absolute session timeout
51app.use((req, res, next) => {
52 if (req.session.loginTime) {
53 const maxAge = 24 * 60 * 60 * 1000; // 24 hours absolute
54 if (Date.now() - req.session.loginTime > maxAge) {
55 return req.session.destroy(() => res.redirect('/login'));
56 }
57 }
58 next();
59});What is session fixation?
Brute Force & Credential Stuffing
Without proper rate limiting, attackers can try thousands of password combinations (brute force) or use leaked credential databases to find valid accounts (credential stuffing).
Vulnerable Login Endpoints
1// VULNERABLE: No rate limiting
2app.post('/login', async (req, res) => {
3 const { username, password } = req.body;
4
5 // Attacker can try millions of passwords!
6 if (await authenticate(username, password)) {
7 req.session.user = username;
8 res.json({ success: true });
9 } else {
10 res.status(401).json({ error: 'Invalid credentials' });
11 }
12});
13
14// VULNERABLE: Rate limiting by IP only
15// Attackers use distributed botnets with many IPs!
16const limiter = rateLimit({
17 windowMs: 15 * 60 * 1000,
18 max: 100,
19 keyGenerator: (req) => req.ip, // Easy to bypass with proxies
20});
21
22// VULNERABLE: No account lockout
23// Attacker can try unlimited passwords for each accountSecure Rate Limiting
1const rateLimit = require('express-rate-limit');
2const RedisStore = require('rate-limit-redis');
3
4// SECURE: Multi-layered rate limiting
5// Layer 1: Global rate limit per IP
6const globalLimiter = rateLimit({
7 windowMs: 15 * 60 * 1000, // 15 minutes
8 max: 100, // 100 requests per window
9 standardHeaders: true,
10 legacyHeaders: false,
11 store: new RedisStore({ client: redisClient }),
12});
13
14// Layer 2: Strict login rate limit per IP
15const loginLimiter = rateLimit({
16 windowMs: 15 * 60 * 1000,
17 max: 5, // Only 5 login attempts per 15 minutes per IP
18 message: 'Too many login attempts, please try again later',
19 store: new RedisStore({ client: redisClient }),
20});
21
22// Layer 3: Per-account rate limiting
23const accountLimiter = async (req, res, next) => {
24 const { username } = req.body;
25 const key = `login_attempts:${username}`;
26
27 const attempts = await redis.incr(key);
28 if (attempts === 1) {
29 await redis.expire(key, 900); // 15 minute window
30 }
31
32 if (attempts > 5) {
33 // Exponential backoff
34 const lockoutMinutes = Math.min(Math.pow(2, attempts - 5), 60);
35 return res.status(429).json({
36 error: `Account locked for ${lockoutMinutes} minutes`,
37 });
38 }
39
40 next();
41};
42
43// Layer 4: Detect credential stuffing patterns
44const stuffingDetector = async (req, res, next) => {
45 const { username, password } = req.body;
46
47 // Log failed attempts for analysis
48 // Detect patterns: many usernames, few passwords
49 // Alert security team on suspicious activity
50
51 next();
52};
53
54app.post('/login',
55 globalLimiter,
56 loginLimiter,
57 accountLimiter,
58 stuffingDetector,
59 loginHandler
60);
61
62// SECURE: Implement CAPTCHA after failed attempts
63const showCaptcha = async (username) => {
64 const attempts = await redis.get(`login_attempts:${username}`);
65 return parseInt(attempts) >= 3;
66};Brute Force Protection Strategies
| Strategy | Protection Against | Considerations |
|---|---|---|
| IP rate limiting | Simple attacks | Bypassed by botnets/proxies |
| Account lockout | Targeted attacks | Can cause DoS on accounts |
| CAPTCHA | Automated attacks | UX friction, accessibility |
| Progressive delays | Both types | Good UX, still allows legitimate use |
| Device fingerprinting | Sophisticated attacks | Privacy concerns |
| Breached password check | Credential stuffing | Requires API integration |
What is credential stuffing?
Account Enumeration
Account enumeration allows attackers to determine valid usernames or email addresses. This information is then used for targeted attacks like credential stuffing or phishing.
Enumeration Vulnerabilities
1// VULNERABLE: Different error messages reveal valid accounts
2
3// Login enumeration
4app.post('/login', async (req, res) => {
5 const { email, password } = req.body;
6 const user = await User.findByEmail(email);
7
8 if (!user) {
9 return res.status(401).json({ error: 'User not found' }); // Reveals invalid email!
10 }
11
12 if (!await verifyPassword(password, user.passwordHash)) {
13 return res.status(401).json({ error: 'Incorrect password' }); // Reveals valid email!
14 }
15
16 // ...
17});
18
19// Registration enumeration
20app.post('/register', async (req, res) => {
21 const { email, password } = req.body;
22
23 if (await User.findByEmail(email)) {
24 return res.status(400).json({ error: 'Email already registered' }); // Enumeration!
25 }
26
27 // ...
28});
29
30// Password reset enumeration
31app.post('/forgot-password', async (req, res) => {
32 const { email } = req.body;
33 const user = await User.findByEmail(email);
34
35 if (!user) {
36 return res.status(404).json({ error: 'Email not found' }); // Enumeration!
37 }
38
39 await sendResetEmail(user);
40 res.json({ success: true });
41});
42
43// VULNERABLE: Timing-based enumeration
44// Even with same message, different response times reveal valid accounts!
45app.post('/login', async (req, res) => {
46 const { email, password } = req.body;
47 const user = await User.findByEmail(email); // ~50ms if found, ~5ms if not
48
49 if (!user || !await verifyPassword(password, user.passwordHash)) {
50 return res.status(401).json({ error: 'Invalid credentials' });
51 }
52 // ...
53});Preventing Account Enumeration
1// SECURE: Generic error messages
2app.post('/login', async (req, res) => {
3 const { email, password } = req.body;
4 const user = await User.findByEmail(email);
5
6 // Always perform password check to prevent timing attacks
7 const validPassword = user
8 ? await verifyPassword(password, user.passwordHash)
9 : await verifyPassword(password, DUMMY_HASH); // Constant time!
10
11 if (!user || !validPassword) {
12 // Same message for both cases
13 return res.status(401).json({
14 error: 'Invalid email or password'
15 });
16 }
17
18 // ...
19});
20
21// SECURE: Registration without revealing existing accounts
22app.post('/register', async (req, res) => {
23 const { email, password } = req.body;
24
25 const existingUser = await User.findByEmail(email);
26
27 if (existingUser) {
28 // Send email to existing account instead of error
29 await sendExistingAccountEmail(email);
30 } else {
31 await createUser(email, password);
32 await sendWelcomeEmail(email);
33 }
34
35 // Same response regardless
36 res.json({
37 message: 'If this email is valid, you will receive further instructions'
38 });
39});
40
41// SECURE: Password reset without enumeration
42app.post('/forgot-password', async (req, res) => {
43 const { email } = req.body;
44
45 // Always return success, send email only if account exists
46 const user = await User.findByEmail(email);
47 if (user) {
48 await sendPasswordResetEmail(user);
49 }
50
51 // Same response and timing
52 res.json({
53 message: 'If an account exists, a reset link has been sent'
54 });
55});
56
57// SECURE: Constant-time comparison for timing attack prevention
58const crypto = require('crypto');
59
60function constantTimeCompare(a, b) {
61 if (a.length !== b.length) {
62 // Still compare to maintain constant time
63 crypto.timingSafeEqual(Buffer.from(a), Buffer.from(a));
64 return false;
65 }
66 return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
67}How can timing attacks reveal valid accounts even with generic error messages?
Password Reset Flaws
Password reset functionality is a critical attack vector. Weak reset tokens, improper validation, or insecure delivery methods can allow attackers to take over accounts without knowing the original password.
Vulnerable Reset Implementations
1// VULNERABLE: Predictable reset tokens
2function generateResetToken(user) {
3 return user.id + '-' + Date.now(); // Guessable!
4}
5
6// VULNERABLE: Short/weak tokens
7function generateResetToken() {
8 return Math.random().toString(36).substring(2, 8); // Only 6 chars!
9}
10
11// VULNERABLE: Token never expires
12app.post('/forgot-password', async (req, res) => {
13 const token = generateToken();
14 await user.update({ resetToken: token }); // No expiry!
15 await sendResetEmail(user.email, token);
16});
17
18// VULNERABLE: Token not invalidated after use
19app.post('/reset-password', async (req, res) => {
20 const { token, newPassword } = req.body;
21 const user = await User.findOne({ resetToken: token });
22
23 if (user) {
24 await user.update({ password: hashPassword(newPassword) });
25 // Token still valid! Can be reused!
26 res.json({ success: true });
27 }
28});
29
30// VULNERABLE: Token in URL (logged, leaked via Referer)
31// Email: Click here to reset: https://example.com/reset?token=abc123
32// Token visible in browser history, server logs, and Referer header!
33
34// VULNERABLE: Weak security questions
35const securityQuestions = [
36 "What is your mother's maiden name?", // Findable via social media
37 "What city were you born in?", // Public information
38 "What is your pet's name?", // Often posted online
39];Secure Password Reset
1const crypto = require('crypto');
2
3// SECURE: Strong random token generation
4function generateResetToken() {
5 return crypto.randomBytes(32).toString('hex'); // 64 hex chars
6}
7
8// SECURE: Token with expiration
9app.post('/forgot-password', async (req, res) => {
10 const { email } = req.body;
11 const user = await User.findByEmail(email);
12
13 if (user) {
14 const token = generateResetToken();
15 const expiry = new Date(Date.now() + 3600000); // 1 hour
16
17 // Store hashed token (prevents DB exposure from revealing tokens)
18 const tokenHash = crypto.createHash('sha256')
19 .update(token)
20 .digest('hex');
21
22 await user.update({
23 resetTokenHash: tokenHash,
24 resetTokenExpiry: expiry,
25 });
26
27 // Send token (not hash) via email
28 await sendResetEmail(user.email, token);
29 }
30
31 // Same response regardless (prevent enumeration)
32 res.json({ message: 'If valid, reset link sent' });
33});
34
35// SECURE: Validate and invalidate token
36app.post('/reset-password', async (req, res) => {
37 const { token, newPassword } = req.body;
38
39 // Hash token to compare with stored hash
40 const tokenHash = crypto.createHash('sha256')
41 .update(token)
42 .digest('hex');
43
44 const user = await User.findOne({
45 resetTokenHash: tokenHash,
46 resetTokenExpiry: { $gt: new Date() }, // Not expired
47 });
48
49 if (!user) {
50 return res.status(400).json({ error: 'Invalid or expired token' });
51 }
52
53 // Validate new password
54 const passwordErrors = validatePassword(newPassword);
55 if (passwordErrors.length > 0) {
56 return res.status(400).json({ errors: passwordErrors });
57 }
58
59 // Update password and invalidate token
60 await user.update({
61 passwordHash: await hashPassword(newPassword),
62 resetTokenHash: null,
63 resetTokenExpiry: null,
64 });
65
66 // Invalidate all sessions (force re-login)
67 await invalidateUserSessions(user.id);
68
69 // Notify user of password change
70 await sendPasswordChangedEmail(user.email);
71
72 res.json({ success: true });
73});Password Reset Checklist
| Control | Implementation | Priority |
|---|---|---|
| Strong tokens | Cryptographically random, 32+ bytes | Critical |
| Token expiration | 1 hour or less | Critical |
| Single use | Invalidate after use | Critical |
| Secure delivery | Email only, not URL params | High |
| Rate limiting | Limit reset requests per account | High |
| Notify on change | Email when password changed | High |
| Invalidate sessions | Log out all devices on reset | High |
Why should reset tokens be stored as hashes in the database?
Prevention Techniques
Preventing authentication vulnerabilities requires a comprehensive approach covering credential management, session security, and monitoring.
Authentication Security Checklist
| Control | Implementation | Priority |
|---|---|---|
| Password hashing | Argon2id or bcrypt | Critical |
| Password policy | 12+ chars, breach checking | Critical |
| Session regeneration | New session ID on login | Critical |
| Secure cookies | httpOnly, secure, sameSite | Critical |
| Rate limiting | IP + account based | Critical |
| Generic errors | Same message for all failures | High |
| MFA | TOTP or hardware keys | High |
| Session timeout | Idle + absolute limits | High |
| Audit logging | Log auth events | High |
| Breach monitoring | Check passwords against leaks | Medium |
Comprehensive Secure Auth Implementation
1// Secure authentication middleware setup
2const helmet = require('helmet');
3const rateLimit = require('express-rate-limit');
4
5// Security headers
6app.use(helmet());
7
8// Global rate limiting
9app.use(rateLimit({
10 windowMs: 15 * 60 * 1000,
11 max: 100,
12}));
13
14// Login rate limiting
15const loginLimiter = rateLimit({
16 windowMs: 15 * 60 * 1000,
17 max: 5,
18 skipSuccessfulRequests: true,
19});
20
21// Secure session configuration
22app.use(session({
23 name: '__Host-session',
24 secret: process.env.SESSION_SECRET,
25 resave: false,
26 saveUninitialized: false,
27 store: new RedisStore({ client: redis }),
28 cookie: {
29 httpOnly: true,
30 secure: true,
31 sameSite: 'strict',
32 maxAge: 3600000,
33 },
34}));
35
36// Audit logging middleware
37const logAuthEvent = async (event, req, success, details = {}) => {
38 await AuditLog.create({
39 event,
40 userId: req.session?.userId,
41 ip: req.ip,
42 userAgent: req.get('User-Agent'),
43 success,
44 timestamp: new Date(),
45 details,
46 });
47};
48
49// Login endpoint
50app.post('/login', loginLimiter, async (req, res) => {
51 const { email, password, mfaToken } = req.body;
52
53 try {
54 const user = await User.findByEmail(email);
55
56 // Constant-time password verification
57 const validPassword = user
58 ? await argon2.verify(user.passwordHash, password)
59 : await argon2.verify(DUMMY_HASH, password);
60
61 if (!user || !validPassword) {
62 await logAuthEvent('login_failed', req, false, { email });
63 return res.status(401).json({ error: 'Invalid credentials' });
64 }
65
66 // MFA verification if enabled
67 if (user.mfaEnabled) {
68 if (!mfaToken || !verifyMfaToken(user.mfaSecret, mfaToken)) {
69 await logAuthEvent('mfa_failed', req, false, { userId: user.id });
70 return res.status(401).json({ error: 'Invalid MFA token' });
71 }
72 }
73
74 // Regenerate session
75 await new Promise((resolve, reject) => {
76 req.session.regenerate((err) => err ? reject(err) : resolve());
77 });
78
79 req.session.userId = user.id;
80 req.session.loginTime = Date.now();
81
82 await logAuthEvent('login_success', req, true, { userId: user.id });
83
84 res.json({ success: true });
85
86 } catch (error) {
87 console.error('Login error:', error);
88 res.status(500).json({ error: 'Authentication failed' });
89 }
90});What is the most important action after successful authentication?