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 в OAuth-потоке (всегда) 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 на токен (через IAM / Keycloak) const tokenData = await auth.exchangeCode(code); const accessToken = tokenData.accessToken; if (!accessToken) throw new Error('No access_token in response'); // Базовый user из JWT (fallback) const payload = jwt.decode(accessToken); if (!payload) throw new Error('Failed to decode token'); // Регенерируем ID сессии — защита от session fixation await new Promise((resolve, reject) => { req.session.regenerate(err => err ? reject(err) : resolve()); }); req.session.token = accessToken; req.session.idToken = tokenData.idToken || null; // Пытаемся обогатить через IAM API try { const iamData = await auth.fetchIamUser(accessToken); req.session.user = { email: iamData.email, clientId: iamData.clientId, allClientIds: iamData.allClientIds, activeClientId: iamData.clientId, activeProfileId: iamData.activeProfileId, companyId: iamData.companyId, companyName: iamData.companyName, isAdmin: iamData.isAdmin, isImpersonated: iamData.isImpersonated, impersonationType: iamData.impersonationType, impersonatedCompanyId: iamData.impersonatedCompanyId, originalUserEmail: iamData.originalUserEmail, originalUserFullName: iamData.originalUserFullName, originalUserCompany: iamData.originalUserCompany, fio: iamData.fio, profiles: iamData.profiles, }; console.log('[oidc] IAM enrichment OK:', iamData.email, iamData.clientId, 'profiles:', iamData.allClientIds.length); } catch (iamErr) { // IAM недоступен — используем fallback из JWT console.error('[oidc] ⚠️ IAM API UNAVAILABLE — falling back to JWT claims:', iamErr.message); console.error('[oidc] User company/isAdmin may be incorrect — profiles[] not available'); const rawClientId = payload.ClientID || payload.client_id || ''; const allClientIds = rawClientId.split(',').map(s => s.trim()).filter(Boolean); const activeClientId = allClientIds[0] || rawClientId || 'UNKNOWN'; req.session.user = { clientId: activeClientId, allClientIds, activeClientId, email: payload.email || payload.login || '', companyId: payload.company_id || null, companyName: payload.company_name || activeClientId, isAdmin: activeClientId === (process.env.ADMIN_CLIENT_ID || 'WZ01112'), }; } const returnTo = '/v2/app'; 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; } /** * Валидирует redirect-цель: разрешает только локальные пути. * Защита от open redirect: //evil.com и https://evil.com → '/'. */ function safeLocal(target) { if (typeof target === 'string' && target.startsWith('/') && !target.startsWith('//')) { return target; } return '/'; } module.exports = { createRouter };