Пакет · jwt

Модель работы

JWT — это не шифрование и не секрет. Это подписанное, читаемое утверждение: любой может увидеть, что в нём написано, но только владелец ключа может доказать его подлинность. Держите это в голове — и каждое правило в этой библиотеке становится следствием.

Три точки, три части

Токен — это всего лишь три строки в base64url, соединённые точками:

text
  заголовок  .  payload  .  подпись
  │             │            │
  │             │            └─ доказывает, что первые две части не менялись
  │             └────────────── claims (sub, exp, произвольные поля)
  └──────────────────────────── какой алгоритм подписал (alg, typ, kid)

Раскодируйте первые две части — и вы сможете их прочитать: они не зашифрованы, а лишь закодированы в base64url (не зашифрованы). Самое интересное — подпись: она вычисляется по header.payload с ключом, поэтому изменение хотя бы одного байта в любой из частей делает её недействительной.

JWT подписан, а не запечатан

Никогда не кладите секреты (пароли, номера карт) в payload — любой, у кого есть токен, может прочитать claims. JWT защищает от подмены, а не от чтения.

Единственное правило: ключ для подписи и ключ для проверки

Всё, что касается того, какой ключ куда идёт, сводится к семейству алгоритма:

  • HMAC (HS*) — симметричный: один секрет делает обе работы. Сторона, которая подписывает, и сторона, которая проверяет, используют одну и ту же строку. Хорошо подходит, когда это одна и та же сторона (ваш собственный API, выпускающий токены для себя).
  • RSA / ECDSA (RS* / ES*) — асимметричные: приватный ключ подписывает, публичный ключ проверяет. Приватный ключ вы храните у себя; публичный можете раздать кому угодно. Хорошо подходит, когда другие должны проверять токены, которые выпустили вы (или когда вы проверяете токены, выпущенные провайдером).

Именно это различие — причина, по которой в библиотеке два отдельных типа:

php
use Flytachi\Jwt\Entity\PrivateKey;
use Flytachi\Jwt\Entity\PublicKey;

// Сторона подписи — encode() всегда принимает только PrivateKey
new PrivateKey($secretOrPrivatePem, 'HS256');

// Сторона проверки — decode() всегда принимает только PublicKey
new PublicKey($secretOrPublicPem, 'HS256');

Для HMAC «приватный» и «публичный» ключ хранят одну и ту же секретную строку — имена типов описывают роль, а не то, что материал ключа различается.

Ключ привязан к своему алгоритму

PrivateKey и PublicKey каждый несёт в себе алгоритм, и decode() требует, чтобы алгоритм ключа для проверки совпадал с тем, что назван в заголовке токена. Это не бюрократия — это защита от классической атаки «путаница алгоритмов», при которой злоумышленник берёт ваш публичный ключ RSA (который, что логично, публичен) и пытается использовать его как секрет HMAC. Поскольку ключ привязан к RS256, токен, заявляющий HS256, отклоняется ещё до того, как вообще запустится проверка.

Вот почему

Каждая ошибка «алгоритм должен совпадать» восходит к этой привязке. Вы выбираете алгоритм при создании ключа, и токен не может вас переубедить.

Что происходит при encode()

Мягко, от начала до конца:

  1. Заголовок строится из алгоритма ключа (и его kid, если задан).
  2. Заголовок и payload кодируются в JSON, затем в base64url.
  3. header.payload подписывается приватным ключом — HMAC для HS*, openssl_sign для RS*/ES*.
  4. Подпись кодируется в base64url и добавляется в конец. Вы получаете header.payload.signature.

Что происходит при decode()

  1. Токен разбивается на три части (неверное количество → отклонение).
  2. Читается заголовок, чтобы узнать алгоритм — и, для асимметричных токенов, kid.
  3. Выбирается нужный PublicKey: единственный ключ для HMAC или ключ, совпавший по kid, для RSA/ECDSA.
  4. Алгоритм ключа сверяется с заголовком (привязка, описанная выше).
  5. Проверяется подпись — с помощью hash_equals для HMAC, поэтому сравнение идёт за постоянное время и не утекает никакой информации о таймингах.
  6. Наконец, проверяются временные claims (exp, nbf, iat) с учётом допуска (leeway).

Только если всё это проходит, вы получаете обратно JwtPayload. Подробная версия — преобразование ECDSA DER, сборка PEM из JWKS, защита от атак по таймингам — описана в разделе Модель безопасности.

Когда выбирать какое семейство

HMAC одна сторона, общий секретRSA / ECDSA много проверяющих, публичный ключ
  • HMAC — ваш сервис выпускает токены и сам же их проверяет. Проще всего, быстрее всего.
  • RSA / ECDSA — сторонние участники (или отдельные сервисы) должны проверять токены без вашего секрета для подписи, либо вы проверяете токены провайдера через JWKS.
  • Хранение секретов в payload — он читаем. Используйте claim как идентификатор, а затем подгружайте чувствительные данные на стороне сервера.

Дальше