# 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 ```php // ✅ 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 ```php 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 ```php // ✅ 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 ```php 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 ```php 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 ```php 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 ```php 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 ```php 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 ```php 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 ```php 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 ```php 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 ```php 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 ```php 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 ```php 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); } }