feat: authLimiter on POST /login, GET /callback, POST /dev-login (10 req / 5 min)

This commit is contained in:
2026-05-30 15:38:38 +03:00
parent b6e8f31cc6
commit bcd8c5631a
3 changed files with 20 additions and 8 deletions
+14 -3
View File
@@ -30,8 +30,7 @@ const mutationLimiter = rateLimit({
message: { error: 'Слишком много запросов. Подождите минуту.' },
});
// ── exportLimiter — для публичного GET /export ────────────────────────────────
// /export не требует авторизации (ТЗ) — поэтому ограничиваем жёстче.
// ── exportLimiter — для GET /export ──────────────────────────────────────────
// 20 запросов в минуту достаточно для автоматической синхронизации
// (nginx/firewall обычно опрашивают раз в несколько минут).
const exportLimiter = rateLimit({
@@ -42,4 +41,16 @@ const exportLimiter = rateLimit({
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
View File
@@ -39,7 +39,7 @@ function safeReturn(target) {
return '/';
}
function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS }) {
function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS, authLimiter }) {
const router = Router();
// ── GET /login ─────────────────────────────────────────────────────────────
@@ -63,7 +63,7 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER
});
// ── POST /login (только mock-режим) ────────────────────────────────────────
router.post('/login', doubleCsrfProtection, (req, res) => {
router.post('/login', authLimiter, doubleCsrfProtection, (req, res) => {
if (auth.isOidc) return res.redirect('/login');
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-режим) ──────────────────────────────────────
router.get('/callback', async (req, res) => {
router.get('/callback', authLimiter, async (req, res) => {
if (!auth.isOidc) return res.redirect('/login');
const { code, state, error } = req.query;
@@ -152,7 +152,7 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER
});
// ── 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.DEV_SECRET && req.session.devKey !== auth.DEV_SECRET) {