Обработка ошибок при недоступности JWKS-эндпоинта

При использовании асимметричных JWT-алгоритмов (например, RS256) библиотека Jose опирается на JWKS (JSON Web Key Set) для получения публичных ключей, необходимых для проверки подписи токена. JWKS обычно предоставляется через HTTP-эндпоинт авторизационного сервера и является критической зависимостью процесса валидации.

Недоступность JWKS-эндпоинта приводит к тому, что проверка подписи JWT становится невозможной в момент обращения к удалённому источнику ключей. Это создаёт ряд сценариев, которые необходимо обрабатывать на уровне инфраструктуры приложения.


Механика работы JWKS в Jose

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

import { jwtVerify, createRemoteJWKSet } from 'jose'

const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'))

Далее этот объект передаётся в jwtVerify:

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

При каждом неизвестном kid (Key ID) библиотека выполняет HTTP-запрос к JWKS-эндпоинту, чтобы получить актуальный набор ключей.


Сценарии недоступности JWKS

Недоступность может проявляться в нескольких формах:

  • DNS-ошибки при резолвинге домена
  • Таймауты сети
  • HTTP 5xx от авторизационного сервера
  • Полная недоступность сервиса идентификации
  • Блокировка на уровне firewall или прокси

В каждом из этих случаев createRemoteJWKSet выбрасывает ошибку при попытке загрузки ключей.


Типы ошибок в Jose при проблемах с JWKS

Библиотека не абстрагирует сетевые сбои полностью, поэтому ошибки могут приходить в виде:

  • JWKSMultipleMatchingKeys
  • JWKSNoMatchingKey
  • JOSENetworkError
  • JWKSTimeout (при кастомных настройках таймаута через fetch)
  • стандартные ошибки fetch (TypeError, AbortError)

На уровне jwtVerify это часто проявляется как выброшенное исключение без успешной верификации токена.


Настройка устойчивости через AbortController

Ограничение времени запроса к JWKS — базовый механизм защиты от зависаний:

const JWKS = createRemoteJWKSet(
  new URL('https://auth.example.com/.well-known/jwks.json'),
  {
    timeoutDuration: 3000
  }
)

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


Стратегии обработки недоступности JWKS

Локальный кеш ключей

Одна из наиболее устойчивых моделей — кеширование JWKS в памяти или внешнем хранилище.

import { createRemoteJWKSet } from 'jose'

const JWKS = createRemoteJWKSet(
  new URL('https://auth.example.com/.well-known/jwks.json')
)

const keyCache = new Map()

async function cachedVerify(token) {
  try {
    return await jwtVerify(token, JWKS)
  } catch (err) {
    if (isNetworkError(err)) {
      const cached = await verifyWithCachedKeys(token, keyCache)
      if (cached) return cached
    }
    throw err
  }
}

Ключевая идея — отделить криптографическую проверку от сетевой доступности.


Предзагрузка JWKS

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

async function preloadJWKS() {
  const res = await fetch('https://auth.example.com/.well-known/jwks.json')
  const jwks = await res.json()
  return jwks.keys
}

Далее используется локальная проверка через importJWK:

import { importJWK, jwtVerify } from 'jose'

async function verifyWithLocalJWKS(token, jwksKeys) {
  const { header } = JSON.parse(Buffer.from(token.split('.')[0], 'base64').toString())

  const key = jwksKeys.find(k => k.kid === header.kid)
  const cryptoKey = await importJWK(key, 'RS256')

  return jwtVerify(token, cryptoKey)
}

Такой подход полностью исключает зависимость от сети в момент проверки.


Graceful degradation (мягкая деградация)

В некоторых системах допускается временное ослабление требований проверки при недоступности JWKS, но только при наличии дополнительных гарантий (например, внутренние сервисные токены или mTLS).

async function verifyToken(token) {
  try {
    return await jwtVerify(token, JWKS)
  } catch (err) {
    if (isJWKSError(err)) {
      return verifyFromInternalTrustLayer(token)
    }
    throw err
  }
}

Подобная стратегия требует строгого контроля доверенной среды.


Retry-логика с экспоненциальной задержкой

При кратковременных сбоях JWKS-эндпоинта помогает повторная попытка:

async function verifyWithRetry(token, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await jwtVerify(token, JWKS)
    } catch (err) {
      if (!isNetworkError(err)) throw err
      await delay(2 ** i * 100)
    }
  }
  throw new Error('JWKS verification failed after retries')
}

Такая схема особенно эффективна при временных перегрузках авторизационного сервера.


Обработка неконсистентности JWKS

Даже при доступном эндпоинте возможны логические ошибки:

  • удалён ключ с нужным kid
  • ротация ключей произошла раньше времени
  • клиент использует устаревший кеш

Jose в этом случае возвращает ошибки вида JWKSNoMatchingKey, что требует отдельной обработки:

catch (err) {
  if (err.code === 'ERR_JWKS_NO_MATCHING_KEY') {
    await forceRefreshJWKS()
  }
}

Разделение ответственности: проверка подписи и доступность сети

Архитектурно важно разделять:

  • криптографическую верификацию JWT
  • сетевую доставку ключей

createRemoteJWKSet объединяет эти два слоя, но в production-системах часто требуется их разъединение через:

  • локальные зеркала JWKS
  • периодическую синхронизацию
  • централизованный key management service

Ограничение зависимости от JWKS в критических системах

В системах с высокой доступностью применяются следующие подходы:

  • репликация JWKS в несколько регионов
  • CDN-кеширование .well-known/jwks.json
  • хранение активных ключей в памяти процесса
  • fallback на заранее загруженные ключи при старте
  • отказ от динамической загрузки в runtime

Поведение при полном отказе JWKS

Если JWKS полностью недоступен и отсутствуют локальные ключи, поведение системы сводится к безопасному отказу:

  • JWT считается недействительным
  • доступ к защищённым ресурсам блокируется
  • логируется причина отказа (network / jwks fetch failure)
catch (err) {
  logger.error({
    message: 'JWT verification failed',
    reason: err.message,
    code: err.code
  })

  throw new AuthenticationError()
}

Практика изоляции JWKS-запросов

Для уменьшения влияния сетевых проблем JWKS-запросы часто выносятся в отдельный слой:

  • отдельный модуль key-provider
  • отдельный кеш-слой
  • изолированный HTTP-клиент с таймаутами
  • метрики доступности JWKS
class JwksProvider {
  constructor(url) {
    this.jwks = createRemoteJWKSet(new URL(url), {
      timeoutDuration: 2000
    })
  }

  async getKey(token) {
    return this.jwks(token)
  }
}

Наблюдаемость и диагностика

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

  • количество fallback-ошибок
  • latency JWKS-запросов
  • частоту cache miss по kid
  • долю успешных локальных проверок

Логи уровня security должны содержать:

  • kid токена
  • источник ключа (remote/cache/local)
  • тип ошибки при отказе

Поведение в распределённых системах

В микросервисной архитектуре JWKS часто становится единым источником доверия. При его недоступности возможны каскадные отказы, поэтому применяются:

  • локальные копии JWKS на каждом сервисе
  • асинхронная синхронизация ключей
  • versioned keys
  • независимость от runtime-fetch в критических сервисах

Такая модель снижает вероятность полного отказа аутентификации при сетевых сбоях авторизационного сервера.