1
0
Files
breadcrumb-the-shire/lib/Service/Auth/MicrosoftOidcStateStoreService.php

100 lines
2.9 KiB
PHP
Raw Normal View History

2026-02-23 12:58:19 +01:00
<?php
namespace MintyPHP\Service\Auth;
2026-03-06 00:44:52 +01:00
use MintyPHP\Http\SessionStoreInterface;
2026-02-23 12:58:19 +01:00
class MicrosoftOidcStateStoreService
{
private const SESSION_KEY = 'microsoft_oidc_states';
public function __construct(
2026-03-06 00:44:52 +01:00
private readonly SessionStoreInterface $sessionStore,
2026-02-23 12:58:19 +01:00
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;
2026-03-06 00:44:52 +01:00
// Cap size to prevent session bloat from many unfinished authorization flows.
2026-02-23 12:58:19 +01:00
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);
}
}
2026-03-06 00:44:52 +01:00
$this->sessionStore->set(self::SESSION_KEY, $states);
2026-02-23 12:58:19 +01:00
}
/**
* @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'];
}
2026-03-06 00:44:52 +01:00
// Remove the state before checking TTL — prevents replay even if the check below rejects it.
2026-02-23 12:58:19 +01:00
unset($states[$state]);
2026-03-06 00:44:52 +01:00
$this->sessionStore->set(self::SESSION_KEY, $states);
2026-02-23 12:58:19 +01:00
$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
{
2026-03-06 00:44:52 +01:00
$this->sessionStore->set(self::SESSION_KEY, $this->pruneExpiredStates($this->states()));
2026-02-23 12:58:19 +01:00
}
private function states(): array
{
2026-03-06 00:44:52 +01:00
$states = $this->sessionStore->get(self::SESSION_KEY, []);
2026-02-23 12:58:19 +01:00
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;
}
}