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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -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)
);
}*/
}