refactor: шаг 5 — /login /logout вынесены в src/routes/auth.js
This commit is contained in:
@@ -54,39 +54,14 @@ async function start() {
|
|||||||
aggregateCIDRs,
|
aggregateCIDRs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Страница входа — отдаём форму выбора мок-пользователя.
|
// GET /login, POST /login, GET /logout.
|
||||||
// Генерируем CSRF-токен и передаём в шаблон для скрытого поля формы.
|
// src/routes/auth.js содержит логику и комментарии.
|
||||||
app.get('/login', (req, res) => {
|
app.use(require('./src/routes/auth').createRouter({
|
||||||
const csrfToken = generateCsrfToken(req, res);
|
auth,
|
||||||
res.render('login', { users: MOCK_USERS, error: req.query.error || null, csrfToken });
|
doubleCsrfProtection,
|
||||||
});
|
generateCsrfToken,
|
||||||
|
MOCK_USERS,
|
||||||
// Обработка логина: создаём 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'); });
|
|
||||||
|
|
||||||
// ── Все маршруты ниже требуют валидного JWT (проверяет auth.middleware) ───────
|
// ── Все маршруты ниже требуют валидного JWT (проверяет auth.middleware) ───────
|
||||||
app.use(auth.middleware);
|
app.use(auth.middleware);
|
||||||
|
|||||||
@@ -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 };
|
||||||
Reference in New Issue
Block a user