JWKS (JSON Web Key Set) представляет собой JSON-документ, содержащий набор криптографических ключей в формате JWK (JSON Web Key). Используется для публикации публичных ключей, применяемых при проверке JWT.
Базовая структура JWKS:
{
"keys": [
{
"kty": "RSA",
"kid": "key-1",
"use": "sig",
"alg": "RS256",
"n": "modulus",
"e": "exponent"
}
]
}
Каждый элемент массива keys описывает отдельный ключ.
Наиболее важные поля:
kty — тип ключа (RSA, EC и др.)kid — уникальный идентификатор ключаuse — назначение (обычно sig для
подписи)alg — алгоритмn и e — параметры RSA публичного
ключаВ библиотеке jose генерация и экспорт ключей выполняется
через WebCrypto API-обёртки.
import { generateKeyPair, exportJWK } from 'jose';
const { publicKey, privateKey } = await generateKeyPair('RS256');
const publicJwk = await exportJWK(publicKey);
const privateJwk = await exportJWK(privateKey);
publicJwk.kid = 'key-1';
publicJwk.use = 'sig';
publicJwk.alg = 'RS256';
После экспорта JWK необходимо дополнить метаданными, которые не всегда автоматически добавляются.
JWKS создаётся как контейнер для одного или нескольких JWK:
const jwks = {
keys: [publicJwk]
};
При ротации ключей структура расширяется:
const jwks = {
keys: [
publicJwk_v1,
publicJwk_v2
]
};
Каждый ключ должен иметь уникальный kid, иначе
невозможно определить, какой ключ использовать для проверки подписи.
При проверке JWT сначала извлекается заголовок токена:
function decodeHeader(token) {
const [header] = token.split('.');
return JSON.parse(Buffer.from(header, 'base64url').toString());
}
Далее извлекается kid:
const header = decodeHeader(token);
const kid = header.kid;
Поиск ключа в JWKS:
function findJwk(jwks, kid) {
return jwks.keys.find(key => key.kid === kid);
}
Для проверки подписи JWK должен быть импортирован в формат, который
понимает jose:
import { importJWK } from 'jose';
const jwk = findJwk(jwks, kid);
const publicKey = await importJWK(jwk, 'RS256');
После получения ключа выполняется проверка токена:
import { jwtVerify } from 'jose';
const { payload } = await jwtVerify(token, publicKey);
Если подпись не совпадает, выбрасывается исключение, а если ключ с
указанным kid отсутствует — процесс проверки прерывается на
этапе поиска.
Последовательность шагов без автоматических помощников:
const header = decodeHeader(token);
const jwk = findJwk(jwks, header.kid);
if (!jwk) {
throw new Error('Key not found');
}
const publicKey = await importJWK(jwk, 'RS256');
const { payload } = await jwtVerify(token, publicKey);
При наличии нескольких ключей важно учитывать:
kid обязателен для всех ключейТипичная схема ротации:
const jwks = {
keys: [
{ kid: 'old-key', ... },
{ kid: 'current-key', ... }
]
};
В случаях, когда ключи хранятся вне библиотеки:
const jwks = {
keys: [
{
kty: 'RSA',
kid: 'manual-key',
use: 'sig',
alg: 'RS256',
n: '<base64url-modulus>',
e: 'AQAB'
}
]
};
Такая структура требует корректного кодирования n и
e в base64url без padding.
Перед использованием ключа важно проверять совместимость:
alg в JWT должен совпадать с alg в
JWKkty должен соответствовать алгоритму (RSA для
RS256)Помимо kid возможно использование thumbprint —
криптографического отпечатка ключа:
import { calculateJwkThumbprint } from 'jose';
const thumbprint = await calculateJwkThumbprint(publicJwk);
Thumbprint позволяет детектировать изменения ключа даже при
одинаковом kid.
Типичные ошибки:
kid в JWTn или eОбработка:
try {
const jwk = findJwk(jwks, kid);
const key = await importJWK(jwk, 'RS256');
await jwtVerify(token, key);
} catch (e) {
// токен недействителен
}
В некоторых системах допускается поиск без kid:
function findAnyKey(jwks) {
return jwks.keys[0];
}
Такой подход используется только при отсутствии ротации ключей и считается упрощённым режимом, не подходящим для распределённых систем.