JWKS (JSON Web Key Set) — это публичный HTTP-эндпоинт, возвращающий набор ключей в формате JSON, используемых для проверки подписи JWT. В интеграционных тестах задача состоит не в проверке отдельной функции, а в валидации всей цепочки:
Библиотека jose предоставляет инструменты для всех этих
этапов, включая создание ключей, экспорт в JWK и верификацию JWT с
удалённым JWKS.
Интеграционные тесты требуют изолированной среды. Основные компоненты:
Пример генерации ключевой пары:
import { generateKeyPair } from 'jose'
const { publicKey, privateKey } = await generateKeyPair('RS256')
Экспорт публичного ключа в JWK:
import { exportJWK } from 'jose'
const jwk = await exportJWK(publicKey)
jwk.kid = 'test-key-id'
jwk.alg = 'RS256'
jwk.use = 'sig'
Минимальный сервер на Express:
import express from 'express'
const app = express()
app.get('/.well-known/jwks.json', (req, res) => {
res.json({
keys: [jwk]
})
})
const server = app.listen(3000)
Ключевые требования:
Content-Type: application/jsonkeyskidJWT подписывается приватным ключом:
import { SignJWT } from 'jose'
const token = await new SignJWT({ sub: '123' })
.setProtectedHeader({ alg: 'RS256', kid: 'test-key-id' })
.setIssuedAt()
.setExpirationTime('2h')
.sign(privateKey)
Важно:
kid должен совпадать с ключом в JWKSalg) должен соответствоватьДля верификации используется createRemoteJWKSet:
import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS = createRemoteJWKSet(
new URL('http://localhost:3000/.well-known/jwks.json')
)
Проверка токена:
const { payload } = await jwtVerify(token, JWKS)
jose автоматически кэширует JWKS. Это важно учитывать в
тестах:
Пример контроля:
const JWKS = createRemoteJWKSet(
new URL('http://localhost:3000/.well-known/jwks.json'),
{
cacheMaxAge: 0
}
)
Это отключает кэширование для чистоты теста.
Ожидаемый результат: успешная расшифровка payload.
kidJWT содержит kid, отсутствующий в JWKS:
.setProtectedHeader({ alg: 'RS256', kid: 'wrong-id' })
Ожидается ошибка:
JWKSNoMatchingKeyСервер выключен или URL неверный:
createRemoteJWKSet(new URL('http://localhost:9999/jwks'))
Ожидается ошибка сети.
Ответ сервера:
{ "invalid": true }
Ожидается ошибка парсинга.
.setExpirationTime('1s')
После задержки:
await new Promise(r => setTimeout(r, 2000))
Ожидается:
JWTExpiredИнтеграционные тесты должны быть детерминированными:
kidsinon.useFakeTimers или
аналог)Остановка сервера после теста:
afterAll(() => {
server.close()
})
При параллельном запуске:
Сценарий:
Проверка:
Пример обновления:
let keys = [jwkA]
app.get('/jwks', (req, res) => {
res.json({ keys })
})
// позже
keys = [jwkB]
createRemoteJWKSet поддерживает настройки:
createRemoteJWKSet(url, {
timeoutDuration: 2000
})
Тестируется:
Даже в тестовой среде важно:
RS256,
ES256)JWKS-эндпоинт должен корректно отдавать:
Cache-ControlContent-TypeТест:
const res = await fetch('/jwks')
expect(res.headers.get('content-type')).toContain('application/json')
При ошибках верификации полезно:
kidkid в JWT{
"keys": [key1, key2, key3]
}
Тестируется выбор ключа по kid.
JWKS может содержать:
Важно проверять:
algjwtVerify(token, JWKS, {
issuer: 'https://auth.example.com',
audience: 'api'
})
Тестируются ошибки:
JWTClaimValidationFailedРекомендуемая структура:
tests/
jwks/
setup.js
success.test.js
errors.test.js
rotation.test.js
При большом количестве тестов:
В CI важно:
Для более реалистичных тестов:
Гарантировать, что:
Такой подход исключает ошибки, которые невозможно выявить на уровне unit-тестов, и обеспечивает надёжность аутентификации в распределённых системах.