Практика: загрузка JWKS с сервера авторизации

JWKS (JSON Web Key Set) используется для публикации публичных ключей, которыми сервер авторизации подписывает JWT-токены. В контексте Jsrsasign это основной механизм, позволяющий валидировать подписи без хранения секретов на стороне клиента или backend-приложения.

Обычно JWKS доступен по стандартному URL:

https://auth-server.example.com/.well-known/jwks.json

Ответ представляет собой JSON-объект следующего вида:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "abc123",
      "use": "sig",
      "alg": "RS256",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

Важнейшим полем является kid — идентификатор ключа, который используется для сопоставления с заголовком JWT.

Получение JWKS через fetch:

async function loadJWKS(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error("Не удалось загрузить JWKS");
  }
  return await response.json();
}

Разбор структуры ключей

Каждый элемент массива keys представляет собой JWK (JSON Web Key). Для RSA-алгоритмов важны поля:

  • kty — тип ключа (RSA, EC)
  • kid — идентификатор ключа
  • n — модуль RSA
  • e — экспонента
  • alg — алгоритм подписи

Jsrsasign умеет преобразовывать JWK в формат, пригодный для проверки подписи.

Выбор ключа по kid

JWT всегда содержит заголовок с kid:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "abc123"
}

Перед проверкой необходимо извлечь этот идентификатор:

function getKid(token) {
  const header = KJUR.jws.JWS.parse(token).headerObj;
  return header.kid;
}

Далее выбирается соответствующий ключ из JWKS:

function findKey(jwks, kid) {
  return jwks.keys.find(k => k.kid === kid);
}

Интеграция с Jsrsasign

Jsrsasign предоставляет утилиту KEYUTIL.getKey, которая преобразует JWK в публичный ключ:

function importPublicKey(jwk) {
  return KEYUTIL.getKey(jwk);
}

После получения ключа можно выполнять проверку JWT:

function verifyToken(token, publicKey) {
  return KJUR.jws.JWS.verify(token, publicKey, ["RS256"]);
}

Полный процесс проверки выглядит так:

async function verifyJwt(token, jwksUrl) {
  const jwks = await loadJWKS(jwksUrl);

  const kid = getKid(token);
  const jwk = findKey(jwks, kid);

  if (!jwk) {
    throw new Error("Подходящий ключ не найден");
  }

  const publicKey = importPublicKey(jwk);

  return verifyToken(token, publicKey);
}

Кэширование и обновление ключей

JWKS не должен загружаться при каждой проверке токена. Обычно применяется кэширование:

let cachedJWKS = null;
let cacheTime = 0;

async function getJWKSWithCache(url, ttl = 3600000) {
  const now = Date.now();

  if (cachedJWKS && now - cacheTime < ttl) {
    return cachedJWKS;
  }

  cachedJWKS = await loadJWKS(url);
  cacheTime = now;

  return cachedJWKS;
}

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

Обработка ошибок и безопасность

При работе с JWKS важно учитывать несколько критических сценариев:

Если kid отсутствует в JWT, проверка должна быть остановлена, поскольку невозможно однозначно выбрать ключ.

Если ключ с указанным kid не найден, это может означать:

  • устаревший токен
  • компрометацию или несинхронизированную ротацию ключей

Дополнительно необходимо проверять алгоритм подписи:

if (header.alg !== "RS256") {
  throw new Error("Недопустимый алгоритм подписи");
}

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

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

Использование JWKS с несколькими ключами

В реальных системах JWKS содержит несколько активных ключей одновременно. Это связано с ротацией:

{
  "keys": [
    { "kid": "old-key", "kty": "RSA", ... },
    { "kid": "new-key", "kty": "RSA", ... }
  ]
}

Алгоритм проверки всегда зависит от kid, а не от первого элемента массива. Поэтому перебор всех ключей без фильтрации считается ошибочной практикой и может приводить к неверной валидации.

Взаимодействие Jsrsasign с JWK форматом

Jsrsasign поддерживает работу с JWK через внутреннее преобразование:

const keyObj = KEYUTIL.getKey(jwk);

После преобразования объект может использоваться не только для проверки JWT, но и для криптографических операций (например, подписи или шифрования, если это предусмотрено ключом).

Важно, что библиотека ожидает корректную структуру JWK, включая base64url-формат значений n и e. Любые изменения формата приводят к ошибкам при импорте ключа.

Практическая схема валидации токена

Полная схема проверки JWT с использованием JWKS и Jsrsasign включает несколько последовательных шагов:

  • получение токена из запроса
  • извлечение заголовка и kid
  • загрузка JWKS (с кэшем)
  • поиск подходящего ключа
  • преобразование JWK в публичный ключ
  • проверка подписи через Jsrsasign
  • при необходимости проверка claims (exp, iss, aud)

Claims проверяются отдельно:

function validateClaims(payload) {
  const now = Math.floor(Date.now() / 1000);

  if (payload.exp < now) {
    throw new Error("Токен истёк");
  }
}

Jsrsasign отвечает только за криптографическую часть, а бизнес-логика проверки всегда реализуется отдельно.

Обновление ключей при ротации

Сервер авторизации может периодически менять ключи подписи. В таких случаях стандартная стратегия:

  • при ошибке валидации попытаться обновить JWKS
  • повторить проверку один раз
  • при повторной ошибке отклонить токен

Это предотвращает ложные отрицательные результаты при смене ключей в реальном времени.

Оптимизация производительности

При большом количестве запросов критично:

  • держать JWKS в памяти
  • избегать повторных парсингов JWK
  • заранее индексировать ключи по kid

Пример оптимизированного хранения:

function indexJWKS(jwks) {
  return jwks.keys.reduce((acc, key) => {
    acc[key.kid] = key;
    return acc;
  }, {});
}

Доступ становится O(1), что особенно важно при высокой нагрузке.

Типичные ошибки интеграции

На практике чаще всего встречаются следующие проблемы:

  • игнорирование kid и попытка использовать первый ключ
  • отсутствие кэширования JWKS
  • несоответствие алгоритма (RS256 vs HS256)
  • неправильная обработка base64url значений
  • отсутствие проверки iss и aud после криптографической валидации

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