Files
ipwhitelist-app/src/routes/oidc.js
T

137 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };