identifier} account locked after {$this->failedAttempts} failed attempts"; $unlockTime = new \DateTimeImmutable("+{$this->lockDurationMinutes} minutes"); $context = ExceptionContext::forOperation('authentication.account_lock', 'Auth') ->withData([ 'user_identifier' => $this->identifier, 'failed_attempts' => $this->failedAttempts, 'lock_duration_minutes' => $this->lockDurationMinutes, 'unlock_time' => $unlockTime->format('Y-m-d H:i:s'), 'event_identifier' => "authn_account_locked:{$this->identifier},{$this->failedAttempts}", 'category' => 'authentication', 'requires_alert' => true, // Account-Sperrungen sind immer alert-würdig ]) ->withMetadata([ 'security_event' => true, 'owasp_compliant' => true, 'log_level' => 'WARN', 'critical_security_event' => true, ]); parent::__construct( message: $message, context: $context, code: 423, // Locked previous: $previous, errorCode: ErrorCode::AUTH_ACCOUNT_LOCKED, retryAfter: $this->lockDurationMinutes * 60 // Retry nach Sperrzeit in Sekunden ); } // === Factory Methods für verschiedene Sperr-Szenarien === public static function tooManyFailedAttempts(string $identifier, int $attempts = 5): self { return new self($identifier, $attempts, 15); // 15 Minuten Standard-Sperrzeit } public static function suspiciousActivity(string $identifier, int $lockDurationMinutes = 60): self { return new self($identifier, 0, $lockDurationMinutes); } public static function administrativeLock(string $identifier): self { return new self($identifier, 0, 0); // 0 = permanente Sperrung } public static function bruteForceDetected(string $identifier, int $attempts): self { return new self($identifier, $attempts, 120); // 2 Stunden bei Brute Force } /** * Gibt OWASP-konforme Event-Daten zurück */ public function getSecurityEventData(): array { return [ 'event_identifier' => "authn_account_locked:{$this->identifier},{$this->failedAttempts}", 'description' => "User {$this->identifier} account locked after {$this->failedAttempts} failed attempts", 'category' => 'authentication', 'log_level' => 'WARN', 'requires_alert' => true, 'user_identifier' => $this->identifier, 'failed_attempts' => $this->failedAttempts, 'lock_duration_minutes' => $this->lockDurationMinutes, ]; } /** * Prüft ob die Sperrung permanent ist */ public function isPermanentLock(): bool { return $this->lockDurationMinutes === 0; } /** * Berechnet die Entsperrzeit */ public function getUnlockTime(): ?\DateTimeImmutable { if ($this->isPermanentLock()) { return null; // Permanente Sperrung } return new \DateTimeImmutable("+{$this->lockDurationMinutes} minutes"); } /** * Gibt verbleibende Sperrzeit in Sekunden zurück */ public function getRemainingLockTimeSeconds(): ?int { $unlockTime = $this->getUnlockTime(); if ($unlockTime === null) { return null; // Permanente Sperrung } $now = new \DateTimeImmutable(); $diff = $unlockTime->getTimestamp() - $now->getTimestamp(); return max(0, $diff); } /** * Account-Sperrungen erfordern immer Alerts */ public function requiresAlert(): bool { return true; } }