44 lines
1.4 KiB
PHP
44 lines
1.4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
final class CsrfGuard
|
|
{
|
|
public function __construct(private string $secret, private string $username, private string $sessionId)
|
|
{
|
|
$this->sessionId = $this->sessionId !== '' ? $this->sessionId : 'no_session';
|
|
}
|
|
|
|
/**
|
|
* @return array{ts:int,sid:string,token:string}
|
|
*/
|
|
public function issue(string $intent): array
|
|
{
|
|
$ts = time();
|
|
return ['ts' => $ts, 'sid' => $this->sessionId, 'token' => $this->buildToken($intent, $ts, $this->sessionId)];
|
|
}
|
|
|
|
public function validate(string $intent, string $timestamp, string $token, int $ttl = 3600, string $sessionHint = ''): bool
|
|
{
|
|
if (!preg_match('/^[0-9]{1,20}$/', $timestamp)) {
|
|
return false;
|
|
}
|
|
$ts = (int)$timestamp;
|
|
$now = time();
|
|
if ($ts <= 0 || $ts > $now + 30 || $now - $ts > $ttl) {
|
|
return false;
|
|
}
|
|
$sids = array_values(array_unique(array_filter([$sessionHint, $this->sessionId, 'no_session'], static fn ($v): bool => $v !== '')));
|
|
foreach ($sids as $sid) {
|
|
if (hash_equals($this->buildToken($intent, $ts, $sid), trim($token))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private function buildToken(string $intent, int $timestamp, string $sid): string
|
|
{
|
|
return hash_hmac('sha256', implode('|', [$this->username, $sid, $intent, (string)$timestamp]), $this->secret);
|
|
}
|
|
}
|