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 — модуль RSAe — экспонентаalg — алгоритм подписиJsrsasign умеет преобразовывать JWK в формат, пригодный для проверки подписи.
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 предоставляет утилиту 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 содержит несколько активных ключей одновременно. Это связано с ротацией:
{
"keys": [
{ "kid": "old-key", "kty": "RSA", ... },
{ "kid": "new-key", "kty": "RSA", ... }
]
}
Алгоритм проверки всегда зависит от kid, а не от первого
элемента массива. Поэтому перебор всех ключей без фильтрации считается
ошибочной практикой и может приводить к неверной валидации.
Jsrsasign поддерживает работу с JWK через внутреннее преобразование:
const keyObj = KEYUTIL.getKey(jwk);
После преобразования объект может использоваться не только для проверки JWT, но и для криптографических операций (например, подписи или шифрования, если это предусмотрено ключом).
Важно, что библиотека ожидает корректную структуру JWK, включая
base64url-формат значений n и e. Любые
изменения формата приводят к ошибкам при импорте ключа.
Полная схема проверки JWT с использованием JWKS и Jsrsasign включает несколько последовательных шагов:
kidexp,
iss, aud)Claims проверяются отдельно:
function validateClaims(payload) {
const now = Math.floor(Date.now() / 1000);
if (payload.exp < now) {
throw new Error("Токен истёк");
}
}
Jsrsasign отвечает только за криптографическую часть, а бизнес-логика проверки всегда реализуется отдельно.
Сервер авторизации может периодически менять ключи подписи. В таких случаях стандартная стратегия:
Это предотвращает ложные отрицательные результаты при смене ключей в реальном времени.
При большом количестве запросов критично:
kidПример оптимизированного хранения:
function indexJWKS(jwks) {
return jwks.keys.reduce((acc, key) => {
acc[key.kid] = key;
return acc;
}, {});
}
Доступ становится O(1), что особенно важно при высокой нагрузке.
На практике чаще всего встречаются следующие проблемы:
kid и попытка использовать первый
ключRS256 vs
HS256)iss и aud после
криптографической валидацииКаждая из этих ошибок может привести к тому, что система либо принимает недействительные токены, либо отклоняет валидные при нормальной работе сервера авторизации