Files
michaelschiemer/docs/components/auth/security.md
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

31 KiB

Auth Module Security Guidelines

Sicherheitsrichtlinien und Best Practices für das Auth Module des Custom PHP Frameworks.

Security Architecture

Defense in Depth Strategy

Das Auth Module implementiert mehrschichtige Sicherheitsmaßnahmen:

┌─────────────────────────────────────────────────────────────┐
│                    Application Layer                        │
├─────────────────────────────────────────────────────────────┤
│ • Rate Limiting & Account Lockout                          │
│ • Session Security & IP Tracking                           │
│ • Multi-Factor Authentication                              │
│ • Password Strength Validation                             │
├─────────────────────────────────────────────────────────────┤
│                   Framework Layer                          │
├─────────────────────────────────────────────────────────────┤
│ • Secure Password Hashing (Argon2ID)                       │
│ • Timing-Safe Comparisons                                  │
│ • Cryptographically Secure Random Generation               │
│ • Hash Value Object with Algorithm Validation              │
├─────────────────────────────────────────────────────────────┤
│                   Infrastructure Layer                      │
├─────────────────────────────────────────────────────────────┤
│ • HTTPS Enforcement                                         │
│ • Secure Headers (HSTS, CSP)                              │
│ • Database Security & Prepared Statements                  │
│ • Logging & Monitoring                                     │
└─────────────────────────────────────────────────────────────┘

Password Security

Secure Password Hashing

// ✅ Recommended: Argon2ID with high memory cost
$passwordHasher = new PasswordHasher(
    kdf: $keyDerivationFunction,
    defaultAlgorithm: 'argon2id',
    defaultSecurityLevel: PasswordHasher::LEVEL_HIGH  // Production
);

// Security parameters for different levels:
// LEVEL_HIGH:     128 MB memory, 6 iterations, 4 threads
// LEVEL_STANDARD: 64 MB memory,  4 iterations, 3 threads  
// LEVEL_LOW:      32 MB memory,  2 iterations, 2 threads

// ❌ Avoid: Weak algorithms or low security levels in production
$weakHasher = new PasswordHasher(
    kdf: $kdf,
    defaultAlgorithm: 'pbkdf2-sha256',  // Less secure than Argon2ID
    defaultSecurityLevel: PasswordHasher::LEVEL_LOW
);

Password Policy Enforcement

final readonly class PasswordPolicy
{
    public const int MIN_LENGTH = 12;              // Increased from 8
    public const int MIN_COMPLEXITY_TYPES = 3;     // Upper, lower, numbers, special
    public const int MIN_STRENGTH_SCORE = 70;      // High security threshold
    
    public function validatePassword(string $password): PasswordPolicyResult
    {
        $violations = [];
        
        // Length requirement
        if (mb_strlen($password) < self::MIN_LENGTH) {
            $violations[] = "Password must be at least " . self::MIN_LENGTH . " characters";
        }
        
        // Complexity requirements
        $complexityScore = $this->calculateComplexity($password);
        if ($complexityScore < self::MIN_COMPLEXITY_TYPES) {
            $violations[] = "Password must include at least " . self::MIN_COMPLEXITY_TYPES . " character types";
        }
        
        // Entropy check
        if ($this->calculateEntropy($password) < 50) {
            $violations[] = "Password lacks sufficient entropy";
        }
        
        // Dictionary check
        if ($this->isCommonPassword($password)) {
            $violations[] = "Password is too common";
        }
        
        // Personal information check
        if ($this->containsPersonalInfo($password)) {
            $violations[] = "Password should not contain personal information";
        }
        
        return new PasswordPolicyResult(
            isValid: empty($violations),
            violations: $violations,
            strengthScore: $this->calculateStrengthScore($password)
        );
    }
    
    private function calculateEntropy(string $password): float
    {
        $length = strlen($password);
        $charsetSize = 0;
        
        if (preg_match('/[a-z]/', $password)) $charsetSize += 26;
        if (preg_match('/[A-Z]/', $password)) $charsetSize += 26;
        if (preg_match('/[0-9]/', $password)) $charsetSize += 10;
        if (preg_match('/[^A-Za-z0-9]/', $password)) $charsetSize += 32;
        
        return $length * log($charsetSize, 2);
    }
    
    private function isCommonPassword(string $password): bool
    {
        // Check against common password lists (e.g., Have I Been Pwned)
        $commonPasswords = [
            'password', '123456', 'password123', 'admin', 'qwerty',
            'letmein', 'welcome', 'monkey', 'dragon', '123456789'
        ];
        
        return in_array(strtolower($password), $commonPasswords, true);
    }
}

Automatic Password Rehashing

// ✅ Implement automatic rehashing on authentication
public function authenticate(string $identifier, string $password): AuthenticationResult
{
    $user = $this->repository->findByIdentifier($identifier);
    if (!$user || !$this->passwordHasher->verify($password, $user->getHashedPassword())) {
        return AuthenticationResult::failed('Invalid credentials');
    }
    
    // Critical: Check for rehashing need
    if ($this->passwordHasher->needsRehash($user->getHashedPassword())) {
        $newHash = $this->passwordHasher->hash($password);
        $this->repository->updateUserPassword($user->getId(), $newHash);
        
        $this->logger->info('Password automatically rehashed', [
            'user_id' => $user->getId(),
            'old_algorithm' => $user->getHashedPassword()->getAlgorithm(),
            'new_algorithm' => $newHash->getAlgorithm()
        ]);
    }
    
    return AuthenticationResult::success($user);
}

Session Security

Secure Session Management

final readonly class SecureSessionManager
{
    public function __construct(
        private AuthenticationService $authService,
        private SessionSecurity $sessionSecurity
    ) {}
    
    public function createSecureSession(
        User $user,
        IpAddress $ipAddress,
        UserAgent $userAgent
    ): AuthenticationSession {
        // Generate cryptographically secure session ID
        $sessionId = $this->generateSecureSessionId();
        
        // Create session with security attributes
        $session = new AuthenticationSession(
            id: $sessionId,
            userId: $user->getId(),
            ipAddress: $ipAddress,
            userAgent: $userAgent,
            createdAt: new \DateTimeImmutable(),
            expiresAt: new \DateTimeImmutable('+1 hour'),
            lastActivity: new \DateTimeImmutable(),
            securityAttributes: new SessionSecurityAttributes(
                isSecure: true,
                isHttpOnly: true,
                sameSite: 'Strict',
                fingerprint: $this->generateFingerprint($ipAddress, $userAgent)
            )
        );
        
        // Set secure session cookie
        $this->setSecureSessionCookie($sessionId, $session->getExpiresAt());
        
        return $session;
    }
    
    private function generateSecureSessionId(): SessionId
    {
        // Use framework's SessionIdGenerator for consistency
        return $this->sessionIdGenerator->generate();
    }
    
    private function setSecureSessionCookie(SessionId $sessionId, \DateTimeImmutable $expiresAt): void
    {
        setcookie('session_id', $sessionId->toString(), [
            'expires' => $expiresAt->getTimestamp(),
            'path' => '/',
            'domain' => '', // Let browser determine
            'secure' => true,    // HTTPS only
            'httponly' => true,  // No JavaScript access
            'samesite' => 'Strict' // CSRF protection
        ]);
    }
    
    private function generateFingerprint(IpAddress $ipAddress, UserAgent $userAgent): string
    {
        return Hash::sha256(
            $ipAddress->toString() . '|' . 
            $userAgent->toString() . '|' . 
            $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
        )->toString();
    }
}

Session Hijacking Protection

final readonly class SessionHijackingProtection
{
    public function validateSession(
        SessionId $sessionId,
        IpAddress $currentIp,
        UserAgent $currentUserAgent
    ): SessionValidationResult {
        $session = $this->repository->findSessionById($sessionId);
        if (!$session) {
            return SessionValidationResult::invalid('Session not found');
        }
        
        // Check session expiration
        if ($session->isExpired()) {
            $this->repository->deleteSession($sessionId);
            return SessionValidationResult::expired();
        }
        
        // IP address consistency check (configurable)
        if ($this->config->checkIpConsistency) {
            if (!$session->getIpAddress()->equals($currentIp)) {
                $this->logSecurityEvent('session_ip_mismatch', [
                    'session_id' => $sessionId->toString(),
                    'original_ip' => (string) $session->getIpAddress(),
                    'current_ip' => (string) $currentIp
                ]);
                
                // Suspicious activity - invalidate session
                $this->repository->deleteSession($sessionId);
                return SessionValidationResult::suspicious('IP address mismatch');
            }
        }
        
        // User Agent consistency (less strict)
        if (!$this->isUserAgentConsistent($session->getUserAgent(), $currentUserAgent)) {
            $this->logSecurityEvent('session_user_agent_change', [
                'session_id' => $sessionId->toString(),
                'original_ua' => (string) $session->getUserAgent(),
                'current_ua' => (string) $currentUserAgent
            ]);
        }
        
        // Update session activity
        $this->repository->updateSessionActivity($sessionId, $currentIp);
        
        return SessionValidationResult::valid($session);
    }
    
    private function isUserAgentConsistent(UserAgent $original, UserAgent $current): bool
    {
        // Allow minor version changes but detect major browser changes
        $originalBrowser = $this->extractBrowser($original);
        $currentBrowser = $this->extractBrowser($current);
        
        return $originalBrowser === $currentBrowser;
    }
}

Token Security

Remember Token Security with Hash Value Object

final readonly class RememberTokenSecurity
{
    public function createRememberToken(string $userId): RememberToken
    {
        // Generate cryptographically secure token
        $tokenValue = bin2hex(random_bytes(32)); // 256-bit token
        
        // Hash token using Hash Value Object for type safety
        $tokenHash = Hash::sha256($tokenValue);
        
        $rememberToken = new RememberToken(
            hash: $tokenHash,
            userId: $userId,
            createdAt: new \DateTimeImmutable(),
            expiresAt: new \DateTimeImmutable('+30 days'),
            lastUsed: null
        );
        
        $this->repository->storeRememberToken($rememberToken);
        
        // Return token with plain text value (only shown once)
        return $rememberToken->withPlainTextValue($tokenValue);
    }
    
    public function validateRememberToken(string $tokenValue): RememberTokenValidation
    {
        // Hash provided token for comparison
        $tokenHash = Hash::sha256($tokenValue);
        
        $rememberToken = $this->repository->findRememberToken($tokenHash);
        if (!$rememberToken) {
            $this->logSecurityEvent('invalid_remember_token', [
                'token_hash' => $tokenHash->toShort(8) // Only log first 8 chars
            ]);
            
            return RememberTokenValidation::invalid();
        }
        
        // Check expiration
        if ($rememberToken->isExpired()) {
            $this->repository->deleteRememberToken($tokenHash);
            return RememberTokenValidation::expired();
        }
        
        // Update last used timestamp
        $this->repository->updateRememberTokenUsage($tokenHash);
        
        return RememberTokenValidation::valid($rememberToken);
    }
    
    public function rotateRememberToken(Hash $oldTokenHash): RememberToken
    {
        $oldToken = $this->repository->findRememberToken($oldTokenHash);
        if (!$oldToken) {
            throw new SecurityException('Cannot rotate non-existent token');
        }
        
        // Create new token
        $newToken = $this->createRememberToken($oldToken->getUserId());
        
        // Delete old token
        $this->repository->deleteRememberToken($oldTokenHash);
        
        $this->logSecurityEvent('remember_token_rotated', [
            'user_id' => $oldToken->getUserId(),
            'old_token_created' => $oldToken->getCreatedAt()->format('Y-m-d H:i:s')
        ]);
        
        return $newToken;
    }
}

// Benefits of using Hash Value Object:
// ✅ Type safety - tokens can't be confused with other strings
// ✅ Algorithm specification - explicit SHA-256 usage
// ✅ Timing-safe comparison - uses hash_equals() internally
// ✅ Validation - ensures proper hash format and length
// ✅ Framework consistency - follows value object patterns

Rate Limiting & Brute Force Protection

Advanced Rate Limiting

final readonly class AdvancedRateLimiter implements RateLimitService
{
    public function __construct(
        private Cache $cache,
        private Logger $logger,
        private RateLimitConfig $config
    ) {}
    
    public function checkRateLimit(
        IpAddress $ipAddress,
        string $action,
        string $identifier = null
    ): RateLimitResult {
        // Multiple rate limiting strategies
        $checks = [
            $this->checkIpBasedLimit($ipAddress, $action),
            $this->checkIdentifierBasedLimit($identifier, $action),
            $this->checkGlobalLimit($action),
            $this->checkAdaptiveLimit($ipAddress, $action)
        ];
        
        foreach ($checks as $check) {
            if ($check->isLimited()) {
                $this->logRateLimitViolation($ipAddress, $action, $check);
                return $check;
            }
        }
        
        // Record successful attempt
        $this->recordAttempt($ipAddress, $action, $identifier);
        
        return RateLimitResult::allowed();
    }
    
    private function checkAdaptiveLimit(IpAddress $ipAddress, string $action): RateLimitResult
    {
        $suspiciousScore = $this->calculateSuspiciousScore($ipAddress);
        
        // Adaptive limits based on suspicious activity
        $maxAttempts = match (true) {
            $suspiciousScore > 80 => 1,   // Highly suspicious
            $suspiciousScore > 60 => 2,   // Suspicious
            $suspiciousScore > 40 => 3,   // Moderately suspicious
            default => 5                   // Normal
        };
        
        $key = "adaptive_limit:{$action}:" . $ipAddress->toString();
        $attempts = $this->cache->get($key, 0);
        
        if ($attempts >= $maxAttempts) {
            return RateLimitResult::limited(
                reason: 'Adaptive rate limit exceeded',
                retryAfter: 900,
                metadata: ['suspicious_score' => $suspiciousScore]
            );
        }
        
        return RateLimitResult::allowed();
    }
    
    private function calculateSuspiciousScore(IpAddress $ipAddress): int
    {
        $score = 0;
        
        // Check for various suspicious indicators
        if ($this->isFromTorNetwork($ipAddress)) $score += 30;
        if ($this->isFromVpnProvider($ipAddress)) $score += 15;
        if ($this->hasRecentFailures($ipAddress)) $score += 20;
        if ($this->isFromUncommonGeolocation($ipAddress)) $score += 10;
        if ($this->hasRapidRequests($ipAddress)) $score += 25;
        
        return min(100, $score);
    }
    
    private function logRateLimitViolation(
        IpAddress $ipAddress,
        string $action,
        RateLimitResult $result
    ): void {
        $this->logger->warning('Rate limit violation', [
            'ip_address' => (string) $ipAddress,
            'action' => $action,
            'reason' => $result->getReason(),
            'retry_after' => $result->getRetryAfter(),
            'metadata' => $result->getMetadata()
        ]);
    }
}

Account Lockout Protection

final readonly class AccountLockoutService
{
    public function __construct(
        private UserRepository $repository,
        private Logger $logger,
        private NotificationService $notifications
    ) {}
    
    public function handleFailedLogin(string $identifier, IpAddress $ipAddress): void
    {
        $user = $this->repository->findByIdentifier($identifier);
        if (!$user) {
            return; // Don't reveal if user exists
        }
        
        $attempts = $this->repository->incrementFailedAttempts($user->getId());
        
        // Progressive lockout strategy
        $lockoutDuration = $this->calculateLockoutDuration($attempts);
        
        if ($attempts >= 3) {
            $this->repository->lockAccount($user->getId(), $lockoutDuration);
            
            $this->logger->warning('Account locked due to failed attempts', [
                'user_id' => $user->getId(),
                'attempts' => $attempts,
                'lockout_duration' => $lockoutDuration,
                'ip_address' => (string) $ipAddress
            ]);
            
            // Notify user of lockout (rate limited to prevent abuse)
            $this->sendLockoutNotification($user, $attempts, $lockoutDuration);
        }
    }
    
    private function calculateLockoutDuration(int $attempts): int
    {
        // Exponential backoff with jitter
        return match (true) {
            $attempts >= 10 => 3600 + random_int(0, 1800), // 1-2.5 hours
            $attempts >= 7  => 1800 + random_int(0, 900),  // 30-45 minutes
            $attempts >= 5  => 900 + random_int(0, 300),   // 15-20 minutes
            $attempts >= 3  => 300 + random_int(0, 120),   // 5-7 minutes
            default => 0
        };
    }
    
    private function sendLockoutNotification(
        User $user,
        int $attempts,
        int $lockoutDuration
    ): void {
        // Rate limit notifications to prevent spam
        $notificationKey = "lockout_notification:" . $user->getId();
        if ($this->cache->has($notificationKey)) {
            return;
        }
        
        $this->cache->put($notificationKey, true, 300); // 5 minutes
        
        $this->notifications->sendSecurityAlert($user->getEmail(), [
            'type' => 'account_lockout',
            'attempts' => $attempts,
            'lockout_duration_minutes' => ceil($lockoutDuration / 60),
            'timestamp' => new \DateTimeImmutable()
        ]);
    }
}

Multi-Factor Authentication Security

TOTP Implementation with Security Features

final readonly class SecureTotpService
{
    private const int SECRET_LENGTH = 32;    // 160-bit secret
    private const int WINDOW_SIZE = 1;       // Allow 1 time step tolerance
    private const int TIME_STEP = 30;        // 30-second time steps
    
    public function generateSecret(): string
    {
        return Base32::encode(random_bytes(self::SECRET_LENGTH));
    }
    
    public function verify(string $code, string $secret, ?int $timestamp = null): bool
    {
        $timestamp = $timestamp ?? time();
        $timeStep = intval($timestamp / self::TIME_STEP);
        
        // Check current and adjacent time windows to account for clock drift
        for ($i = -self::WINDOW_SIZE; $i <= self::WINDOW_SIZE; $i++) {
            $calculatedCode = $this->calculateTotp($secret, $timeStep + $i);
            
            if (hash_equals($code, $calculatedCode)) {
                // Prevent replay attacks by storing used codes
                $this->markCodeAsUsed($secret, $timeStep + $i);
                return true;
            }
        }
        
        return false;
    }
    
    private function calculateTotp(string $secret, int $timeStep): string
    {
        $binarySecret = Base32::decode($secret);
        $timeBytes = pack('N*', 0) . pack('N*', $timeStep);
        
        $hash = hash_hmac('sha1', $timeBytes, $binarySecret, true);
        $offset = ord($hash[19]) & 0xf;
        
        $code = (
            ((ord($hash[$offset]) & 0x7f) << 24) |
            ((ord($hash[$offset + 1]) & 0xff) << 16) |
            ((ord($hash[$offset + 2]) & 0xff) << 8) |
            (ord($hash[$offset + 3]) & 0xff)
        ) % 1000000;
        
        return str_pad((string) $code, 6, '0', STR_PAD_LEFT);
    }
    
    private function markCodeAsUsed(string $secret, int $timeStep): void
    {
        // Prevent replay attacks
        $key = 'totp_used:' . Hash::sha256($secret . ':' . $timeStep)->toShort(16);
        $this->cache->put($key, true, self::TIME_STEP * 2);
    }
}

Backup Code Security

final readonly class BackupCodeService
{
    private const int CODE_LENGTH = 8;
    private const int CODE_COUNT = 8;
    
    public function generateBackupCodes(string $userId): array
    {
        $codes = [];
        
        for ($i = 0; $i < self::CODE_COUNT; $i++) {
            $code = $this->generateHumanReadableCode();
            $codes[] = $code;
            
            // Store hashed version
            $backupCode = new MfaBackupCode(
                userId: $userId,
                codeHash: Hash::sha256($code),
                createdAt: new \DateTimeImmutable(),
                usedAt: null
            );
            
            $this->repository->storeBackupCode($backupCode);
        }
        
        return $codes;
    }
    
    public function verifyBackupCode(string $userId, string $code): bool
    {
        $codeHash = Hash::sha256($code);
        $backupCode = $this->repository->findBackupCode($userId, $codeHash);
        
        if (!$backupCode || $backupCode->isUsed()) {
            return false;
        }
        
        // Mark as used (one-time use only)
        $this->repository->markBackupCodeAsUsed($backupCode, new \DateTimeImmutable());
        
        return true;
    }
    
    private function generateHumanReadableCode(): string
    {
        // Generate readable codes avoiding confusing characters
        $chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // No 0, 1, I, O
        $code = '';
        
        for ($i = 0; $i < self::CODE_LENGTH; $i++) {
            $code .= $chars[random_int(0, strlen($chars) - 1)];
        }
        
        // Format as XXXX-XXXX for readability
        return substr($code, 0, 4) . '-' . substr($code, 4, 4);
    }
}

Monitoring & Incident Response

Security Event Monitoring

final readonly class SecurityEventMonitor
{
    public function __construct(
        private Logger $securityLogger,
        private AlertingService $alerting,
        private MetricsCollector $metrics
    ) {}
    
    public function recordSecurityEvent(SecurityEvent $event): void
    {
        // Log all security events
        $this->securityLogger->warning('Security event recorded', [
            'event_type' => $event->getType(),
            'severity' => $event->getSeverity(),
            'user_id' => $event->getUserId(),
            'ip_address' => (string) $event->getIpAddress(),
            'user_agent' => (string) $event->getUserAgent(),
            'metadata' => $event->getMetadata(),
            'timestamp' => $event->getTimestamp()->format('Y-m-d H:i:s')
        ]);
        
        // Update security metrics
        $this->metrics->increment('auth.security_events', [
            'type' => $event->getType(),
            'severity' => $event->getSeverity()
        ]);
        
        // Send alerts for critical events
        if ($event->isCritical()) {
            $this->alerting->sendSecurityAlert($event);
        }
        
        // Check for attack patterns
        $this->analyzeForAttackPatterns($event);
    }
    
    private function analyzeForAttackPatterns(SecurityEvent $event): void
    {
        $ipAddress = $event->getIpAddress();
        $recentEvents = $this->getRecentSecurityEvents($ipAddress, minutes: 10);
        
        // Detect brute force attacks
        $failedLogins = array_filter($recentEvents, 
            fn($e) => $e->getType() === 'authentication_failed'
        );
        
        if (count($failedLogins) >= 5) {
            $this->alerting->sendAlert(new BruteForceDetectionAlert(
                ipAddress: $ipAddress,
                attemptCount: count($failedLogins),
                timeWindow: 10
            ));
        }
        
        // Detect credential stuffing
        $uniqueIdentifiers = array_unique(array_map(
            fn($e) => $e->getMetadata()['identifier'] ?? null,
            $failedLogins
        ));
        
        if (count($uniqueIdentifiers) >= 10) {
            $this->alerting->sendAlert(new CredentialStuffingAlert(
                ipAddress: $ipAddress,
                uniqueIdentifiers: count($uniqueIdentifiers)
            ));
        }
    }
}

Automated Response System

final readonly class AutomatedSecurityResponse
{
    public function handleSecurityEvent(SecurityEvent $event): void
    {
        match ($event->getType()) {
            'brute_force_detected' => $this->handleBruteForce($event),
            'credential_stuffing_detected' => $this->handleCredentialStuffing($event),
            'session_hijacking_suspected' => $this->handleSessionHijacking($event),
            'suspicious_login_pattern' => $this->handleSuspiciousLogin($event),
            default => null
        };
    }
    
    private function handleBruteForce(SecurityEvent $event): void
    {
        $ipAddress = $event->getIpAddress();
        
        // Immediately block IP for 1 hour
        $this->firewall->blockIpAddress($ipAddress, duration: 3600);
        
        // Increase monitoring for this IP
        $this->monitoring->increaseWatchLevel($ipAddress);
        
        // Notify security team
        $this->alerting->sendUrgentAlert(new BruteForceBlockAlert($ipAddress));
    }
    
    private function handleSessionHijacking(SecurityEvent $event): void
    {
        $userId = $event->getUserId();
        $sessionId = $event->getMetadata()['session_id'];
        
        // Immediately terminate all user sessions
        $this->authService->logoutAll($userId);
        
        // Require password reset
        $this->userService->requirePasswordReset($userId);
        
        // Send security notification to user
        $user = $this->userRepository->findById($userId);
        $this->notifications->sendSecurityBreach($user->getEmail(), [
            'incident_type' => 'session_hijacking',
            'affected_session' => $sessionId,
            'timestamp' => $event->getTimestamp()
        ]);
    }
}

Compliance & Audit

GDPR Compliance

final readonly class GdprCompliantAuthService
{
    public function processPersonalData(User $user, string $purpose): void
    {
        // Log data processing for GDPR audit trail
        $this->auditLogger->info('Personal data processed', [
            'user_id' => $user->getId(),
            'data_types' => ['email', 'login_timestamps', 'ip_addresses'],
            'processing_purpose' => $purpose,
            'legal_basis' => 'legitimate_interest', // or 'consent'
            'retention_period' => '2_years'
        ]);
    }
    
    public function exportUserData(string $userId): UserDataExport
    {
        $user = $this->repository->findById($userId);
        if (!$user) {
            throw new UserNotFoundException($userId);
        }
        
        return new UserDataExport([
            'personal_data' => [
                'user_id' => $user->getId(),
                'email' => (string) $user->getEmail(),
                'created_at' => $user->getCreatedAt()->format('c'),
                'last_login' => $user->getLastLoginAt()?->format('c')
            ],
            'authentication_data' => [
                'password_last_changed' => $user->getPasswordChangedAt()?->format('c'),
                'mfa_enabled' => $user->hasMfaEnabled(),
                'failed_login_attempts' => $this->repository->getFailedLoginAttempts($userId)
            ],
            'session_data' => $this->getSessionHistory($userId),
            'security_events' => $this->getSecurityEventHistory($userId)
        ]);
    }
    
    public function deleteUserData(string $userId): void
    {
        // GDPR right to erasure (right to be forgotten)
        $this->repository->anonymizeUser($userId);
        $this->repository->deleteAllUserSessions($userId);
        $this->repository->deleteAllUserRememberTokens($userId);
        $this->repository->deleteUserSecurityEvents($userId);
        
        $this->auditLogger->info('User data deleted per GDPR request', [
            'user_id' => $userId,
            'deletion_timestamp' => (new \DateTimeImmutable())->format('c')
        ]);
    }
}

Security Audit Logging

final readonly class SecurityAuditLogger
{
    public function logAuthenticationEvent(string $eventType, array $context): void
    {
        $auditRecord = [
            'event_type' => $eventType,
            'timestamp' => (new \DateTimeImmutable())->format('c'),
            'user_id' => $context['user_id'] ?? null,
            'session_id' => $context['session_id'] ?? null,
            'ip_address' => $context['ip_address'] ?? null,
            'user_agent' => $context['user_agent'] ?? null,
            'success' => $context['success'] ?? false,
            'failure_reason' => $context['failure_reason'] ?? null,
            'risk_score' => $this->calculateRiskScore($context)
        ];
        
        // Store in secure audit log (append-only, tamper-evident)
        $this->auditStorage->append($auditRecord);
        
        // Forward to SIEM if configured
        if ($this->siemForwarder) {
            $this->siemForwarder->forward($auditRecord);
        }
    }
    
    private function calculateRiskScore(array $context): int
    {
        $score = 0;
        
        // Location-based risk
        if (isset($context['ip_address'])) {
            $location = $this->geolocator->locate($context['ip_address']);
            if ($location->isHighRiskCountry()) $score += 25;
            if ($location->isFromTor()) $score += 50;
        }
        
        // Time-based risk
        $hour = (int) date('H');
        if ($hour < 6 || $hour > 22) $score += 10; // Outside business hours
        
        // Failure patterns
        if (!($context['success'] ?? true)) $score += 20;
        
        return min(100, $score);
    }
}