Defense in Depth: Beyond Password Hashing

Defense in Depth: Beyond Password Hashing

Password hashing represents just one layer in a comprehensive authentication security strategy. Relying solely on strong hashing creates a single point of failure that sophisticated attackers can exploit. Defense in depth applies multiple independent security controls, ensuring that compromise of one layer doesn't lead to complete system breach. This approach acknowledges that no single security measure is perfect and that real-world systems face diverse, evolving threats.

Rate limiting forms the first line of defense against online attacks. By restricting the number of authentication attempts per account, IP address, or time period, systems prevent brute force attacks regardless of password hash strength. Effective rate limiting must balance security with usability—overly restrictive limits frustrate legitimate users, while permissive limits enable attacks. Progressive delays, where wait times increase with failed attempts, provide good balance.

import time
import redis
from datetime import datetime, timedelta
from functools import wraps

class RateLimiter:
    """Comprehensive rate limiting for authentication systems"""
    
    def __init__(self, redis_client):
        self.redis = redis_client
        
    def check_rate_limit(self, identifier, limit_type='login'):
        """Check if action is rate limited"""
        
        limits = {
            'login_per_account': (5, 300),      # 5 attempts per 5 minutes
            'login_per_ip': (20, 300),          # 20 attempts per 5 minutes
            'global_failed': (1000, 3600),      # 1000 failures per hour globally
            'password_reset': (3, 3600),        # 3 resets per hour
        }
        
        if limit_type not in limits:
            return False, 0
            
        max_attempts, window_seconds = limits[limit_type]
        key = f"rate_limit:{limit_type}:{identifier}"
        
        # Get current attempt count
        attempts = self.redis.incr(key)
        
        # Set expiry on first attempt
        if attempts == 1:
            self.redis.expire(key, window_seconds)
        
        if attempts > max_attempts:
            ttl = self.redis.ttl(key)
            return True, ttl
            
        return False, 0
    
    def progressive_delay(self, username):
        """Implement progressive delays for failed attempts"""
        
        fail_key = f"failed_attempts:{username}"
        failures = self.redis.incr(fail_key)
        
        # Set expiry for 1 hour
        if failures == 1:
            self.redis.expire(fail_key, 3600)
        
        # Calculate delay based on failures
        if failures <= 3:
            delay = 0
        elif failures <= 5:
            delay = 2 ** (failures - 3)  # 2, 4 seconds
        elif failures <= 10:
            delay = 30  # 30 seconds
        else:
            delay = 300  # 5 minutes
            
        return delay, failures
    
    def clear_limits(self, username, ip_address):
        """Clear rate limits after successful login"""
        
        keys_to_clear = [
            f"rate_limit:login_per_account:{username}",
            f"rate_limit:login_per_ip:{ip_address}",
            f"failed_attempts:{username}"
        ]
        
        for key in keys_to_clear:
            self.redis.delete(key)

# Example usage with decorator pattern
def rate_limited(limit_type='login'):
    """Decorator for rate-limited endpoints"""
    
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Extract identifier based on limit type
            request = args[0]  # Assuming first arg is request
            
            if limit_type == 'login_per_ip':
                identifier = request.remote_addr
            elif limit_type == 'login_per_account':
                identifier = request.form.get('username', 'unknown')
            else:
                identifier = 'global'
            
            # Check rate limit
            limiter = RateLimiter(redis.Redis())
            is_limited, retry_after = limiter.check_rate_limit(identifier, limit_type)
            
            if is_limited:
                return {
                    'error': 'Too many attempts. Please try again later.',
                    'retry_after': retry_after
                }, 429
                
            return func(*args, **kwargs)
            
        return wrapper
    return decorator

Account lockout policies provide another defensive layer but require careful implementation to avoid denial-of-service vulnerabilities. Temporary lockouts after repeated failures protect against sustained attacks while allowing legitimate users to regain access. Permanent lockouts should trigger administrative review rather than automatic enforcement. Consider implementing CAPTCHA challenges before lockouts to distinguish automated attacks from user errors.