JARM: подписанный ответ авторизации

JARM (JWT Secured Authorization Response Mode) представляет собой механизм защиты ответов авторизации в OAuth 2.0 / OpenID Connect, при котором результат авторизации передаётся клиенту не в виде набора query-параметров, а в виде подписанного JWT. Это позволяет гарантировать целостность данных ответа, защититься от подмены параметров и упростить обработку результата на стороне клиента.

В классическом OAuth2 редирект после авторизации содержит параметры:

  • code
  • state
  • error
  • error_description

Эти значения передаются в URL и могут быть перехвачены или модифицированы в промежуточной среде.

JARM заменяет этот механизм единым объектом:

  • весь ответ авторизации упаковывается в JWT
  • JWT подписывается (JWS), а при необходимости шифруется (JWE)
  • клиент получает только один параметр, например response

Пример редиректа:

https://client.example.com/callback?response=eyJhbGciOi...

После декодирования это JWT, содержащий стандартные OAuth2/OpenID поля.

Структура JARM JWT

Payload обычно включает:

  • iss — issuer (сервер авторизации)
  • aud — client_id клиента
  • exp — время истечения
  • iat — время выпуска
  • code — authorization code (если используется Authorization Code Flow)
  • state — защита от CSRF
  • iss или 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"
}

Модель безопасности JARM

Основные гарантии:

  • целостность ответа (подпись JWT)
  • невозможность подмены параметров в URL
  • защита от MITM-изменений
  • возможность шифрования чувствительных данных (JWE)
  • унификация обработки ответа

Ключевая идея: OAuth response становится криптографически защищённым контейнером.


Работа с JARM через библиотеку jose

Библиотека jose предоставляет реализацию всех механизмов JOSE:

  • JWS (подпись)
  • JWE (шифрование)
  • JWT (работа с токенами)

Подписание JARM ответа

На стороне 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.


Проверка JARM ответа на клиенте

Клиент должен валидировать:

  • подпись
  • issuer
  • audience
  • срок действия
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)

Любое несоответствие приводит к выбросу исключения, что делает невозможной обработку поддельного ответа.


Использование асимметричных ключей (RS256 / ES256)

В production JARM почти всегда использует асимметричную криптографию.

Подпись (Authorization Server)

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)

Проверка (Client)

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

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)
}

Шифрование JARM (JWE)

Если требуется скрыть содержимое ответа, используется 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)

Проверка state и защита от CSRF

Даже при использовании JARM параметр state остаётся критически важным.

Алгоритм проверки:

  1. Клиент сохраняет state перед редиректом
  2. После получения JARM извлекает state
  3. Сравнивает значения
if (payload.state !== storedState) {
  throw new Error('Invalid state parameter')
}

Типовые ошибки при реализации JARM

1. Отсутствие проверки audience

Без проверки aud возможна подмена JWT от другого клиента.

2. Игнорирование exp

Устаревшие ответы могут быть повторно использованы.

3. Использование симметричного ключа на клиенте

Это нарушает модель доверия и делает систему уязвимой.

4. Отсутствие валидации issuer

Позволяет принять токен от стороннего Authorization Server.


Практическая архитектура использования JARM

  • Authorization Server:

    • генерирует JWT
    • подписывает (и при необходимости шифрует)
    • возвращает через redirect
  • Client Application:

    • извлекает response
    • валидирует подпись
    • проверяет стандартные claims
    • извлекает OAuth параметры

Связь JARM и OpenID Connect

JARM является расширением OpenID Connect и используется в сценариях:

  • высокозащищённые финансовые системы
  • корпоративные SSO
  • API с повышенными требованиями к целостности
  • мобильные приложения с риском перехвата redirect URI

Роль jose в экосистеме JARM

Библиотека jose фактически является стандартным инструментом для реализации:

  • JWS (подпись JARM)
  • JWE (шифрование JARM)
  • JWT validation (проверка ответа)

Она обеспечивает корректную реализацию криптографических примитивов без необходимости писать собственные реализации алгоритмов, что критично для безопасности OAuth-инфраструктуры.