Верификация JWT через JWKS строится вокруг идеи динамического получения публичных ключей, используемых для подписи токенов, и последующей проверки подписи без необходимости заранее хранить ключи в приложении. Такой подход особенно важен в распределённых системах, где ключи регулярно ротируются.
JWT (JSON Web Token) обычно состоит из трёх частей: заголовка, полезной нагрузки и подписи. При использовании асимметричного алгоритма (например, RS256) подпись формируется закрытым ключом, а проверка выполняется соответствующим публичным ключом. Именно здесь появляется JWKS (JSON Web Key Set) — JSON-документ, содержащий набор публичных ключей, опубликованных сервером авторизации.
JWKS представляет собой объект вида:
{
"keys": [
{
"kty": "RSA",
"kid": "abc123",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}
Ключевой элемент — kid (Key ID). Он связывает JWT с
конкретным публичным ключом. В заголовке токена всегда присутствует
соответствующее значение:
{
"alg": "RS256",
"typ": "JWT",
"kid": "abc123"
}
Алгоритм проверки всегда начинается с сопоставления kid
из токена и ключа из JWKS.
В реальных системах JWKS обычно доступен по URL:
https://auth.example.com/.well-known/jwks.json
Работа с библиотекой jsrsasign требует преобразования JWK-ключа в формат, пригодный для проверки подписи.
async function fetchJWKS(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Ошибка загрузки JWKS');
}
return await response.json();
}
После получения набора ключей выполняется выбор нужного по
kid.
function getJWKByKid(jwks, kid) {
return jwks.keys.find(key => key.kid === kid);
}
Если ключ не найден, проверка JWT невозможна, и токен считается недействительным.
Библиотека jsrsasign предоставляет утилиту
KEYUTIL.getKey, которая умеет преобразовывать JWK в объект
ключа.
import { KEYUTIL, KJUR } from 'jsrsasign';
function jwkToPem(jwk) {
return KEYUTIL.getKey(jwk);
}
На выходе получается объект, который может быть использован для проверки подписи JWT.
Основной процесс включает:
kidfunction decodeHeader(jwt) {
const parts = jwt.split('.');
if (parts.length !== 3) {
throw new Error('Некорректный JWT');
}
return JSON.parse(atob(parts[0]));
}
import { KJUR, KEYUTIL } from 'jsrsasign';
async function verifyJwtWithJwks(token, jwksUrl) {
const header = decodeHeader(token);
if (!header.kid) {
throw new Error('Отсутствует kid в JWT');
}
const jwks = await fetchJWKS(jwksUrl);
const jwk = getJWKByKid(jwks, header.kid);
if (!jwk) {
throw new Error('Подходящий ключ не найден в JWKS');
}
const publicKey = KEYUTIL.getKey(jwk);
const isValid = KJUR.jws.JWS.verifyJWT(token, publicKey, {
alg: [header.alg]
});
return isValid;
}
После успешной проверки подписи имеет смысл дополнительно проверить содержимое токена:
function parsePayload(jwt) {
const payload = jwt.split('.')[1];
return JSON.parse(atob(payload));
}
Типовые проверки включают:
exp — срок действияiss — издательaud — аудиторияfunction validateClaims(payload, expectedIssuer, expectedAudience) {
const now = Math.floor(Date.now() / 1000);
if (payload.exp && now > payload.exp) {
throw new Error('JWT истёк');
}
if (payload.iss !== expectedIssuer) {
throw new Error('Неверный issuer');
}
if (payload.aud !== expectedAudience) {
throw new Error('Неверная аудитория');
}
}
Запрос JWKS на каждый токен является неэффективным. Обычно используется кэш:
const jwksCache = {
data: null,
timestamp: 0
};
async function getCachedJWKS(url, ttl = 3600000) {
const now = Date.now();
if (jwksCache.data && now - jwksCache.timestamp < ttl) {
return jwksCache.data;
}
jwksCache.data = await fetchJWKS(url);
jwksCache.timestamp = now;
return jwksCache.data;
}
При ротации ключей возможна ситуация, когда kid из
токена отсутствует в кэше. В таком случае выполняется принудительное
обновление:
async function resolveKey(jwksUrl, kid) {
let jwks = await getCachedJWKS(jwksUrl);
let key = getJWKByKid(jwks, kid);
if (!key) {
jwks = await fetchJWKS(jwksUrl);
jwksCache.data = jwks;
jwksCache.timestamp = Date.now();
key = getJWKByKid(jwks, kid);
}
return key;
}
Если JWT подписан RS256, а в проверке не указан алгоритм, верификация может завершиться неуспешно.
jsrsasign ожидает корректный JWK. Отсутствие полей n или
e делает ключ непригодным.
Попытка проверять JWT без сопоставления kid приводит к
использованию неправильного ключа при ротации.
Подпись подтверждает подлинность токена, но не его актуальность или принадлежность.
jsrsasign предоставляет несколько уровней работы с JWT:
KJUR.jws.JWS.verify)verifyJWT)KEYUTIL)Высокоуровневый метод предпочтителен:
KJUR.jws.JWS.verifyJWT(jwt, key, {
alg: ['RS256']
});
Но при JWKS интеграции ключ всегда динамический, поэтому основная сложность заключается не в проверке, а в управлении ключами.
kidТакая последовательность формирует основу безопасной обработки токенов в клиентских и серверных JavaScript-приложениях.