v0.5.13: подготовка OIDC (Keycloak) — роуты готовы, ждут DevOps

- src/routes/oidc.js: /login, /callback, /logout
  Активны ТОЛЬКО при KC_CLIENT_ID+KC_CLIENT_SECRET
  Сейчас (mock) — пустой router, запросы уходят в UI
- server.js: подключены OIDC-роуты
- docs/deploy-keycloak.md: что нужно от DevOps для включения
This commit is contained in:
2026-06-03 06:12:22 +03:00
parent 9344eb57fb
commit ba0779db51
3 changed files with 166 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const { Router } = require('express');
function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, authLimiter }) {
const router = Router();
// ── Если OIDC не активен — пропускаем запросы к следующему роуту (UI) ──
if (!auth.isOidc) {
// В mock-режиме /login, /callback, /logout обрабатывает ui/routes/auth.js
// Просто не регистрируем обработчики — запрос уйдёт дальше по middleware chain
return router;
}
// ═════════════════════════════════════════════════════════════════════════
// OIDC-режим — только когда есть KC_CLIENT_ID + KC_CLIENT_SECRET
// ═════════════════════════════════════════════════════════════════════════
// GET /login — редирект на Keycloak
router.get('/login', (req, res) => {
if (req.session && req.session.token) return res.redirect('/');
const state = crypto.randomBytes(16).toString('hex');
req.session.oidcState = state;
req.session.oidcReturnTo = req.query.returnTo || '/';
const url = auth.buildAuthUrl(state);
res.redirect(url);
});
// GET /callback — обмен code → token → сессия
router.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Проверка state (защита от CSRF на callback)
if (!state || state !== req.session.oidcState) {
return res.status(403).send('Invalid state parameter');
}
delete req.session.oidcState;
if (!code) {
return res.status(400).send('Missing authorization code');
}
try {
// Обмен code на токен (через Keycloak или API Gateway)
const tokenData = await auth.exchangeCode(code);
const accessToken = tokenData.access_token;
// Верификация токена (проверка подписи через JWKS)
// userFromPayload уже встроена в exchangeCode или делается здесь
const payload = jwt.decode(accessToken);
if (!payload) throw new Error('Failed to decode token');
// Определяем компании пользователя
const rawClientId = payload.ClientID || payload.client_id || '';
const allClientIds = rawClientId.split(',').map(s => s.trim()).filter(Boolean);
const activeClientId = allClientIds[0] || rawClientId;
req.session.token = accessToken;
req.session.user = {
clientId: rawClientId,
allClientIds,
activeClientId,
email: payload.email || payload.login || '',
companyId: payload.company_id || payload.companyId || null,
companyName: payload.company_name || payload.companyName || activeClientId,
isAdmin: activeClientId === (process.env.ADMIN_CLIENT_ID || 'WZ01112'),
};
const returnTo = req.session.oidcReturnTo || '/';
delete req.session.oidcReturnTo;
res.redirect(returnTo);
} catch (e) {
console.error('[oidc] Callback error:', e);
res.status(500).send('Authentication failed: ' + e.message);
}
});
// GET /logout
router.get('/logout', (req, res) => {
const idToken = req.session && req.session.idToken;
req.session.destroy(() => {
const url = auth.buildLogoutUrl(idToken);
res.redirect(url);
});
});
return router;
}
module.exports = { createRouter };