Вложенные JWT (Nested JWT) представляют собой композицию токенов, где один JWT инкапсулируется в другой уровень защиты. На практике это чаще всего означает, что подписанный токен (JWS) дополнительно шифруется (JWE), формируя структуру вида «подписать, затем зашифровать». Такой подход позволяет одновременно обеспечить целостность данных и их конфиденциальность при передаче между системами.
Базовая модель JWT опирается на три формата, определённых спецификацией JOSE:
Вложенный JWT строится как комбинация:
JWS → JWE
Сначала создаётся подписанный токен, затем он используется как полезная нагрузка для шифрования.
Итоговая структура:
Таким образом, содержимое защищено дважды:
Существуют два основных паттерна:
Наиболее распространённый вариант:
Этот подход обеспечивает:
Редко используемая схема:
Недостаток заключается в том, что подпись относится уже к зашифрованному содержимому, что снижает гибкость проверки на стороне промежуточных сервисов.
Библиотека jose предоставляет низкоуровневые и высокоуровневые инструменты для работы с JWS и JWE в Node.js и браузере. Основные используемые компоненты:
SignJWT — создание подписанного JWTjwtVerify — проверка подписиCompactEncrypt — создание JWEcompactDecrypt — расшифровка JWEКомбинирование этих примитивов позволяет вручную строить вложенные токены.
Сначала создаётся подписанный токен:
import { SignJWT } from 'jose'
const secretKey = new TextEncoder().encode('access-secret-key')
const jws = await new SignJWT({ userId: 123, role: 'admin' })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('2h')
.sign(secretKey)
Результатом является компактный JWS в формате:
header.payload.signature
Далее подписанный токен используется как payload для шифрования:
import { CompactEncrypt } from 'jose'
const encryptionKey = new TextEncoder().encode('encryption-secret-key')
const jwe = await new CompactEncrypt(
new TextEncoder().encode(jws)
)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.encrypt(encryptionKey)
На этом этапе получается полностью вложенный JWT:
Обратный процесс требует двух шагов: расшифровать и затем проверить подпись.
import { compactDecrypt } from 'jose'
const { plaintext } = await compactDecrypt(jwe, encryptionKey)
const innerJws = new TextDecoder().decode(plaintext)
После этого извлекается исходный JWS.
import { jwtVerify } from 'jose'
const { payload } = await jwtVerify(innerJws, secretKey)
console.log(payload)
Вложенные токены применяются в системах, где требуется разделение ответственности:
Это особенно важно в распределённых системах, где токен проходит через несколько сервисов, но не должен быть читаемым на промежуточных этапах.
Сервис A формирует токен, сервис B расшифровывает и проверяет подпись, сервис C уже не имеет доступа к содержимому без ключа.
Часто используется для защиты транзакционных данных, где утечка payload недопустима даже при перехвате токена.
В сценариях SSO вложенные JWT позволяют разделить:
Критическим аспектом является разделение ключей:
Рекомендуется:
Это снижает уровень безопасности и нарушает принцип разделения обязанностей.
Расшифровка JWE без проверки JWS делает систему уязвимой к подмене данных внутри payload.
Шифрование до подписи часто приводит к невозможности верификации промежуточными сервисами и усложняет архитектуру.
Каждый уровень должен явно задавать алгоритмы:
HS256, RS256, ES256A256GCM, RSA-OAEP,
dirНесогласованность алгоритмов приводит к ошибкам декодирования.
В системах с высокой степенью безопасности возможно многоуровневое вложение:
Хотя технически возможно построение цепочек, каждый дополнительный слой увеличивает:
Поэтому архитектура должна оставаться минимально достаточной.
Библиотека jose не предоставляет отдельного «nested JWT builder», так как концепция вложенности строится композиционно:
SignJWTCompactEncryptcompactDecrypt и jwtVerifyТакой подход даёт гибкость и контроль над каждым уровнем криптографической защиты.