feat: authLimiter on POST /login, GET /callback, POST /dev-login (10 req / 5 min)
This commit is contained in:
@@ -27,7 +27,7 @@ const { initAuth, requireAdmin } = require('./src/auth');
|
|||||||
const { aggregateCIDRs } = require('./src/validators');
|
const { aggregateCIDRs } = require('./src/validators');
|
||||||
const q = require('./src/queries');
|
const q = require('./src/queries');
|
||||||
const { MOCK_USERS, backUrl } = require('./src/config');
|
const { MOCK_USERS, backUrl } = require('./src/config');
|
||||||
const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit');
|
const { mutationLimiter, exportLimiter, authLimiter } = require('./src/middleware/rateLimit');
|
||||||
const { initCsrf } = require('./src/middleware/csrf');
|
const { initCsrf } = require('./src/middleware/csrf');
|
||||||
const { createSessionMiddleware } = require('./src/middleware/session');
|
const { createSessionMiddleware } = require('./src/middleware/session');
|
||||||
const { CSP_DIRECTIVES } = require('./src/middleware/csp');
|
const { CSP_DIRECTIVES } = require('./src/middleware/csp');
|
||||||
@@ -97,6 +97,7 @@ async function start() {
|
|||||||
doubleCsrfProtection,
|
doubleCsrfProtection,
|
||||||
generateCsrfToken,
|
generateCsrfToken,
|
||||||
MOCK_USERS,
|
MOCK_USERS,
|
||||||
|
authLimiter,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ── Все маршруты ниже требуют авторизации (auth.middleware) ────────────────
|
// ── Все маршруты ниже требуют авторизации (auth.middleware) ────────────────
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ const mutationLimiter = rateLimit({
|
|||||||
message: { error: 'Слишком много запросов. Подождите минуту.' },
|
message: { error: 'Слишком много запросов. Подождите минуту.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── exportLimiter — для публичного GET /export ────────────────────────────────
|
// ── exportLimiter — для GET /export ──────────────────────────────────────────
|
||||||
// /export не требует авторизации (ТЗ) — поэтому ограничиваем жёстче.
|
|
||||||
// 20 запросов в минуту достаточно для автоматической синхронизации
|
// 20 запросов в минуту достаточно для автоматической синхронизации
|
||||||
// (nginx/firewall обычно опрашивают раз в несколько минут).
|
// (nginx/firewall обычно опрашивают раз в несколько минут).
|
||||||
const exportLimiter = rateLimit({
|
const exportLimiter = rateLimit({
|
||||||
@@ -42,4 +41,16 @@ const exportLimiter = rateLimit({
|
|||||||
message: 'Слишком много запросов к /export. Подождите минуту.',
|
message: 'Слишком много запросов к /export. Подождите минуту.',
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = { mutationLimiter, exportLimiter };
|
// ── authLimiter — для POST /login, POST /dev-login, GET /callback ─────────────
|
||||||
|
// Защита от брутфорса и нагрузки на Keycloak token endpoint.
|
||||||
|
// 10 попыток в 5 минут с одного IP — достаточно для нормального пользования,
|
||||||
|
// жёстко блокирует автоматические переборы.
|
||||||
|
const authLimiter = rateLimit({
|
||||||
|
windowMs: 5 * 60_000, // скользящее окно: 5 минут
|
||||||
|
limit: 10, // не более 10 попыток входа за окно
|
||||||
|
standardHeaders: 'draft-7',
|
||||||
|
legacyHeaders: false,
|
||||||
|
message: { error: 'Слишком много попыток входа. Попробуйте через 5 минут.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = { mutationLimiter, exportLimiter, authLimiter };
|
||||||
|
|||||||
+4
-4
@@ -39,7 +39,7 @@ function safeReturn(target) {
|
|||||||
return '/';
|
return '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS }) {
|
function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS, authLimiter }) {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// ── GET /login ─────────────────────────────────────────────────────────────
|
// ── GET /login ─────────────────────────────────────────────────────────────
|
||||||
@@ -63,7 +63,7 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── POST /login (только mock-режим) ────────────────────────────────────────
|
// ── POST /login (только mock-режим) ────────────────────────────────────────
|
||||||
router.post('/login', doubleCsrfProtection, (req, res) => {
|
router.post('/login', authLimiter, doubleCsrfProtection, (req, res) => {
|
||||||
if (auth.isOidc) return res.redirect('/login');
|
if (auth.isOidc) return res.redirect('/login');
|
||||||
|
|
||||||
const user = MOCK_USERS.find(u => u.id === req.body.user);
|
const user = MOCK_USERS.find(u => u.id === req.body.user);
|
||||||
@@ -92,7 +92,7 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── GET /callback (только OIDC-режим) ──────────────────────────────────────
|
// ── GET /callback (только OIDC-режим) ──────────────────────────────────────
|
||||||
router.get('/callback', async (req, res) => {
|
router.get('/callback', authLimiter, async (req, res) => {
|
||||||
if (!auth.isOidc) return res.redirect('/login');
|
if (!auth.isOidc) return res.redirect('/login');
|
||||||
|
|
||||||
const { code, state, error } = req.query;
|
const { code, state, error } = req.query;
|
||||||
@@ -152,7 +152,7 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── POST /dev-login ────────────────────────────────────────────────────────
|
// ── POST /dev-login ────────────────────────────────────────────────────────
|
||||||
router.post('/dev-login', doubleCsrfProtection, (req, res) => {
|
router.post('/dev-login', authLimiter, doubleCsrfProtection, (req, res) => {
|
||||||
if (!auth.devLoginEnabled) return res.status(404).send('Not Found');
|
if (!auth.devLoginEnabled) return res.status(404).send('Not Found');
|
||||||
|
|
||||||
if (auth.DEV_SECRET && req.session.devKey !== auth.DEV_SECRET) {
|
if (auth.DEV_SECRET && req.session.devKey !== auth.DEV_SECRET) {
|
||||||
|
|||||||
Reference in New Issue
Block a user