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:
@@ -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 };
|
||||
Reference in New Issue
Block a user