67 lines
2.4 KiB
PHP
67 lines
2.4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
final class JwtToken
|
|
{
|
|
public static function issue(int $userId, string $sessionId, int $ttlSeconds): string
|
|
{
|
|
$key = AppConfig::required('JWT_SIGNING_KEY');
|
|
if (strlen($key) < 32) {
|
|
throw new RuntimeException('JWT_SIGNING_KEY must be at least 32 bytes');
|
|
}
|
|
$now = time();
|
|
$header = self::base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
|
|
$payload = self::base64UrlEncode(json_encode([
|
|
'iss' => rtrim((string) (getenv('APP_URL') ?: ''), '/'),
|
|
'sub' => (string) $userId,
|
|
'sid' => $sessionId,
|
|
'iat' => $now,
|
|
'exp' => $now + $ttlSeconds,
|
|
'jti' => bin2hex(random_bytes(16)),
|
|
], JSON_UNESCAPED_SLASHES));
|
|
$signingInput = $header . '.' . $payload;
|
|
return $signingInput . '.' . self::base64UrlEncode(hash_hmac('sha256', $signingInput, $key, true));
|
|
}
|
|
|
|
public static function verify(string $token): ?array
|
|
{
|
|
$parts = explode('.', $token);
|
|
if (count($parts) !== 3) {
|
|
return null;
|
|
}
|
|
[$headerPart, $payloadPart, $signaturePart] = $parts;
|
|
$header = json_decode(self::base64UrlDecode($headerPart), true);
|
|
$claims = json_decode(self::base64UrlDecode($payloadPart), true);
|
|
if (!is_array($header) || ($header['alg'] ?? null) !== 'HS256' || !is_array($claims)) {
|
|
return null;
|
|
}
|
|
try {
|
|
$key = AppConfig::required('JWT_SIGNING_KEY');
|
|
} catch (Throwable $exception) {
|
|
return null;
|
|
}
|
|
if (strlen($key) < 32) {
|
|
return null;
|
|
}
|
|
$expected = self::base64UrlEncode(hash_hmac('sha256', $headerPart . '.' . $payloadPart, $key, true));
|
|
if (!hash_equals($expected, $signaturePart) || (int) ($claims['exp'] ?? 0) <= time()) {
|
|
return null;
|
|
}
|
|
if (!ctype_digit((string) ($claims['sub'] ?? '')) || !is_string($claims['sid'] ?? null)) {
|
|
return null;
|
|
}
|
|
return $claims;
|
|
}
|
|
|
|
private static function base64UrlEncode(string $value): string
|
|
{
|
|
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
|
|
}
|
|
|
|
private static function base64UrlDecode(string $value): string
|
|
{
|
|
$decoded = base64_decode(strtr($value, '-_', '+/'), true);
|
|
return $decoded === false ? '' : $decoded;
|
|
}
|
|
}
|