В экосистеме OpenID Connect (OIDC) discovery-документ представляет собой стандартный JSON-ресурс, который описывает конфигурацию провайдера аутентификации. Он публикуется по фиксированному пути:
https://<issuer>/.well-known/openid-configuration
Этот документ служит точкой входа для автоматического получения всех необходимых параметров взаимодействия с провайдером идентификации. Вместо ручной настройки endpoints клиент получает структурированное описание возможностей сервера.
Типичное содержимое discovery-документа включает:
issuer — идентификатор провайдераauthorization_endpoint — endpoint авторизацииtoken_endpoint — endpoint выдачи токеновuserinfo_endpoint — endpoint пользовательской
информацииjwks_uri — URI набора публичных ключей (JWKS)response_types_supported,
subject_types_supported и другие метаданныеОсобое значение в контексте проверки JWT имеет параметр
jwks_uri.
JWKS (JSON Web Key Set) — это структура, содержащая публичные ключи, используемые для проверки цифровых подписей JWT.
Формат JWKS представляет собой JSON:
{
"keys": [
{
"kty": "RSA",
"kid": "abc123",
"use": "sig",
"n": "...",
"e": "AQAB"
}
]
}
Каждый ключ содержит:
kid — идентификатор ключа (Key ID)kty — тип ключа (RSA, EC и т.д.)n, e для
RSA)Поле jwks_uri в discovery-документе указывает, где можно
получить этот набор ключей.
Перед использованием JWKS можно явно получить конфигурацию провайдера:
const issuer = 'https://auth.example.com';
const res = await fetch(`${issuer}/.well-known/openid-configuration`);
const config = await res.json();
const jwksUri = config.jwks_uri;
После этого jwks_uri используется для загрузки
ключей.
Библиотека jose предоставляет встроенные механизмы работы с
удалёнными JWKS через функцию createRemoteJWKSet.
Основная идея заключается в том, что библиотека самостоятельно:
kidimport { jwtVerify, createRemoteJWKSet } from 'jose';
const issuer = 'https://auth.example.com';
// JWKS URI берётся из discovery-документа
const jwksUri = new URL(
'https://auth.example.com/.well-known/jwks.json'
);
const JWKS = createRemoteJWKSet(jwksUri);
const token = 'eyJ...';
const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.example.com',
audience: 'my-client-id'
});
В этом сценарии createRemoteJWKSet принимает URL JWKS и
возвращает функцию-резолвер ключей.
JWT содержит заголовок:
{
"alg": "RS256",
"kid": "key-1"
}
При верификации происходит следующее:
kidkeys[] выбирается ключ с совпадающим
kidЕсли ключ с нужным kid отсутствует, библиотека
инициирует повторную загрузку JWKS.
Важная особенность подхода через createRemoteJWKSet
заключается в том, что библиотека не требует ручного управления ротацией
ключей.
Поведение при изменениях:
Это критично для провайдеров, которые регулярно ротируют ключи подписи.
Полный поток интеграции обычно выглядит так:
const issuer = 'https://auth.example.com';
const configRes = await fetch(`${issuer}/.well-known/openid-configuration`);
const config = await configRes.json();
const JWKS = createRemoteJWKSet(new URL(config.jwks_uri));
Далее JWKS используется во всех проверках JWT.
const { payload } = await jwtVerify(token, JWKS, {
issuer: config.issuer,
audience: 'api-service',
clockTolerance: 5
});
Здесь:
issuer предотвращает подмену токенов между
системамиaudience ограничивает использование токена конкретным
сервисомclockTolerance компенсирует расхождение времени между
системамиТипичные сценарии ошибок:
jwks_urikidПример обработки:
try {
const { payload } = await jwtVerify(token, JWKS);
} catch (err) {
if (err.code === 'ERR_JWKS_NO_MATCHING_KEY') {
// возможно ключ был ротирован
}
throw err;
}
createRemoteJWKSet внутри себя реализует кэширование
ключей, что снижает количество сетевых запросов.
Особенности поведения:
kidДля высоконагруженных систем это критично, так как проверка JWT выполняется на каждом запросе.
В системах с несколькими провайдерами идентификации используется отдельный JWKS для каждого issuer:
const jwksAuth0 = createRemoteJWKSet(new URL('https://auth0.com/.well-known/jwks.json'));
const jwksKeycloak = createRemoteJWKSet(new URL('https://keycloak.local/realms/main/protocol/openid-connect/certs'));
Далее выбор JWKS происходит на основе iss из токена.
При работе с discovery-документом и JWKS важны следующие ограничения:
issuerjwks_uriОсобенно критично не подменять discovery-документ без валидации issuer, так как он определяет всю цепочку доверия.
Хотя библиотека предоставляет автоматизацию, возможен низкоуровневый подход:
const jwksRes = await fetch(config.jwks_uri);
const jwks = await jwksRes.json();
const key = jwks.keys.find(k => k.kid === protectedHeader.kid);
Однако этот вариант требует самостоятельной реализации:
Поэтому в реальных системах он используется редко.