From 55d26c1ece5cbeb0e0666b1624af5e6c01d36621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 13:39:04 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D1=88=D0=B0=D0=B3=205=20=E2=80=94?= =?UTF-8?q?=20/login=20/logout=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B2=20src/routes/auth.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 41 ++++---------------- src/routes/auth.js | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 33 deletions(-) create mode 100644 src/routes/auth.js diff --git a/server.js b/server.js index 9599884..e5ba71f 100644 --- a/server.js +++ b/server.js @@ -54,39 +54,14 @@ async function start() { aggregateCIDRs, })); - // Страница входа — отдаём форму выбора мок-пользователя. - // Генерируем CSRF-токен и передаём в шаблон для скрытого поля формы. - app.get('/login', (req, res) => { - const csrfToken = generateCsrfToken(req, res); - res.render('login', { users: MOCK_USERS, error: req.query.error || null, csrfToken }); - }); - - // Обработка логина: создаём JWT и кладём в httpOnly cookie. - // doubleCsrfProtection проверяет, что форма отправлена с нашей страницы /login. - app.post('/login', doubleCsrfProtection, async (req, res) => { - const user = MOCK_USERS.find(u => u.id === req.body.user); - if (!user) return res.redirect('/login?error=' + encodeURIComponent('Пользователь не найден')); - try { - const token = await auth.issue({ - clientId: user.clientId, - companyId: user.companyId, - companyName: user.companyName, - email: user.email, - }); - res.cookie('jwt', token, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - maxAge: 24 * 60 * 60 * 1000, // 24 часа - }); - res.redirect(req.query.returnTo || '/'); - } catch (e) { - res.redirect('/login?error=' + encodeURIComponent('Ошибка выпуска токена')); - } - }); - - // Выход: очищаем cookie и возвращаем на /login - app.get('/logout', (req, res) => { res.clearCookie('jwt'); res.redirect('/login'); }); + // GET /login, POST /login, GET /logout. + // src/routes/auth.js содержит логику и комментарии. + app.use(require('./src/routes/auth').createRouter({ + auth, + doubleCsrfProtection, + generateCsrfToken, + MOCK_USERS, + })); // ── Все маршруты ниже требуют валидного JWT (проверяет auth.middleware) ─────── app.use(auth.middleware); diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..7c31a06 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,93 @@ +/** + * src/routes/auth.js — маршруты аутентификации (mock-режим). + * + * Маршруты: + * GET /login — страница выбора пользователя (мок-форма) + * POST /login — обработка выбора: выпускает JWT, кладёт в cookie + * GET /logout — очищает cookie, возвращает на /login + * + * Зависимости (приходят через deps): + * auth — объект из initAuth(): { issue, middleware, jwksHandler } + * doubleCsrfProtection — middleware проверки CSRF-токена + * generateCsrfToken — функция генерации токена для передачи в шаблон + * MOCK_USERS — список мок-пользователей (из src/config.js) + * + * В продакшене этот файл заменяется на редирект к Keycloak (OIDC Authorization Code Flow). + * Маршруты регистрируются ДО auth.middleware — т.е. доступны без авторизации. + * + * @param {{ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS }} deps + * @returns {import('express').Router} + */ + +'use strict'; + +const { Router } = require('express'); + +function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS }) { + const router = Router(); + + // GET /login — отдаём форму с выпадающим списком мок-пользователей. + // Генерируем CSRF-токен здесь, чтобы он попал в скрытое поле формы. + // Без этого POST /login будет отклонён doubleCsrfProtection. + router.get('/login', (req, res) => { + // generateCsrfToken одновременно: устанавливает CSRF cookie и возвращает токен-строку + const csrfToken = generateCsrfToken(req, res); + res.render('login', { + users: MOCK_USERS, + error: req.query.error || null, // ошибка от предыдущего неудачного логина + csrfToken, + }); + }); + + // POST /login — обработка формы входа. + // doubleCsrfProtection идёт первым: если CSRF-токен невалиден — отдаёт 403 + // ещё до того как мы начнём работать с req.body. + router.post('/login', doubleCsrfProtection, async (req, res) => { + // Ищем выбранного пользователя по id ('admin', 'test', 'client2') + const user = MOCK_USERS.find(u => u.id === req.body.user); + + if (!user) { + // Неизвестный id — скорее всего попытка подделки запроса + return res.redirect('/login?error=' + encodeURIComponent('Пользователь не найден')); + } + + try { + // Выпускаем JWT с clientId, companyId, companyName, email. + // В продакшене этот токен приходит от Keycloak — здесь мы его имитируем. + const token = await auth.issue({ + clientId: user.clientId, + companyId: user.companyId, + companyName: user.companyName, + email: user.email, + }); + + // Кладём токен в httpOnly cookie — JS на странице его не видит (защита от XSS). + // sameSite: lax — cookie отправляется при навигации, но не при кросс-сайтовых запросах. + res.cookie('jwt', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', // только HTTPS в проде + sameSite: 'lax', + maxAge: 24 * 60 * 60 * 1000, // 24 часа в миллисекундах + }); + + // returnTo — параметр от auth.middleware: куда перенаправить после логина. + // Если не задан — на главную. + res.redirect(req.query.returnTo || '/'); + } catch (e) { + console.error('Login error:', e); + res.redirect('/login?error=' + encodeURIComponent('Ошибка выпуска токена')); + } + }); + + // GET /logout — удаляем cookie и отправляем на /login. + // Простой GET (а не POST) допустим для мок-режима. + // В продакшене логаут должен быть POST с CSRF-токеном + редирект к Keycloak. + router.get('/logout', (req, res) => { + res.clearCookie('jwt'); + res.redirect('/login'); + }); + + return router; +} + +module.exports = { createRouter };