Files
michaelschiemer/src/Framework/Exception/Authentication/AccountLockedException.php
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

145 lines
4.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Exception\Authentication;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
/**
* Ausnahme für gesperrte Benutzerkonten
*
* Verwendet OWASP-konforme Nachrichten für Account-Sperrungen
*/
final class AccountLockedException extends FrameworkException
{
/**
* @param string $identifier Benutzer-Identifikator
* @param int $failedAttempts Anzahl der Fehlversuche die zur Sperrung führten
* @param int $lockDurationMinutes Dauer der Sperrung in Minuten
* @param \Throwable|null $previous Vorherige Ausnahme
*/
public function __construct(
public readonly string $identifier,
public readonly int $failedAttempts,
public readonly int $lockDurationMinutes = 15,
?\Throwable $previous = null
) {
// OWASP-konforme Nachricht mit Platzhaltern
$message = "User {$this->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;
}
}