JARM (JWT Secured Authorization Response Mode) представляет собой механизм защиты ответов авторизации в OAuth 2.0 / OpenID Connect, при котором результат авторизации передаётся клиенту не в виде набора query-параметров, а в виде подписанного JWT. Это позволяет гарантировать целостность данных ответа, защититься от подмены параметров и упростить обработку результата на стороне клиента.
В классическом OAuth2 редирект после авторизации содержит параметры:
codestateerrorerror_descriptionЭти значения передаются в URL и могут быть перехвачены или модифицированы в промежуточной среде.
JARM заменяет этот механизм единым объектом:
responseПример редиректа:
https://client.example.com/callback?response=eyJhbGciOi...
После декодирования это JWT, содержащий стандартные OAuth2/OpenID поля.
Payload обычно включает:
iss — issuer (сервер авторизации)aud — client_id клиентаexp — время истеченияiat — время выпускаcode — authorization code (если используется
Authorization Code Flow)state — защита от CSRFiss или iss/sub в зависимости
от профиляerror / error_description при ошибках
авторизацииПример payload:
{
"iss": "https://auth.example.com",
"aud": "client_123",
"exp": 1714750000,
"iat": 1714746400,
"code": "SplxlOBeZQQYbYS6WxSbIA",
"state": "af0ifjsldkj",
"iss": "https://auth.example.com"
}
Основные гарантии:
Ключевая идея: OAuth response становится криптографически защищённым контейнером.
Библиотека jose предоставляет реализацию всех механизмов JOSE:
На стороне Authorization Server формируется JWT:
import { SignJWT } from 'jose'
const secret = new TextEncoder().encode('super-secret-signing-key')
const payload = {
code: 'SplxlOBeZQQYbYS6WxSbIA',
state: 'af0ifjsldkj'
}
const jwt = await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuer('https://auth.example.com')
.setAudience('client_123')
.setIssuedAt()
.setExpirationTime('5m')
.sign(secret)
console.log(jwt)
Этот JWT затем передаётся клиенту как response.
Клиент должен валидировать:
import { jwtVerify } from 'jose'
const secret = new TextEncoder().encode('super-secret-signing-key')
const { payload } = await jwtVerify(token, secret, {
issuer: 'https://auth.example.com',
audience: 'client_123'
})
console.log(payload.code)
console.log(payload.state)
Любое несоответствие приводит к выбросу исключения, что делает невозможной обработку поддельного ответа.
В production JARM почти всегда использует асимметричную криптографию.
import { SignJWT, importPKCS8 } from 'jose'
const privateKey = await importPKCS8(PKCS8_KEY, 'RS256')
const jwt = await new SignJWT({ code, state })
.setProtectedHeader({ alg: 'RS256' })
.setIssuer('https://auth.example.com')
.setAudience('client_123')
.setExpirationTime('5m')
.sign(privateKey)
import { jwtVerify, importSPKI } from 'jose'
const publicKey = await importSPKI(SPKI_KEY, 'RS256')
const { payload } = await jwtVerify(token, publicKey, {
issuer: 'https://auth.example.com',
audience: 'client_123'
})
Использование публичного ключа исключает необходимость хранения секрета на клиенте.
JARM также инкапсулирует ошибки OAuth:
{
"error": "access_denied",
"error_description": "User denied consent",
"state": "af0ifjsldkj"
}
Проверка выполняется так же, как для успешного ответа:
try {
const { payload } = await jwtVerify(token, publicKey)
if (payload.error) {
throw new Error(payload.error_description)
}
} catch (err) {
console.error('JARM validation failed', err)
}
Если требуется скрыть содержимое ответа, используется JWE.
Пример создания зашифрованного JARM:
import { EncryptJWT } from 'jose'
const secret = new TextEncoder().encode('encryption-key')
const jwt = await new EncryptJWT({ code, state })
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setIssuedAt()
.setExpirationTime('5m')
.encrypt(secret)
Расшифровка:
import { jwtDecrypt } from 'jose'
const { payload } = await jwtDecrypt(token, secret)
console.log(payload.code)
Даже при использовании JARM параметр state остаётся
критически важным.
Алгоритм проверки:
state перед редиректомstateif (payload.state !== storedState) {
throw new Error('Invalid state parameter')
}
Без проверки aud возможна подмена JWT от другого
клиента.
Устаревшие ответы могут быть повторно использованы.
Это нарушает модель доверия и делает систему уязвимой.
Позволяет принять токен от стороннего Authorization Server.
Authorization Server:
Client Application:
responseJARM является расширением OpenID Connect и используется в сценариях:
Библиотека jose фактически является стандартным инструментом для реализации:
Она обеспечивает корректную реализацию криптографических примитивов без необходимости писать собственные реализации алгоритмов, что критично для безопасности OAuth-инфраструктуры.