01 //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 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.
- 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
Broken authentication leads to account takeover, unauthorized access to sensitive data, identity theft, and compliance violations.
Which vulnerability class typically has the highest impact?
02 //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.
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}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;
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,
39 memoryCost: 65536,
40 timeCost: 3,
41 parallelism: 4,
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 (bad) | 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?
03 //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.
- Attacker gets session ID from server
- Attacker sends victim a link with that session ID
- Victim logs in, session becomes authenticated
- Attacker uses same session ID to access account
- Victim logs in and gets session ID
- Attacker steals session ID (XSS, network sniffing)
- Attacker replays session ID
- Attacker has full access to victim's session
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 req.session.user = username;
18 res.redirect('/dashboard');
19 }
20});
21
22// VULNERABLE: Sessions never expire
23app.use(session({
24 secret: 'secret',
25 cookie: {} // No maxAge
26}));
27
28// VULNERABLE: Session ID in URL
29app.get('/dashboard', (req, res) => {
30 const sessionId = req.query.sid; // Leaked via Referer
31});
32
33// VULNERABLE: Missing secure cookie flags
34app.use(session({
35 secret: 'secret',
36 cookie: {
37 // Missing: httpOnly, secure, sameSite
38 }
39}));1const session = require('express-session');
2
3// SECURE: Session configuration
4app.use(session({
5 secret: process.env.SESSION_SECRET,
6 name: '__Host-sessionId',
7 resave: false,
8 saveUninitialized: false,
9 cookie: {
10 httpOnly: true, // Prevent XSS access
11 secure: true, // HTTPS only
12 sameSite: 'strict', // CSRF protection
13 maxAge: 3600000, // 1 hour
14 path: '/',
15 },
16 store: new RedisStore({ client: redisClient }),
17}));
18
19// SECURE: Regenerate session on authentication change
20app.post('/login', async (req, res) => {
21 const { username, password } = req.body;
22
23 if (await authenticate(username, password)) {
24 req.session.regenerate((err) => {
25 if (err) return res.status(500).send('Error');
26
27 req.session.user = username;
28 req.session.loginTime = Date.now();
29 req.session.save((err) => {
30 if (err) return res.status(500).send('Error');
31 res.redirect('/dashboard');
32 });
33 });
34 }
35});
36
37// SECURE: Destroy session on logout
38app.post('/logout', (req, res) => {
39 req.session.destroy(() => {
40 res.clearCookie('__Host-sessionId');
41 res.redirect('/login');
42 });
43});
44
45// SECURE: Implement absolute session timeout
46app.use((req, res, next) => {
47 if (req.session.loginTime) {
48 const maxAge = 24 * 60 * 60 * 1000;
49 if (Date.now() - req.session.loginTime > maxAge) {
50 return req.session.destroy(() => res.redirect('/login'));
51 }
52 }
53 next();
54});What is session fixation?
04 //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).
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,
20});
21
22// VULNERABLE: No account lockout1const rateLimit = require('express-rate-limit');
2const RedisStore = require('rate-limit-redis');
3
4// Layer 1: Global rate limit per IP
5const globalLimiter = rateLimit({
6 windowMs: 15 * 60 * 1000,
7 max: 100,
8 standardHeaders: true,
9 legacyHeaders: false,
10 store: new RedisStore({ client: redisClient }),
11});
12
13// Layer 2: Strict login rate limit per IP
14const loginLimiter = rateLimit({
15 windowMs: 15 * 60 * 1000,
16 max: 5,
17 message: 'Too many login attempts, please try again later',
18 store: new RedisStore({ client: redisClient }),
19});
20
21// Layer 3: Per-account rate limiting
22const accountLimiter = async (req, res, next) => {
23 const { username } = req.body;
24 const key = `login_attempts:${username}`;
25
26 const attempts = await redis.incr(key);
27 if (attempts === 1) {
28 await redis.expire(key, 900);
29 }
30
31 if (attempts > 5) {
32 const lockoutMinutes = Math.min(Math.pow(2, attempts - 5), 60);
33 return res.status(429).json({
34 error: `Account locked for ${lockoutMinutes} minutes`,
35 });
36 }
37
38 next();
39};
40
41app.post('/login',
42 globalLimiter,
43 loginLimiter,
44 accountLimiter,
45 loginHandler
46);
47
48// CAPTCHA after failed attempts
49const showCaptcha = async (username) => {
50 const attempts = await redis.get(`login_attempts:${username}`);
51 return parseInt(attempts) >= 3;
52};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?
05 //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.
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// Registration enumeration
18app.post('/register', async (req, res) => {
19 const { email, password } = req.body;
20
21 if (await User.findByEmail(email)) {
22 return res.status(400).json({ error: 'Email already registered' });
23 }
24});
25
26// Password reset enumeration
27app.post('/forgot-password', async (req, res) => {
28 const { email } = req.body;
29 const user = await User.findByEmail(email);
30
31 if (!user) {
32 return res.status(404).json({ error: 'Email not found' });
33 }
34
35 await sendResetEmail(user);
36 res.json({ success: true });
37});
38
39// VULNERABLE: Timing-based enumeration
40app.post('/login', async (req, res) => {
41 const { email, password } = req.body;
42 const user = await User.findByEmail(email); // ~50ms if found, ~5ms if not
43
44 if (!user || !await verifyPassword(password, user.passwordHash)) {
45 return res.status(401).json({ error: 'Invalid credentials' });
46 }
47});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);
10
11 if (!user || !validPassword) {
12 return res.status(401).json({
13 error: 'Invalid email or password'
14 });
15 }
16});
17
18// SECURE: Registration without revealing existing accounts
19app.post('/register', async (req, res) => {
20 const { email, password } = req.body;
21
22 const existingUser = await User.findByEmail(email);
23
24 if (existingUser) {
25 await sendExistingAccountEmail(email);
26 } else {
27 await createUser(email, password);
28 await sendWelcomeEmail(email);
29 }
30
31 res.json({
32 message: 'If this email is valid, you will receive further instructions'
33 });
34});
35
36// SECURE: Password reset without enumeration
37app.post('/forgot-password', async (req, res) => {
38 const { email } = req.body;
39
40 const user = await User.findByEmail(email);
41 if (user) {
42 await sendPasswordResetEmail(user);
43 }
44
45 res.json({
46 message: 'If an account exists, a reset link has been sent'
47 });
48});
49
50// SECURE: Constant-time comparison
51const crypto = require('crypto');
52
53function constantTimeCompare(a, b) {
54 if (a.length !== b.length) {
55 crypto.timingSafeEqual(Buffer.from(a), Buffer.from(a));
56 return false;
57 }
58 return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
59}How can timing attacks reveal valid accounts even with generic error messages?
06 //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.
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: Weak security questions
31const securityQuestions = [
32 "What is your mother's maiden name?",
33 "What city were you born in?",
34 "What is your pet's name?",
35];1const crypto = require('crypto');
2
3// SECURE: Strong random token generation
4function generateResetToken() {
5 return crypto.randomBytes(32).toString('hex');
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);
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 await sendResetEmail(user.email, token);
28 }
29
30 res.json({ message: 'If valid, reset link sent' });
31});
32
33// SECURE: Validate and invalidate token
34app.post('/reset-password', async (req, res) => {
35 const { token, newPassword } = req.body;
36
37 const tokenHash = crypto.createHash('sha256')
38 .update(token)
39 .digest('hex');
40
41 const user = await User.findOne({
42 resetTokenHash: tokenHash,
43 resetTokenExpiry: { $gt: new Date() },
44 });
45
46 if (!user) {
47 return res.status(400).json({ error: 'Invalid or expired token' });
48 }
49
50 const passwordErrors = validatePassword(newPassword);
51 if (passwordErrors.length > 0) {
52 return res.status(400).json({ errors: passwordErrors });
53 }
54
55 await user.update({
56 passwordHash: await hashPassword(newPassword),
57 resetTokenHash: null,
58 resetTokenExpiry: null,
59 });
60
61 await invalidateUserSessions(user.id);
62 await sendPasswordChangedEmail(user.email);
63
64 res.json({ success: true });
65});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?
07 //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 |
1const helmet = require('helmet');
2const rateLimit = require('express-rate-limit');
3
4// Security headers
5app.use(helmet());
6
7// Global rate limiting
8app.use(rateLimit({
9 windowMs: 15 * 60 * 1000,
10 max: 100,
11}));
12
13// Login rate limiting
14const loginLimiter = rateLimit({
15 windowMs: 15 * 60 * 1000,
16 max: 5,
17 skipSuccessfulRequests: true,
18});
19
20// Secure session configuration
21app.use(session({
22 name: '__Host-session',
23 secret: process.env.SESSION_SECRET,
24 resave: false,
25 saveUninitialized: false,
26 store: new RedisStore({ client: redis }),
27 cookie: {
28 httpOnly: true,
29 secure: true,
30 sameSite: 'strict',
31 maxAge: 3600000,
32 },
33}));
34
35// Audit logging middleware
36const logAuthEvent = async (event, req, success, details = {}) => {
37 await AuditLog.create({
38 event,
39 userId: req.session?.userId,
40 ip: req.ip,
41 userAgent: req.get('User-Agent'),
42 success,
43 timestamp: new Date(),
44 details,
45 });
46};
47
48// Login endpoint
49app.post('/login', loginLimiter, async (req, res) => {
50 const { email, password, mfaToken } = req.body;
51
52 try {
53 const user = await User.findByEmail(email);
54
55 // Constant-time password verification
56 const validPassword = user
57 ? await argon2.verify(user.passwordHash, password)
58 : await argon2.verify(DUMMY_HASH, password);
59
60 if (!user || !validPassword) {
61 await logAuthEvent('login_failed', req, false, { email });
62 return res.status(401).json({ error: 'Invalid credentials' });
63 }
64
65 // MFA verification if enabled
66 if (user.mfaEnabled) {
67 if (!mfaToken || !verifyMfaToken(user.mfaSecret, mfaToken)) {
68 await logAuthEvent('mfa_failed', req, false, { userId: user.id });
69 return res.status(401).json({ error: 'Invalid MFA token' });
70 }
71 }
72
73 // Regenerate session
74 await new Promise((resolve, reject) => {
75 req.session.regenerate((err) => err ? reject(err) : resolve());
76 });
77
78 req.session.userId = user.id;
79 req.session.loginTime = Date.now();
80
81 await logAuthEvent('login_success', req, true, { userId: user.id });
82
83 res.json({ success: true });
84
85 } catch (error) {
86 console.error('Login error:', error);
87 res.status(500).json({ error: 'Authentication failed' });
88 }
89});What is the most important action after successful authentication?