feat: Authorization Code Flow (KK) + session middleware + dev-login

- src/auth.js: OIDC mode (KC_CLIENT_ID/KC_CLIENT_SECRET), session-based
  middleware, legacy jwt-cookie migration, httpGet/httpPost без внешних зав.
- src/routes/auth.js: GET /callback (OIDC), GET+POST /dev-login (backdoor),
  session-based login/logout для обоих режимов
- server.js: express-session (8h, httpOnly, sameSite lax), trust proxy в prod
- views/dev-login.ejs: форма с пресетами MOCK_USERS + произвольные поля
- views/login.ejs: ссылка на /dev-login если devLoginEnabled
- tests: +5 тестов (session middleware), 50 PASS / 0 FAIL

Env-переменные:
  KC_CLIENT_ID, KC_CLIENT_SECRET, KC_BASE_URL, APP_URL  — OIDC режим
  SESSION_SECRET                                         — секрет сессии
  DEV_MODE=true                                          — /dev-login без пароля
  DEV_SECRET=xyz                                         — /dev-login с ?key=xyz
This commit is contained in:
2026-05-30 14:37:29 +03:00
parent 876e03b78b
commit 86ca944c52
8 changed files with 624 additions and 169 deletions
+19
View File
@@ -17,6 +17,7 @@
'use strict';
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const path = require('path');
const helmet = require('helmet');
@@ -41,6 +42,9 @@ app.set('views', path.join(__dirname, 'views'));
// Referrer-Policy и пр. CSP отключён явно — EJS использует inline-стили/скрипты.
app.use(helmet({ contentSecurityPolicy: false }));
// За reverse-proxy (nginx/k8s ingress) — доверяем первому хопу для secure cookies.
if (process.env.NODE_ENV === 'production') app.set('trust proxy', 1);
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.urlencoded({ extended: true, limit: '32kb' }));
app.use(cookieParser());
@@ -48,6 +52,21 @@ app.use(cookieParser());
async function start() {
const auth = await initAuth();
// ── Сессии ────────────────────────────────────────────────────────────────
// SESSION_SECRET из .env (обязателен в production).
// MemoryStore — только для одного инстанса; для multi-pod нужен Redis.
app.use(session({
secret: process.env.SESSION_SECRET || 'dev-session-secret-change-me',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // только HTTPS в проде
sameSite: 'lax',
maxAge: 8 * 60 * 60 * 1000, // 8 часов
},
}));
// ── CSRF защита ────────────────────────────────────────────────────────────
// initCsrf() читает CSRF_SECRET и NODE_ENV из process.env.
// Возвращает: doubleCsrfProtection (middleware на POST) и