1
0
Files
breadcrumb-the-shire/lib/Service/Auth/MicrosoftOidcStateStoreService.php
2026-03-06 00:44:52 +01:00

100 lines
2.9 KiB
PHP

<?php
namespace MintyPHP\Service\Auth;
use MintyPHP\Http\SessionStoreInterface;
class MicrosoftOidcStateStoreService
{
private const SESSION_KEY = 'microsoft_oidc_states';
public function __construct(
private readonly SessionStoreInterface $sessionStore,
private readonly int $ttlSeconds = 600,
private readonly int $maxEntries = 50
) {
}
public function store(string $state, array $payload): void
{
$state = trim($state);
if ($state === '') {
return;
}
$states = $this->states();
$states = $this->pruneExpiredStates($states);
$payload['created_at'] = (int) ($payload['created_at'] ?? time());
$states[$state] = $payload;
// Cap size to prevent session bloat from many unfinished authorization flows.
if (count($states) > $this->maxEntries) {
uasort($states, static function (array $a, array $b): int {
return ((int) ($a['created_at'] ?? 0)) <=> ((int) ($b['created_at'] ?? 0));
});
while (count($states) > $this->maxEntries) {
array_shift($states);
}
}
$this->sessionStore->set(self::SESSION_KEY, $states);
}
/**
* @return array{ok:bool,error?:string,entry?:array}
*/
public function consume(string $state): array
{
$state = trim($state);
if ($state === '') {
return ['ok' => false, 'error' => 'state_invalid'];
}
$states = $this->states();
$entry = is_array($states[$state] ?? null) ? $states[$state] : null;
if ($entry === null) {
return ['ok' => false, 'error' => 'state_invalid'];
}
// Remove the state before checking TTL — prevents replay even if the check below rejects it.
unset($states[$state]);
$this->sessionStore->set(self::SESSION_KEY, $states);
$createdAt = (int) ($entry['created_at'] ?? 0);
if ($createdAt <= 0 || (time() - $createdAt) > $this->ttlSeconds) {
return ['ok' => false, 'error' => 'state_expired'];
}
return ['ok' => true, 'entry' => $entry];
}
public function prune(): void
{
$this->sessionStore->set(self::SESSION_KEY, $this->pruneExpiredStates($this->states()));
}
private function states(): array
{
$states = $this->sessionStore->get(self::SESSION_KEY, []);
return is_array($states) ? $states : [];
}
private function pruneExpiredStates(array $states): array
{
$now = time();
foreach ($states as $key => $payload) {
if (!is_array($payload)) {
unset($states[$key]);
continue;
}
$createdAt = (int) ($payload['created_at'] ?? 0);
if ($createdAt <= 0 || ($now - $createdAt) > $this->ttlSeconds) {
unset($states[$key]);
}
}
return $states;
}
}