v0.5.14 — вход по токену, JWKS в mock-режиме, production deploy docs

- POST /login-token: принимает реальный JWT (не только clientId)
- auth.js: verifyAnyToken — mock + JWKS (Keycloak Nubes) одновременно
- auth.js: JWKS предзагружается всегда, даже в mock-режиме
- devops-deploy.md: полная production-инструкция для DevOps
- .env.example: production-ready шаблон
- README.md: актуальный (v0.5.13, архитектура, тесты, ссылки)
- .gitignore: очищен от дубликатов
This commit is contained in:
2026-06-03 12:37:10 +03:00
parent 22beaa86a3
commit 38c7adf25d
7 changed files with 396 additions and 143 deletions
+41 -16
View File
@@ -66,16 +66,17 @@ async function initAuth() {
const isOidc = !!(KC_CLIENT_ID && KC_CLIENT_SECRET);
// Всегда предзагружаем JWKS (даже в mock-режиме), чтобы можно было
// проверить реальный Bearer токен от Keycloak без KC_CLIENT_ID/SECRET.
try {
cachedJwks = JSON.parse(await httpGet(OIDC.certs));
console.log('[auth] JWKS loaded:', cachedJwks.keys.length, 'ключей');
} catch (e) {
console.warn('[auth] JWKS preload failed:', e.message, '— только mock-режим');
}
if (isOidc) {
console.log('[auth] OIDC — Keycloak', KC_BASE_URL);
// Предзагрузка JWKS при старте — избегаем задержки на первом запросе.
// При ошибке логируем предупреждение: JWKS будет загружен в exchangeCode().
try {
cachedJwks = JSON.parse(await httpGet(OIDC.certs));
console.log('[auth] JWKS loaded:', cachedJwks.keys.length, 'ключей');
} catch (e) {
console.warn('[auth] JWKS preload failed:', e.message, '— будет загружен при первом запросе');
}
} else {
console.log('[auth] MOCK — local RS256 key');
mockKeyPair = crypto.generateKeyPairSync('rsa', {
@@ -122,7 +123,7 @@ function createMiddleware(isOidc) {
const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
if (bearer) {
try {
const payload = isOidc ? verifyOidcToken(bearer) : verifyMockToken(bearer);
const payload = verifyAnyToken(bearer);
req.user = userFromPayload(payload);
return next();
} catch (e) {
@@ -234,12 +235,36 @@ function verifyOidcToken(token) {
if (!key) throw new Error('No matching RSA sig key in JWKS' + (kid ? ` (kid=${kid})` : ''));
const pem = crypto.createPublicKey({ key, format: 'jwk' }).export({ type: 'spki', format: 'pem' });
// Проверяем issuer и audience — защита от токенов других realm/клиентов (A02).
return jwt.verify(token, pem, {
algorithms: ['RS256'],
issuer: KC_BASE_URL,
audience: KC_CLIENT_ID,
});
// Проверяем issuer — защита от токенов других realm.
// audience проверяем только если задан KC_CLIENT_ID (для OIDC-режима).
const opts = { algorithms: ['RS256'], issuer: KC_BASE_URL };
if (KC_CLIENT_ID) opts.audience = KC_CLIENT_ID;
return jwt.verify(token, pem, opts);
}
/**
* Универсальная верификация: пробует mock, затем OIDC (JWKS).
* Позволяет принимать и mock-токены (тесты, dev), и реальные токены от Keycloak.
*/
function verifyAnyToken(token) {
let lastErr = null;
// 1. Mock (быстрый, локальный)
if (mockKeyPair) {
try {
return jwt.verify(token, mockKeyPair.publicKey, { algorithms: ['RS256'], issuer: ISSUER });
} catch (e) {
lastErr = e;
}
}
// 2. OIDC (JWKS от Keycloak)
if (cachedJwks) {
try {
return verifyOidcToken(token);
} catch (e) {
lastErr = e;
}
}
throw lastErr || new Error('Token verification failed: no verifier available');
}
function userFromPayload(payload) {
@@ -269,7 +294,7 @@ function createBearerMiddleware(isOidc) {
return res.status(401).json({ error: 'Bearer token required' });
}
try {
const payload = isOidc ? verifyOidcToken(bearer) : verifyMockToken(bearer);
const payload = verifyAnyToken(bearer);
req.user = userFromPayload(payload);
return next();
} catch (e) {