При использовании асимметричных JWT-алгоритмов (например, RS256) библиотека Jose опирается на JWKS (JSON Web Key Set) для получения публичных ключей, необходимых для проверки подписи токена. JWKS обычно предоставляется через HTTP-эндпоинт авторизационного сервера и является критической зависимостью процесса валидации.
Недоступность JWKS-эндпоинта приводит к тому, что проверка подписи JWT становится невозможной в момент обращения к удалённому источнику ключей. Это создаёт ряд сценариев, которые необходимо обрабатывать на уровне инфраструктуры приложения.
В типичном сценарии используется 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-эндпоинту, чтобы получить актуальный набор
ключей.
Недоступность может проявляться в нескольких формах:
В каждом из этих случаев createRemoteJWKSet выбрасывает
ошибку при попытке загрузки ключей.
Библиотека не абстрагирует сетевые сбои полностью, поэтому ошибки могут приходить в виде:
JWKSMultipleMatchingKeysJWKSNoMatchingKeyJOSENetworkErrorJWKSTimeout (при кастомных настройках таймаута через
fetch)fetch (TypeError, AbortError)На уровне jwtVerify это часто проявляется как
выброшенное исключение без успешной верификации токена.
Ограничение времени запроса к JWKS — базовый механизм защиты от зависаний:
const JWKS = createRemoteJWKSet(
new URL('https://auth.example.com/.well-known/jwks.json'),
{
timeoutDuration: 3000
}
)
При недоступности сервиса быстрее срабатывает ошибка, позволяя системе переключиться на альтернативную стратегию.
Одна из наиболее устойчивых моделей — кеширование 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
}
}
Ключевая идея — отделить криптографическую проверку от сетевой доступности.
При старте приложения можно загружать ключи заранее:
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)
}
Такой подход полностью исключает зависимость от сети в момент проверки.
В некоторых системах допускается временное ослабление требований проверки при недоступности JWKS, но только при наличии дополнительных гарантий (например, внутренние сервисные токены или mTLS).
async function verifyToken(token) {
try {
return await jwtVerify(token, JWKS)
} catch (err) {
if (isJWKSError(err)) {
return verifyFromInternalTrustLayer(token)
}
throw err
}
}
Подобная стратегия требует строгого контроля доверенной среды.
При кратковременных сбоях 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')
}
Такая схема особенно эффективна при временных перегрузках авторизационного сервера.
Даже при доступном эндпоинте возможны логические ошибки:
kidJose в этом случае возвращает ошибки вида
JWKSNoMatchingKey, что требует отдельной обработки:
catch (err) {
if (err.code === 'ERR_JWKS_NO_MATCHING_KEY') {
await forceRefreshJWKS()
}
}
Архитектурно важно разделять:
createRemoteJWKSet объединяет эти два слоя, но в
production-системах часто требуется их разъединение через:
В системах с высокой доступностью применяются следующие подходы:
.well-known/jwks.jsonЕсли JWKS полностью недоступен и отсутствуют локальные ключи, поведение системы сводится к безопасному отказу:
catch (err) {
logger.error({
message: 'JWT verification failed',
reason: err.message,
code: err.code
})
throw new AuthenticationError()
}
Для уменьшения влияния сетевых проблем JWKS-запросы часто выносятся в отдельный слой:
class JwksProvider {
constructor(url) {
this.jwks = createRemoteJWKSet(new URL(url), {
timeoutDuration: 2000
})
}
async getKey(token) {
return this.jwks(token)
}
}
При работе с JWKS критично отслеживать:
kidЛоги уровня security должны содержать:
kid токенаВ микросервисной архитектуре JWKS часто становится единым источником доверия. При его недоступности возможны каскадные отказы, поэтому применяются:
Такая модель снижает вероятность полного отказа аутентификации при сетевых сбоях авторизационного сервера.