- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
145 lines
4.7 KiB
PHP
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;
|
|
}
|
|
}
|