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
This commit is contained in:
@@ -4,57 +4,151 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Http\Session;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\Security\CsrfToken;
|
||||
use App\Framework\Security\CsrfTokenData;
|
||||
use App\Framework\Security\CsrfTokenGenerator;
|
||||
|
||||
final readonly class CsrfProtection
|
||||
{
|
||||
private const int TOKEN_LIFETIME = 3600; //1h
|
||||
private const int TOKEN_LIFETIME = 7200; // 2h - Better UX for long forms
|
||||
private const int MAX_TOKENS_PER_FORM = 3;
|
||||
private const int RE_SUBMIT_WINDOW = 30; // 30 seconds for re-submits
|
||||
private const int USED_TOKEN_CLEANUP_TIME = 300; // 5 minutes
|
||||
|
||||
private Session $session;
|
||||
|
||||
public function __construct(Session $session)
|
||||
{
|
||||
public function __construct(
|
||||
Session $session,
|
||||
private CsrfTokenGenerator $tokenGenerator,
|
||||
private Clock $clock
|
||||
) {
|
||||
$this->session = $session;
|
||||
|
||||
// Sicherstellen, dass CSRF-Bereich existiert
|
||||
if (!$this->session->has(SessionKey::CSRF->value)) {
|
||||
$this->session->set(SessionKey::CSRF->value, []);
|
||||
}
|
||||
// Initialization happens via SessionData now, no need to set here
|
||||
}
|
||||
|
||||
public function generateToken(string $formId): string
|
||||
public function generateToken(string $formId): CsrfToken
|
||||
{
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$csrf = $this->session->get(SessionKey::CSRF->value, []);
|
||||
$token = $this->tokenGenerator->generate();
|
||||
|
||||
if (!isset($csrf[$formId])) {
|
||||
$csrf[$formId] = [];
|
||||
}
|
||||
// Get current CSRF data from session
|
||||
$sessionData = $this->session->getSessionData();
|
||||
$csrfData = $sessionData->csrfData;
|
||||
|
||||
$csrf[$formId][] = [
|
||||
'token' => $token,
|
||||
'created_at' => time()
|
||||
];
|
||||
// Get tokens for this form
|
||||
$formTokens = $csrfData->getFormData($formId);
|
||||
|
||||
$csrf[$formId] = $this->cleanupOldTokens($csrf[$formId]);
|
||||
// Add new token
|
||||
$formTokens[] = CsrfTokenData::create($token, $this->clock)->toArray();
|
||||
|
||||
$this->session->set(SessionKey::CSRF->value, $csrf);
|
||||
// Cleanup old tokens
|
||||
$formTokens = $this->cleanupOldTokens($formTokens);
|
||||
|
||||
// Update CSRF data
|
||||
$newCsrfData = $csrfData->withFormData($formId, $formTokens);
|
||||
|
||||
// Update session with new CSRF data
|
||||
$this->session->updateSessionData(function ($data) use ($newCsrfData) {
|
||||
return $data->withCsrfData($newCsrfData);
|
||||
});
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function validateToken(string $formId, string $token): bool
|
||||
/**
|
||||
* Generates a fresh token and rotates the existing one for a form.
|
||||
* This should be called after successful form submission.
|
||||
*/
|
||||
public function rotateToken(string $formId): CsrfToken
|
||||
{
|
||||
$csrf = $this->session->get(SessionKey::CSRF->value, []);
|
||||
// Invalidate all existing tokens for this form
|
||||
$sessionData = $this->session->getSessionData();
|
||||
$csrfData = $sessionData->csrfData;
|
||||
|
||||
if (!isset($csrf[$formId])) {
|
||||
// Clear tokens for this form
|
||||
$newCsrfData = $csrfData->withFormData($formId, []);
|
||||
|
||||
// Update session
|
||||
$this->session->updateSessionData(function ($data) use ($newCsrfData) {
|
||||
return $data->withCsrfData($newCsrfData);
|
||||
});
|
||||
|
||||
// Generate a fresh token
|
||||
return $this->generateToken($formId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a token exists and is valid (not expired)
|
||||
*/
|
||||
public function isTokenValid(string $formId, CsrfToken $token): bool
|
||||
{
|
||||
$sessionData = $this->session->getSessionData();
|
||||
$csrfData = $sessionData->csrfData;
|
||||
$formTokens = $csrfData->getFormData($formId);
|
||||
|
||||
if (empty($formTokens)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($csrf[$formId] as $index => $tokenData) {
|
||||
if ($tokenData['token'] === $token) {
|
||||
// Token ist gültig - NICHT löschen, nur als "used" markieren
|
||||
$csrf[$formId][$index]['used_at'] = time();
|
||||
$this->session->set(SessionKey::CSRF->value, $csrf);
|
||||
$currentTime = $this->clock->time();
|
||||
|
||||
foreach ($formTokens as $tokenData) {
|
||||
if ($token->equalsString($tokenData['token'])) {
|
||||
$age = $currentTime - $tokenData['created_at'];
|
||||
|
||||
return $age < self::TOKEN_LIFETIME;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of active tokens for a form
|
||||
*/
|
||||
public function getActiveTokenCount(string $formId): int
|
||||
{
|
||||
$sessionData = $this->session->getSessionData();
|
||||
$csrfData = $sessionData->csrfData;
|
||||
|
||||
return count($csrfData->getFormData($formId));
|
||||
}
|
||||
|
||||
public function validateToken(string $formId, CsrfToken $token): bool
|
||||
{
|
||||
$sessionData = $this->session->getSessionData();
|
||||
$csrfData = $sessionData->csrfData;
|
||||
$formTokens = $csrfData->getFormData($formId);
|
||||
|
||||
if (empty($formTokens)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$reSubmitWindow = Duration::fromSeconds(self::RE_SUBMIT_WINDOW);
|
||||
|
||||
foreach ($formTokens as $index => $tokenArray) {
|
||||
$tokenData = CsrfTokenData::fromArray($tokenArray, $this->clock);
|
||||
|
||||
if ($tokenData->matches($token->toString())) {
|
||||
// Check if token was already used and if re-submit window has passed
|
||||
if ($tokenData->usedAt && ! $tokenData->wasUsedRecently($this->clock, $reSubmitWindow)) {
|
||||
return false; // Token was used too long ago
|
||||
}
|
||||
|
||||
// Mark token as used and save back to session
|
||||
$updatedTokenData = $tokenData->markAsUsed($this->clock);
|
||||
$formTokens[$index] = $updatedTokenData->toArray();
|
||||
|
||||
// Update CSRF data with marked token
|
||||
$newCsrfData = $csrfData->withFormData($formId, $formTokens);
|
||||
|
||||
// Update session
|
||||
$this->session->updateSessionData(function ($data) use ($newCsrfData) {
|
||||
return $data->withCsrfData($newCsrfData);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -64,34 +158,21 @@ final readonly class CsrfProtection
|
||||
|
||||
private function cleanupOldTokens(array $tokens): array
|
||||
{
|
||||
$now = time();
|
||||
$lifetime = Duration::fromSeconds(self::TOKEN_LIFETIME);
|
||||
$usedWindow = Duration::fromSeconds(self::USED_TOKEN_CLEANUP_TIME);
|
||||
|
||||
$cleaned = [];
|
||||
|
||||
foreach ($tokens as $tokenData) {
|
||||
// Behalte Token die:
|
||||
// 1. Noch nicht abgelaufen sind
|
||||
// 2. Kürzlich verwendet wurden (für Re-Submits)
|
||||
$age = $now - $tokenData['created_at'];
|
||||
$usedRecently = isset($tokenData['used_at']) &&
|
||||
($now - $tokenData['used_at']) < 300; // 5 Minuten
|
||||
foreach ($tokens as $tokenArray) {
|
||||
$tokenData = CsrfTokenData::fromArray($tokenArray, $this->clock);
|
||||
|
||||
if ($age < self::TOKEN_LIFETIME || $usedRecently) {
|
||||
$cleaned[] = $tokenData;
|
||||
// Keep tokens that are either not expired or were used recently
|
||||
if ($tokenData->shouldKeep($this->clock, $lifetime, $usedWindow)) {
|
||||
$cleaned[] = $tokenArray; // Keep original array format for session storage
|
||||
}
|
||||
}
|
||||
|
||||
// Behalte nur die neuesten N Tokens
|
||||
// Keep only the newest N tokens
|
||||
return array_slice($cleaned, -self::MAX_TOKENS_PER_FORM);
|
||||
}
|
||||
|
||||
// Diese Methode nur für spezielle Fälle behalten (z.B. AJAX)
|
||||
/*public function renderHiddenFields(string $formId): string
|
||||
{
|
||||
$token = $this->generateToken($formId);
|
||||
return sprintf(
|
||||
'<input type="hidden" name="_token" value="%s"><input type="hidden" name="_form_id" value="%s">',
|
||||
htmlspecialchars($token),
|
||||
htmlspecialchars($formId)
|
||||
);
|
||||
}*/
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user