Выбор между JWT и PASETO — это компромисс между гибкостью и устойчивостью к ошибкам использования. JWT (RFC 7519, обычно в виде JWS) позволяет токену самому назвать свой алгоритм из длинного реестра, и это открыло дорогу классическим ошибкам вроде alg: none и подмены RS256 на HS256. PASETO (Platform-Agnostic SEcurity TOkens) фиксирует криптографию для каждой версии протокола, поэтому токен v4 public всегда использует Ed25519, а токен v4 local — всегда XChaCha20 с BLAKE2b.
Коротко о главном
| JWT (JWS) | PASETO v4 | |
|---|---|---|
| Спецификация | RFC 7519 с RFC 7515/7518 (стандарты IETF) | Спецификация сообщества (paseto-standard/paseto-spec); срок действия черновиков IETF истёк |
| Формат | base64url(header).base64url(claims).base64url(signature) | v4.public. + base64url(сообщение ‖ подпись), необязательный .footer |
| Выбор алгоритма | Заголовок alg из реестра IANA | Задаётся версией и назначением |
| Асимметричная подпись | ES256, RS256, PS256, EdDSA/Ed25519 и другие | v4.public: только Ed25519 (v3.public: ECDSA P-384) |
| Симметричные токены | HS256 (только MAC, утверждения читаемы) или JWE | v4.local: XChaCha20 + BLAKE2b-MAC (шифрование и аутентификация) |
none и подмена алгоритма | Возможны, если проверяющая сторона неправильно настроена | Невыразимы в формате |
| Неявные утверждения (аутентифицируются, но не передаются) | Нет | Да (v3 и v4) |
| Размер примера (замер ниже) | 227 символов | 280 символов |
| Формат ключей | JWK и JWKS | PASERK (k4.public…, k4.local…) |
| Экосистема | Все IdP, OAuth/OIDC, шлюзы, облачные сервисы | Библиотеки на многих языках; в IdP и шлюзах поддержки почти нет |
Различия в устройстве
Проверяющая сторона видит в JWT заголовок, в котором назван алгоритм, и должна решить, доверять ли ему. RFC 7518 регистрирует больше десятка алгоритмов подписи и MAC плюс none, а библиотеки исторически выбирали алгоритм по заголовку, что и вызвало волну ошибок 2015 года: принимались неподписанные токены, а открытые ключи RSA использовались как секреты HMAC (см. HS256 или RS256). RFC 8725 решает эту проблему правилами настройки, которые поддерживает любая современная библиотека, но применить их должно каждое развёртывание.
PASETO заменяет согласование алгоритма версиями. Каждая версия фиксирует один примитив для каждого назначения: v4 использует Ed25519 для токенов public, а для токенов local — шифрование XChaCha20 и MAC на основе BLAKE2b в режиме с ключом; v3 использует алгоритмы NIST (ECDSA P-384, AES-256-CTR с HMAC-SHA-384) для сред, где они требуются. Проверяющую сторону, которая принимает токены v4 public, невозможно обманом заставить принять что-то другое, а ключи типизированы, поэтому ключ для токенов local нельзя использовать как открытый ключ. Кодирование перед аутентификацией (pre-authentication encoding, PAE) связывает версию, назначение, полезную нагрузку, footer и необязательные неявные утверждения (implicit assertions) — например, идентификатор арендатора, который известен обеим сторонам, но не передаётся в токене.
На практике важны ещё два различия. Симметричные токены local в PASETO зашифрованы, а симметричные токены JWT с HS256 только защищены MAC и остаются читаемыми; аналог v4.local в мире JWT — это JWE. Кроме того, у PASETO нет аналога обнаружения ключей через JWKS, так что распространение ключей остаётся на вас.
Один пример в двух форматах
Скрипт ниже (запускался на Node.js 26.10 с paseto 3.1.4 и jose 6.2.13) выпускает одни и те же утверждения с одним ключом Ed25519 в обоих форматах.
import { generateKeyPairSync } from "node:crypto";
import { SignJWT, jwtVerify } from "jose";
import { V4 } from "paseto";
const claims = { sub: "user-42", scope: "orders:read" };
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
// PASETO v4.public: Ed25519 is implied by the version; no alg header exists.
const paseto = await V4.sign(claims, privateKey, { expiresIn: "15m", issuer: "https://auth.example.com" });
const pasetoClaims = await V4.verify(paseto, publicKey, { issuer: "https://auth.example.com" });
// JWT with the same key: the verifier must pin the algorithm itself.
const jwt = await new SignJWT(claims).setProtectedHeader({ alg: "EdDSA" }).setIssuer("https://auth.example.com").setExpirationTime("15m").sign(privateKey);
const { payload } = await jwtVerify(jwt, publicKey, { algorithms: ["EdDSA"], issuer: "https://auth.example.com" });
console.log("PASETO prefix:", paseto.slice(0, 10), "length:", paseto.length, "sub:", pasetoClaims.sub);
console.log("JWT prefix:", jwt.slice(0, 10), "length:", jwt.length, "sub:", payload.sub);
PASETO prefix: v4.public. length: 280 sub: user-42
JWT prefix: eyJhbGciOi length: 227 sub: user-42
Здесь токен PASETO немного длиннее, потому что библиотека добавляет iat и использует метки времени в формате ISO 8601, а JWT — числовые даты; размеры подписей одинаковы. Обратите внимание на параметр algorithms: ["EdDSA"] на стороне JWT: именно эту строку PASETO делает ненужной.
Экосистема и совместимость
JWT встроен в OAuth 2.0 (токены доступа по RFC 9068), ID-токены OpenID Connect, DPoP, SD-JWT, FAPI, удостоверения рабочих нагрузок в облаке и практически в каждый API-шлюз и поставщик удостоверений. Библиотеки есть для любого языка, и их безопасность проверяют уже много лет.
Спецификацию PASETO ведёт на GitHub проект PASETO standard; срок действия более ранних черновиков IETF (Internet-Draft) истёк, и RFC они так и не стали. Библиотеки есть для многих языков, но их качество и охват версий различаются, шлюзы и поставщики удостоверений редко принимают PASETO, а стандартного механизма обнаружения нет. Поэтому выбор PASETO означает, что за издателя, проверяющие стороны и распространение ключей отвечаете вы сами.
Что когда выбирать
- OAuth или OpenID Connect, сторонние потребители, API-шлюзы: JWT со списком разрешённых алгоритмов для каждого ключа.
- Внутренние сервисные токены в системе на нескольких языках, которую вы контролируете: подойдёт любой; PASETO — если для каждого языка есть поддерживаемая библиотека с v4.
- Зашифрованные токены, хранящиеся у клиентов (данные сессии в cookie): PASETO
v4.localпроще, чем JWE; в случае JWT используйте JWE сdirилиA256KWиA256GCM. - Среды с требованиями FIPS: JWT с ES256 или PS256 либо PASETO v3; XChaCha20 и BLAKE2b из v4 не одобрены FIPS.
- Команды, которые только знакомятся с безопасностью токенов: PASETO исключает целый класс ошибок конфигурации; библиотека JWT с безопасными настройками по умолчанию (например,
jose, которая требует, чтобы алгоритм соответствовал типу ключа) сокращает этот разрыв.
Заметки о миграции
- Не пропускайте оба формата через одну проверку: по префиксу (
v4.public.илиeyJ) направляйте токены в разные ветки кода проверки с разными ключами. - Сопоставляйте утверждения явно: PASETO резервирует
iss,sub,aud,exp,nbf,iat,jtiиkid, а даты записывает в формате ISO 8601; JWT использует секунды NumericDate. - Замените JWKS собственным механизмом распространения ключей (строки PASERK, система управления конфигурацией или служба ключей), включая ротацию по
kidв footer токена PASETO. - Оставьте JWT на границах системы: многие системы выдают JWT внешним сторонам, а внутри используют PASETO.
- Если остаётесь на JWT, примените RFC 8725 уже сейчас: закрепите алгоритмы, проверяйте
typ,issиaud, используйте 256-битные секреты HMAC или асимметричные ключи и отклоняйтеnone.