Локальный JWKS: createLocalJWKSet

Работа с JWT в реальных приложениях почти всегда упирается в необходимость проверки подписи токена. Когда система использует асимметричную криптографию (например, RS256, ES256), проверка подписи требует публичного ключа. В современных архитектурах эти ключи публикуются в формате JWKS (JSON Web Key Set).

JWKS представляет собой JSON-документ, содержащий набор публичных ключей. Обычно он доступен по URL (например, https://auth.example.com/.well-known/jwks.json) и обновляется при ротации ключей.

Однако постоянные сетевые запросы к JWKS-endpoint могут стать узким местом. Именно здесь появляется идея локального JWKS — когда набор ключей заранее загружается или хранится в памяти приложения. В библиотеке jose для этого используется функция createLocalJWKSet.


Локальный JWKS — это не динамический источник, а заранее подготовленный объект с ключами, который используется для проверки JWT без сетевых запросов.

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

Основные сценарии использования:

  • микросервисы с фиксированным набором ключей
  • тестовые окружения
  • оффлайн-валидация токенов
  • системы, где ключи распространяются через деплой

Структура JWKS

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

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-1",
      "use": "sig",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

Каждый ключ содержит:

  • kty — тип ключа (RSA, EC и т.д.)
  • kid — идентификатор ключа
  • use — назначение (обычно sig)
  • криптографические параметры (n, e, crv, x, y и т.д.)

createLocalJWKSet: базовая концепция

Функция createLocalJWKSet принимает JWKS-объект и возвращает функцию, которая используется для поиска ключей при верификации JWT.

Ключевой момент: никакого сетевого взаимодействия не происходит.

Простейший пример:

import { createLocalJWKSet, jwtVerify } from 'jose'

const jwks = {
  keys: [
    {
      kty: 'RSA',
      kid: 'example-key-1',
      use: 'sig',
      n: '...',
      e: 'AQAB'
    }
  ]
}

const JWKS = createLocalJWKSet(jwks)

const { payload } = await jwtVerify(token, JWKS)

Как происходит поиск ключа

При верификации JWT библиотека извлекает заголовок токена:

{
  "alg": "RS256",
  "kid": "example-key-1"
}

Далее выполняется:

  1. Извлечение kid
  2. Поиск ключа в локальном JWKS
  3. Проверка соответствия алгоритма (alg)
  4. Возврат подходящего публичного ключа

Если ключ не найден — возникает ошибка верификации.


Поведение при отсутствии kid

Некоторые JWT могут не содержать kid. В этом случае createLocalJWKSet пытается:

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

Это важный момент: локальный JWKS менее гибкий, чем удалённый, поскольку не может запрашивать новые ключи.


Преимущества локального JWKS

Отсутствие сетевых запросов

Вся проверка происходит в памяти, что даёт:

  • минимальную задержку
  • отсутствие зависимости от внешнего сервиса
  • предсказуемость поведения

Повышенная стабильность

При использовании createRemoteJWKSet возможны:

  • таймауты сети
  • недоступность JWKS endpoint
  • задержки при холодных запросах

Локальный JWKS полностью исключает эти проблемы.


Контроль над ключами

Разработчик сам определяет набор доверенных ключей. Это снижает риск:

  • неожиданных ротаций ключей
  • подмены JWKS endpoint
  • ошибок конфигурации внешнего сервиса

Ограничения подхода

Отсутствие автоматической ротации

Если ключи меняются, приложение должно:

  • быть перезапущено
  • или получить обновлённый JWKS вручную

Рост сложности управления

При большом количестве сервисов необходимо:

  • синхронизировать ключи
  • следить за версиями JWKS
  • избегать рассинхронизации

Не подходит для динамических провайдеров

Если identity provider часто меняет ключи, локальный JWKS становится неудобным решением.


Работа с несколькими ключами

Локальный JWKS может содержать несколько ключей одновременно:

const jwks = {
  keys: [
    {
      kty: 'RSA',
      kid: 'key-1',
      use: 'sig',
      n: '...',
      e: 'AQAB'
    },
    {
      kty: 'RSA',
      kid: 'key-2',
      use: 'sig',
      n: '...',
      e: 'AQAB'
    }
  ]
}

const JWKS = createLocalJWKSet(jwks)

В этом случае библиотека автоматически выбирает нужный ключ по kid.


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

Основной сценарий использования — передача результата createLocalJWKSet в jwtVerify.

import { jwtVerify, createLocalJWKSet } from 'jose'

const JWKS = createLocalJWKSet({
  keys: [
    {
      kty: 'EC',
      kid: 'ec-key-1',
      crv: 'P-256',
      x: '...',
      y: '...',
      use: 'sig'
    }
  ]
})

const { payload, protectedHeader } = await jwtVerify(
  token,
  JWKS,
  {
    issuer: 'https://auth.example.com',
    audience: 'api-service'
  }
)

Здесь происходит полная проверка:

  • подпись токена
  • issuer
  • audience
  • срок действия

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

В случае локального JWKS кэширование происходит естественным образом:

  • ключи уже находятся в памяти
  • отсутствует необходимость в повторной загрузке
  • доступ к ключу O(1) при нормальной реализации

Типичные ошибки при использовании

Несовпадение kid

Если токен подписан ключом, которого нет в JWKS:

JWKSNoMatchingKey

Причина почти всегда одна — устаревший набор ключей.


Неверный формат ключа

Ошибки вида:

  • некорректный n или e для RSA
  • отсутствующие обязательные поля
  • неправильный kty

Использование неподходящего алгоритма

Если токен подписан, например, RS256, а ключ EC — проверка завершится ошибкой.


Когда использовать createLocalJWKSet

Подход оправдан в ситуациях:

  • статический набор ключей
  • строгая изоляция от сети
  • embedded-сервисы
  • edge-среды с ограниченным доступом
  • тестирование и CI

Сравнение с удалённым JWKS

Удалённый JWKS:

  • динамическая ротация
  • автоматическое обновление
  • зависимость от сети
  • возможные задержки

Локальный JWKS:

  • фиксированный набор ключей
  • максимальная скорость
  • отсутствие сетевых вызовов
  • ручное обновление

Поведение при масштабировании

В распределённых системах локальный JWKS часто используется в связке с CI/CD:

  • ключи обновляются при деплое
  • контейнеры получают актуальный JWKS
  • исключается runtime-загрузка ключей

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


Внутренние особенности реализации

Внутри jose локальный JWKS:

  • нормализует входной объект
  • индексирует ключи по kid
  • фильтрует ключи по use
  • проверяет совместимость алгоритмов

Это делает поиск ключа детерминированным и быстрым.


Безопасность использования

Локальный JWKS усиливает контроль безопасности за счёт:

  • отсутствия внешних источников ключей во время выполнения
  • предсказуемости набора доверенных ключей
  • исключения MITM-рисков на JWKS endpoint

Однако безопасность полностью зависит от процесса доставки ключей в приложение.