Пакет · jwt

Проверка через JWKS

Провайдеры идентификации публикуют свои публичные ключи в виде JSON Web Key Set (JWKS) по известному URL. Парсер JWK превращает этот документ в карту PublicKey, проиндексированную по kid, так что вы можете проверять токены провайдера — и переживать ротацию ключей — без какого-либо жёсткого кодирования.

Загрузка и разбор набора ключей

JWK::parseKeySet() принимает декодированный массив JWKS и возвращает [kid => PublicKey] — ровно ту форму, которая нужна decode():

verify-idtoken.php
<?php

use Flytachi\Jwt\JWK;
use Flytachi\Jwt\JWT;

// 1. Загружаем JWKS-документ провайдера
$jwksJson = file_get_contents('https://www.googleapis.com/oauth2/v3/certs');
$jwks = json_decode($jwksJson, true);

// 2. Разбираем его в [kid => PublicKey]
$publicKeys = JWK::parseKeySet($jwks);

// 3. Проверяем ID-токен — decode() сопоставляет kid токена с ключом
$payload = JWT::decode($idToken, $publicKeys);

echo $payload->getClaim('email');

Почему ротация kid просто работает

Провайдеры выполняют ротацию ключей и отдают сразу несколько ключей, каждый со своим kid. Поскольку вы передаёте в decode() весь набор, он выбирает тот, которым был подписан токен. Когда провайдер делает ротацию, вы просто заново загружаете набор — без изменений кода.

Кеширование набора ключей

Не загружайте JWKS при каждом запросе — он меняется редко. Кешируйте разобранные ключи и периодически обновляйте их (учитывайте Cache-Control конечной точки, где это возможно):

php
function getProviderKeys(CacheInterface $cache): array
{
  return $cache->remember('google_jwks', 3600, function () {
      $jwks = json_decode(
          file_get_contents('https://www.googleapis.com/oauth2/v3/certs'),
          true
      );
      return JWK::parseKeySet($jwks); // массив PublicKey, проиндексированный по kid
  });
}

Некорректные ключи пропускаются, а не приводят к ошибке

parseKeySet() пропускает любой отдельный ключ, который не может разобрать (и любую запись без kid), и возвращает остальные. Одна плохая запись в документе провайдера не сломает проверку для корректных ключей.

Разбор одного ключа

Если вы уже знаете, какой ключ вам нужен, parseKey() преобразует один объект JWK в PublicKey:

php
use Flytachi\Jwt\JWK;

$publicKey = JWK::parseKey([
  'kty' => 'RSA',
  'alg' => 'RS256',
  'n'   => '0vx7agoebGcQ...',
  'e'   => 'AQAB',
]);

Поддерживаемые типы ключей

Парсер обрабатывает три стандартных типа ключей JWK:

kty Обязательные поля Создаёт
RSA n, e Публичный ключ RSA для проверки RS*
EC crv, x, y Публичный ключ ECDSA (P-256/P-384/P-521) для ES*
oct k Симметричный секрет для проверки HS*

Каждый JWK также должен нести alg (алгоритм) — без него парсер выбрасывает исключение, поскольку итоговый PublicKey привязан к этому алгоритму.

Всегда проверяйте и claims

Действительная подпись доказывает лишь кто её поставил. Для сторонних токенов вы также должны проверить, что токен был предназначен вам и выпущен тем, кого вы ожидаете — проверяйте iss и aud самостоятельно после декодирования:

php
use Flytachi\Jwt\JWTException;

$payload = JWT::decode($idToken, $publicKeys); // подпись + exp/nbf/iat

if ($payload->getClaim('iss') !== 'https://accounts.google.com') {
  throw new JWTException('Unexpected issuer.');
}
if ($payload->getClaim('aud') !== 'my-client-id.apps.googleusercontent.com') {
  throw new JWTException('Token was not issued for this app.');
}

$userId = $payload->getClaim('sub');

exp проверяется, iss/aud — нет

decode() автоматически проверяет временные claims (exp, nbf, iat), но не проверяет iss или aud — они специфичны для приложения. Всегда проверяйте их самостоятельно, когда потребляете токены, которые выпустили не вы.

Связанные материалы