Broken User Authentication

Broken User Authentication

Authentication vulnerabilities allow attackers to compromise user accounts or bypass authentication entirely. Common issues include weak password policies, lack of account lockout mechanisms, predictable tokens, and missing multi-factor authentication. These vulnerabilities often combine to create severe security risks.

// Node.js example: Secure authentication implementation
const bcrypt = require('bcrypt');
const speakeasy = require('speakeasy');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

class SecureAuthenticationService {
    constructor(db, redis, config) {
        this.db = db;
        this.redis = redis;
        this.config = config;
    }
    
    // VULNERABLE: Weak authentication
    async authenticateVulnerable(username, password) {
        const user = await this.db.users.findOne({ username });
        
        // Direct password comparison - NO!
        if (user && user.password === password) {
            return { token: jwt.sign({ userId: user.id }, 'secret') };
        }
        return null;
    }
    
    // SECURE: Comprehensive authentication
    async authenticate(username, password, mfaToken = null) {
        // Input validation
        if (!this.isValidUsername(username) || !password) {
            throw new Error('Invalid credentials');
        }
        
        // Check rate limiting
        const attempts = await this.checkRateLimit(username);
        if (attempts >= 5) {
            await this.notifySecurityTeam('Brute force attempt', { username });
            throw new Error('Account temporarily locked');
        }
        
        // Fetch user with timing-safe comparison
        const user = await this.db.users.findOne({ 
            username: username.toLowerCase() 
        });
        
        // Always perform password check to prevent timing attacks
        const dummyHash = '$2b$10$dummy.hash.to.prevent.timing.attacks';
        const passwordHash = user ? user.passwordHash : dummyHash;
        const isValidPassword = await bcrypt.compare(password, passwordHash);
        
        if (!user || !isValidPassword) {
            await this.incrementFailedAttempts(username);
            // Generic error message
            throw new Error('Invalid credentials');
        }
        
        // Check if account is active
        if (user.status !== 'active') {
            throw new Error('Account is not active');
        }
        
        // Check for suspicious activity
        await this.checkSuspiciousActivity(user);
        
        // Verify MFA if enabled
        if (user.mfaEnabled) {
            if (!mfaToken) {
                return { 
                    requiresMfa: true, 
                    sessionToken: this.generateSessionToken(user.id) 
                };
            }
            
            const isValidMfa = this.verifyMfaToken(user.mfaSecret, mfaToken);
            if (!isValidMfa) {
                await this.incrementFailedAttempts(username);
                throw new Error('Invalid MFA token');
            }
        }
        
        // Generate tokens
        const accessToken = this.generateAccessToken(user);
        const refreshToken = await this.generateRefreshToken(user);
        
        // Update login metadata
        await this.updateLoginMetadata(user.id, {
            lastLoginAt: new Date(),
            lastLoginIp: this.getClientIp(),
            loginCount: user.loginCount + 1
        });
        
        // Clear failed attempts
        await this.clearFailedAttempts(username);
        
        return {
            accessToken,
            refreshToken,
            expiresIn: 3600,
            user: this.sanitizeUser(user)
        };
    }
    
    async checkRateLimit(username) {
        const key = `auth_attempts:${username}`;
        const attempts = await this.redis.incr(key);
        
        if (attempts === 1) {
            await this.redis.expire(key, 900); // 15 minutes
        }
        
        return attempts;
    }
    
    generateAccessToken(user) {
        const payload = {
            sub: user.id,
            username: user.username,
            roles: user.roles,
            permissions: user.permissions,
            iat: Math.floor(Date.now() / 1000),
            exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
        };
        
        return jwt.sign(payload, this.config.jwtSecret, {
            algorithm: 'RS256',
            issuer: this.config.issuer,
            audience: this.config.audience
        });
    }
    
    async generateRefreshToken(user) {
        const token = crypto.randomBytes(32).toString('hex');
        const hashedToken = crypto
            .createHash('sha256')
            .update(token)
            .digest('hex');
        
        // Store hashed token
        await this.db.refreshTokens.insert({
            userId: user.id,
            tokenHash: hashedToken,
            createdAt: new Date(),
            expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
            metadata: {
                userAgent: this.getUserAgent(),
                ip: this.getClientIp()
            }
        });
        
        return token;
    }
    
    // Password reset with security measures
    async initiatePasswordReset(email) {
        const user = await this.db.users.findOne({ email: email.toLowerCase() });
        
        // Always return success to prevent email enumeration
        const response = { message: 'If the email exists, a reset link has been sent' };
        
        if (!user) {
            // Log attempt for security monitoring
            await this.logSecurityEvent('password_reset_invalid_email', { email });
            return response;
        }
        
        // Check rate limiting for password resets
        const resetKey = `password_reset:${user.id}`;
        const recentReset = await this.redis.get(resetKey);
        
        if (recentReset) {
            // Silently ignore if recent reset was requested
            return response;
        }
        
        // Generate secure reset token
        const resetToken = crypto.randomBytes(32).toString('hex');
        const hashedToken = crypto
            .createHash('sha256')
            .update(resetToken)
            .digest('hex');
        
        // Store reset token with expiration
        await this.db.passwordResets.insert({
            userId: user.id,
            tokenHash: hashedToken,
            createdAt: new Date(),
            expiresAt: new Date(Date.now() + 3600000), // 1 hour
            used: false
        });
        
        // Set rate limit
        await this.redis.setex(resetKey, 3600, '1');
        
        // Send reset email
        await this.emailService.sendPasswordResetEmail(user.email, resetToken);
        
        return response;
    }
}