From 38c7adf25d6c511bffc0883e6df4bfac32505659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Wed, 3 Jun 2026 12:37:10 +0300 Subject: [PATCH] =?UTF-8?q?v0.5.14=20=E2=80=94=20=D0=B2=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D1=83,=20JWKS?= =?UTF-8?q?=20=D0=B2=20mock-=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B5,=20produc?= =?UTF-8?q?tion=20deploy=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /login-token: принимает реальный JWT (не только clientId) - auth.js: verifyAnyToken — mock + JWKS (Keycloak Nubes) одновременно - auth.js: JWKS предзагружается всегда, даже в mock-режиме - devops-deploy.md: полная production-инструкция для DevOps - .env.example: production-ready шаблон - README.md: актуальный (v0.5.13, архитектура, тесты, ссылки) - .gitignore: очищен от дубликатов --- .env.example | 16 +-- .gitignore | 34 ++---- README.md | 104 +++++++++++----- docs/devops-deploy.md | 269 ++++++++++++++++++++++++++++++++---------- src/auth.js | 57 ++++++--- ui/routes/auth.js | 46 +++++++- views/ui-login.ejs | 13 ++ 7 files changed, 396 insertions(+), 143 deletions(-) diff --git a/.env.example b/.env.example index 0366a95..8c99258 100644 --- a/.env.example +++ b/.env.example @@ -2,15 +2,17 @@ DB_HOST=localhost DB_PORT=5432 DB_NAME=ipwhitelist -DB_USER=super -DB_PASS=сменить_пароль +DB_USER=ipwhitelist +DB_PASS= DB_SSLMODE=disable -# ── Режим разработки (true = без Keycloak, mock-логин) ── +# ── Режим разработки ── +# true = mock-логин (без Keycloak, для тестов) +# false = OIDC SSO через Keycloak (ОБЯЗАТЕЛЬНО в production) DEV_MODE=true DEFAULT_LIMIT=15 -# ── Сессия (сменить на случайные строки) ── +# ── Сессия (сгенерировать: openssl rand -base64 32) ── SESSION_SECRET=сменить_на_случайную_строку CSRF_SECRET=сменить_на_случайную_строку @@ -20,8 +22,8 @@ PORT=3001 # ── Администратор (clientId) ── ADMIN_CLIENT_ID=WZ01112 -# ── Keycloak (оставить пустым пока нет OIDC) ── -# KC_CLIENT_ID= -# KC_CLIENT_SECRET= +# ── Keycloak / OIDC (ЗАПОЛНИТЬ при DEV_MODE=false) ── +# KC_CLIENT_ID= # ID зарегистрированного OIDC-клиента в Keycloak +# KC_CLIENT_SECRET= # секрет клиента # KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud # APP_URL=https://ваш-домен.ru diff --git a/.gitignore b/.gitignore index 45c021d..8091391 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,27 @@ -.env +# Система node_modules/ +.env __pycache__/ *.pyc .DS_Store .vscode/ .idea/ -Files/nubes_login.har +# Креды, токены, конфиги token*.* +Files/ +kubeconfig-* -# Внутренняя аналитика — не для публичного доступа -PROJECT-AUDIT.md -AGENT-DIAGNOSIS.md +# Внутренняя аналитика (не для публичного доступа) AGENT.md +AGENT-DIAGNOSIS.md +PROJECT-AUDIT.md CONTEXT.md -prompt-opus-*.md -docs/questions-devops.md -docs/PLAN-*.md -docs/STATE-*.md -docs/cluster-cleanup-*.md -docs/tests-*.md -docs/test-api.sh -docs/nubes-design-system.md -docs/deploy-vm.md +SESSION-SUMMARY.md +prompt-*.md # Результаты код-ревью агентов research/ -# Тестовые данные (токены, HAR, etc) -Files/ -token*.* - # Логи тестов test-results/ -research/opus-full-review-*.md -research/REVIEW-SUMMARY.md -docs/STATE-old.md - -# Результаты тестов — генерируются при прогоне -test-results/ diff --git a/README.md b/README.md index 3f078dc..aa5f461 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,97 @@ -# IP WhiteList - -> ⚠️ **В процессе разработки.** НЕ для production. +# IP WhiteList v0.5.13 Self-service портал для управления доверенными IPv4-адресами клиентов облачного провайдера. Записи исключаются из блокировки системами фильтрации во время DDoS-атак. -**[Полное техническое задание](docs/ТЗ.md)** · **[Отчёт о соответствии](docs/TZ-COMPLIANCE-REPORT.md)** +**[Техническое задание](docs/ТЗ.md)** · **[DevOps — production deploy](docs/devops-deploy.md)** --- -## Что делает +## Возможности -- **Клиент** авторизуется через Keycloak (OIDC) и управляет списком своих IPv4/CIDR: добавляет, редактирует, удаляет (soft delete) -- **Валидация** на сервере: маски /22–/32, запрет 14 приватных/служебных диапазонов, запрет дубликатов и пересечений внутри компании, нормализация host-битов -- **Лимиты:** глобальный (15 по умолчанию, настраивается через env) и индивидуальный на компанию (устанавливает админ) -- **Администратор** видит записи всех компаний, управляет лимитами, просматривает журнал аудита -- **Аудит:** все изменения (создание/редактирование/удаление) фиксируются — кто, когда, что было и что стало +- **Управление белым списком:** добавление, редактирование, удаление (soft delete) IPv4/CIDR +- **Валидация** на сервере: маски `/22`–`/32`, запрет 14 приватных/служебных диапазонов, запрет дубликатов и пересечений, нормализация host-битов +- **Лимиты:** глобальный (по умолчанию 15) и индивидуальный на компанию (настраивает админ) +- **Агрегированный экспорт** CIDR-списка для систем фильтрации (`GET /export`) +- **Мульти-компания:** один пользователь может управлять несколькими компаниями (clientId через запятую) +- **Аудит:** все изменения фиксируются — кто, когда, что было и что стало +- **Администрирование:** просмотр всех компаний, управление лимитами, журнал аудита -## Роли +## Аутентификация -| Роль | Права | +| Режим | Описание | |---|---| -| Клиент | CRUD своих записей в пределах лимита | -| Администратор (WZ01112) | CRUD всех записей, изменение лимитов, просмотр аудита | +| **DEV_MODE=true** (тесты/разработка) | Mock JWT, локальная RSA-пара. Вход через форму выбора пользователя или вставку Bearer-токена | +| **DEV_MODE=false** (production) | OIDC SSO через Keycloak Nubes. Редирект на Keycloak login → `/callback` → сессия | -## Как получить список для фильтрации - -Агрегированный список всех активных CIDR в формате txt (одна строка — одна подсеть). - -| Endpoint | Авторизация | Статус | -|---|---|---| -| `GET /export` | Bearer-токен | Будет основным (публичным по ТЗ) | -| `GET /exp` | **Нет** | Временный, работает сейчас | - -После перевода `/export` на публичный доступ `/exp` будет удалён. +В production требуются `KC_CLIENT_ID` + `KC_CLIENT_SECRET` от DevOps. Подробнее — [deploy guide](docs/devops-deploy.md). ## Стек -Node.js 18+ / Express 4 / EJS (SSR) / PostgreSQL 17 / jsonwebtoken RS256 +``` +Node.js 20+ → Express 4 → EJS (SSR) → PostgreSQL 16+ +jsonwebtoken RS256 / JWKS (Keycloak Nubes) +express-session + connect-pg-simple +``` ## Архитектура ``` -Browser → UI (EJS, сессия) → REST API (Bearer JWT) → PostgreSQL +Browser → nginx:443 → Express:3001 + ├── /export (публичный CIDR) + ├── /api/v1/* (REST API, Bearer JWT) + └── /* (UI, SSR EJS, сессия → api-client.js → API) ``` -Два слоя: SSR UI для браузера, JSON API для внешних потребителей. +Два слоя: SSR UI для браузера, JSON API для внешних систем. -## Деплой +## Multi-company -Nubes k8s. Редеплой — Nubes UI из Gitea после `git push`. -URL: `https://white.nodejsk8s.dev.nubes.ru` +Пользователь может принадлежать нескольким компаниям. В JWT claim `ClientID` передаются значения через запятую: +``` +ClientID: "WZ11125,WZ03816" +``` +Первое значение — активная компания. Переключение компаний — через UI. -## Разработка +## Тесты -Перед любой задачей читать [`AGENT.md`](AGENT.md) — руководство для агентов и разработчиков. +```bash +npm test # 121 API-тест (Bearer JWT, CRUD, изоляция, admin) +node tests/integration.js # 68 UI-тестов (сессии, EJS) +node tests/tz-compliance.js # 47 тестов ТЗ +node tests/tz-full-compliance.js # 111 тестов (20 компаний × 100 CIDR) +node tests/stress.js # 191 стресс-тест (concurrency, auth attacks, edge cases) +``` + +Все тесты требуют реального PostgreSQL. + +## Быстрый старт (dev) + +```bash +git clone https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git +cd ipwhitelist-app +cp .env.example .env # заполнить DB_* и SESSION_SECRET +npm install +psql -U postgres -d ipwhitelist -f sql/schema.sql +npm start # http://localhost:3001 + DEV_MODE=true +``` + +## Production deploy + +См. [`docs/devops-deploy.md`](docs/devops-deploy.md) — nginx, HTTPS, PM2, Keycloak SSO. + +## Документация + +- [Техническое задание](docs/ТЗ.md) +- [Уточнения ТЗ](docs/ТЗ-плюс.md) +- [DevOps deployment guide](docs/devops-deploy.md) (англ.) +- [AI Agent Guide](.github/AGENT-GUIDE.md) (англ., для Copilot) +- [Copilot Instructions](.github/copilot-instructions.md) (правила для AI) +- [Keycloak/OIDC reference](docs/keycloak-auth-reference.md) +- [Deploy Keycloak](docs/deploy-keycloak.md) + +## Текущий деплой + +- **URL:** `https://italo.kube5s.ru` +- **Режим:** DEV_MODE=true (mock) +- **БД:** PostgreSQL на ВМ, localhost only diff --git a/docs/devops-deploy.md b/docs/devops-deploy.md index 1a5bf61..2c95cce 100644 --- a/docs/devops-deploy.md +++ b/docs/devops-deploy.md @@ -1,73 +1,140 @@ -# Деплой ipwhitelist для DevOps +# Деплой ipwhitelist — PRODUCTION -> Репозиторий: `https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git` +> Репозиторий: `https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git` (публичный) +> Стек: Node.js 20+ / Express 4 / EJS / PostgreSQL 16+ +> Требования: доступ к Keycloak Nubes (realm `cloud`), зарегистрированный OIDC-клиент -## Быстрый старт +--- + +## 1. Подготовка сервера ```bash -# 1. Установить Node.js v20+ +# Node.js 20+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - -apt install -y nodejs git +apt install -y nodejs git nginx certbot python3-certbot-nginx postgresql -# 2. Скачать -git clone https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git -cd ipwhitelist-app -npm install - -# 3. Создать .env (см. ниже) -cp .env.example .env -nano .env - -# 4. Создать БД PostgreSQL -sudo -u postgres psql -c "CREATE USER super WITH PASSWORD '...';" -sudo -u postgres psql -c "CREATE DATABASE ipwhitelist OWNER super;" -psql -h localhost -U super -d ipwhitelist -f sql/schema.sql - -# 5. Запустить -npm start -# ИЛИ через PM2: -npm i -g pm2 -pm2 start server.js --name ipwhitelist +# Код +git clone https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git /opt/ipwhitelist +cd /opt/ipwhitelist +npm install --production ``` -## .env (все параметры) +--- + +## 2. PostgreSQL ```bash -# ── База данных PostgreSQL ── +# Создать пользователя и БД +sudo -u postgres psql -c "CREATE USER ipwhitelist WITH PASSWORD '${PASSWORD}';" +sudo -u postgres psql -c "CREATE DATABASE ipwhitelist OWNER ipwhitelist;" + +# Применить схему +PGPASSWORD="${PASSWORD}" psql -h localhost -U ipwhitelist -d ipwhitelist -f /opt/ipwhitelist/sql/schema.sql + +# Настройка: слушать только localhost (безопасность) +grep -c "^listen_addresses = 'localhost'" /etc/postgresql/16/main/postgresql.conf \ + || echo "listen_addresses = 'localhost'" >> /etc/postgresql/16/main/postgresql.conf +systemctl restart postgresql +``` + +--- + +## 3. .env (PRODUCTION) + +```bash +cp /opt/ipwhitelist/.env.example /opt/ipwhitelist/.env +chmod 600 /opt/ipwhitelist/.env +``` + +```ini +# ── База данных ── DB_HOST=localhost DB_PORT=5432 DB_NAME=ipwhitelist -DB_USER=super -DB_PASS=ваш_пароль -DB_SSLMODE=disable +DB_USER=ipwhitelist +DB_PASS=<сгенерировать: openssl rand -base64 24> +DB_SSLMODE=disable # localhost — шифрование не нужно -# ── Режим разработки (оставить true пока нет Keycloak) ── -DEV_MODE=true +# ── Аутентификация ── +DEV_MODE=false # ОБЯЗАТЕЛЬНО false в production +ADMIN_CLIENT_ID=WZ01112 # clientId пользователя-администратора + +# ── Keycloak / OIDC (данные от DevOps) ── +KC_CLIENT_ID= # зарегистрированный OIDC-клиент +KC_CLIENT_SECRET= # секрет клиента +KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud +APP_URL=https://<домен> # публичный URL приложения (без слеша) + +# ── Сессия ── +SESSION_SECRET= +CSRF_SECRET= + +# ── Лимит записей по умолчанию ── DEFAULT_LIMIT=15 -# ── Сессия (сменить на случайные строки) ── -SESSION_SECRET=случайная_строка_32_символа -CSRF_SECRET=другая_случайная_строка_32_символа - -# ── Порт ── +# ── Порт (для nginx upstream) ── PORT=3001 -# ── Админ (clientId администратора) ── -ADMIN_CLIENT_ID=WZ01112 - -# ── Keycloak (не заполнять пока нет) ── -# KC_CLIENT_ID=ipwhitelist -# KC_CLIENT_SECRET=секрет -# KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud -# APP_URL=https://ваш-домен.ru +# ── API_BASE — куда UI-слой ходит за API (обычно localhost) ── +API_BASE=http://localhost:3001 ``` -## nginx +**Генерация секретов:** +```bash +openssl rand -base64 32 # для SESSION_SECRET +openssl rand -base64 32 # для CSRF_SECRET +openssl rand -base64 24 # для DB_PASS +``` + +--- + +## 4. Keycloak — регистрация OIDC-клиента + +**Что сделать в админке Keycloak (realm `cloud`):** + +1. Создать клиента: + - Client ID: `<домен>` (например `white.nubes.ru`) + - Client authentication: ON + - Standard flow: ON + - Valid redirect URIs: `https://<домен>/callback` + - Web origins: `https://<домен>` (или `+`) + +2. Забрать `Client secret` → в .env как `KC_CLIENT_SECRET` + +3. **Mapper'ы** (создать на клиенте, тип `User Attribute → Token Claim`): + +| Claim name | User attribute | Назначение | +|---|---|---| +| `ClientID` | `client_id` | ID компании (может быть через запятую) | +| `company_id` | `company_id` | ID компании в БД | +| `company_name` | `company_name` | Название компании | + +4. **Пользователи** — создать в Keycloak с заполненными атрибутами `client_id`, `company_id`, `company_name`. + +**Формат multi-company:** `client_id = "WZ11125,WZ03816"` — первое значение до запятой = активная компания. Приложение парсит автоматически. + +--- + +## 5. nginx + HTTPS ```nginx server { listen 80; - server_name ваш-домен.ru; + server_name <домен>; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name <домен>; + + ssl_certificate /etc/letsencrypt/live/<домен>/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/<домен>/privkey.pem; + + # Security headers + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; location / { proxy_pass http://127.0.0.1:3001; @@ -75,27 +142,109 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; } } ``` -## Keycloak (когда появится) +```bash +# Получить сертификат +certbot --nginx -d <домен> -Нужно будет вписать в `.env`: -``` -DEV_MODE=false -KC_CLIENT_ID=ipwhitelist -KC_CLIENT_SECRET=... -KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud -APP_URL=https://ваш-домен.ru +# Проверить авто-обновление +certbot renew --dry-run ``` -И в админке Keycloak зарегистрировать клиента (см. `docs/deploy-keycloak.md`). +--- -## Проверка +## 6. PM2 — автозапуск -| URL | Что должно быть | +```bash +npm i -g pm2 + +# Запуск +pm2 start /opt/ipwhitelist/server.js --name ipwhitelist --update-env + +# Автостарт при перезагрузке +pm2 startup systemd +pm2 save + +# Проверка +pm2 status +pm2 logs ipwhitelist --lines 5 +# Должно быть: [auth] OIDC — Keycloak +# Если [auth] MOCK — значит KC_CLIENT_ID не задан +``` + +--- + +## 7. Порядок ввода в эксплуатацию + +```bash +# 1. Убедиться что DEV_MODE=false и заданы KC_* +grep -E "DEV_MODE|KC_CLIENT_ID|KC_CLIENT_SECRET" /opt/ipwhitelist/.env + +# 2. Перезапустить +pm2 restart ipwhitelist --update-env + +# 3. Проверить логи — должен быть OIDC-режим +pm2 logs ipwhitelist --lines 3 --nostream + +# 4. Проверить health +curl -s https://<домен>/healthz +# → OK + +# 5. Проверить SSO: открыть https://<домен>/login в браузере +# → редирект на Keycloak +# → после входа → редирект обратно на главную +``` + +--- + +## 8. Проверка + +| URL | Ожидаемый результат | |---|---| -| `https://ваш-домен.ru/healthz` | `OK` | -| `https://ваш-домен.ru/export` | Список CIDR (text/plain) | -| `https://ваш-домен.ru/` | Страница входа (при DEV_MODE) | +| `https://<домен>/healthz` | `OK` | +| `https://<домен>/` | Редирект на Keycloak login (если нет сессии) | +| `https://<домен>/login` | Редирект на Keycloak (если `DEV_MODE=false`) | +| `https://<домен>/export` | CIDR-список (text/plain) | +| `https://<домен>/api/v1/entries` | 401 (нужен Bearer) | +| `https://<домен>/callback?code=...` | Обмен code → сессия → редирект на `/` | + +--- + +## 9. Обновление + +```bash +cd /opt/ipwhitelist +git pull +npm install --production +pm2 restart ipwhitelist --update-env +``` + +--- + +## 10. Переменные окружения (справочно) + +| Переменная | Обязательна | По умолчанию | Описание | +|---|---|---|---| +| `DB_HOST` | да | — | Хост PostgreSQL | +| `DB_PORT` | нет | 5432 | Порт PostgreSQL | +| `DB_NAME` | да | — | Имя БД | +| `DB_USER` | да | — | Пользователь БД | +| `DB_PASS` | да | — | Пароль БД | +| `DB_SSLMODE` | нет | disable | `require` для внешнего подключения | +| `DEV_MODE` | нет | true | `false` в production (ОБЯЗАТЕЛЬНО) | +| `ADMIN_CLIENT_ID` | нет | WZ01112 | clientId администратора | +| `KC_CLIENT_ID` | да* | — | OIDC client ID (обязателен для SSO) | +| `KC_CLIENT_SECRET` | да* | — | OIDC client secret | +| `KC_BASE_URL` | нет | `https://keycloak.nubes.ru/realms/cloud` | Keycloak realm URL | +| `APP_URL` | да* | — | Публичный URL приложения (для OIDC redirect) | +| `SESSION_SECRET` | да | — | Подпись сессионных cookie | +| `CSRF_SECRET` | нет | — | CSRF-токены | +| `PORT` | нет | 3000 | Порт Express | +| `DEFAULT_LIMIT` | нет | 15 | Лимит записей по умолчанию | +| `API_BASE` | нет | `http://localhost:PORT` | URL для UI→API запросов | + +\* — обязательны при `DEV_MODE=false` diff --git a/src/auth.js b/src/auth.js index 28638f2..81ce63d 100644 --- a/src/auth.js +++ b/src/auth.js @@ -66,16 +66,17 @@ async function initAuth() { const isOidc = !!(KC_CLIENT_ID && KC_CLIENT_SECRET); + // Всегда предзагружаем JWKS (даже в mock-режиме), чтобы можно было + // проверить реальный Bearer токен от Keycloak без KC_CLIENT_ID/SECRET. + try { + cachedJwks = JSON.parse(await httpGet(OIDC.certs)); + console.log('[auth] JWKS loaded:', cachedJwks.keys.length, 'ключей'); + } catch (e) { + console.warn('[auth] JWKS preload failed:', e.message, '— только mock-режим'); + } + if (isOidc) { console.log('[auth] OIDC — Keycloak', KC_BASE_URL); - // Предзагрузка JWKS при старте — избегаем задержки на первом запросе. - // При ошибке логируем предупреждение: JWKS будет загружен в exchangeCode(). - try { - cachedJwks = JSON.parse(await httpGet(OIDC.certs)); - console.log('[auth] JWKS loaded:', cachedJwks.keys.length, 'ключей'); - } catch (e) { - console.warn('[auth] JWKS preload failed:', e.message, '— будет загружен при первом запросе'); - } } else { console.log('[auth] MOCK — local RS256 key'); mockKeyPair = crypto.generateKeyPairSync('rsa', { @@ -122,7 +123,7 @@ function createMiddleware(isOidc) { const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim(); if (bearer) { try { - const payload = isOidc ? verifyOidcToken(bearer) : verifyMockToken(bearer); + const payload = verifyAnyToken(bearer); req.user = userFromPayload(payload); return next(); } catch (e) { @@ -234,12 +235,36 @@ function verifyOidcToken(token) { if (!key) throw new Error('No matching RSA sig key in JWKS' + (kid ? ` (kid=${kid})` : '')); const pem = crypto.createPublicKey({ key, format: 'jwk' }).export({ type: 'spki', format: 'pem' }); - // Проверяем issuer и audience — защита от токенов других realm/клиентов (A02). - return jwt.verify(token, pem, { - algorithms: ['RS256'], - issuer: KC_BASE_URL, - audience: KC_CLIENT_ID, - }); + // Проверяем issuer — защита от токенов других realm. + // audience проверяем только если задан KC_CLIENT_ID (для OIDC-режима). + const opts = { algorithms: ['RS256'], issuer: KC_BASE_URL }; + if (KC_CLIENT_ID) opts.audience = KC_CLIENT_ID; + return jwt.verify(token, pem, opts); +} + +/** + * Универсальная верификация: пробует mock, затем OIDC (JWKS). + * Позволяет принимать и mock-токены (тесты, dev), и реальные токены от Keycloak. + */ +function verifyAnyToken(token) { + let lastErr = null; + // 1. Mock (быстрый, локальный) + if (mockKeyPair) { + try { + return jwt.verify(token, mockKeyPair.publicKey, { algorithms: ['RS256'], issuer: ISSUER }); + } catch (e) { + lastErr = e; + } + } + // 2. OIDC (JWKS от Keycloak) + if (cachedJwks) { + try { + return verifyOidcToken(token); + } catch (e) { + lastErr = e; + } + } + throw lastErr || new Error('Token verification failed: no verifier available'); } function userFromPayload(payload) { @@ -269,7 +294,7 @@ function createBearerMiddleware(isOidc) { return res.status(401).json({ error: 'Bearer token required' }); } try { - const payload = isOidc ? verifyOidcToken(bearer) : verifyMockToken(bearer); + const payload = verifyAnyToken(bearer); req.user = userFromPayload(payload); return next(); } catch (e) { diff --git a/ui/routes/auth.js b/ui/routes/auth.js index 84207ab..233c36a 100644 --- a/ui/routes/auth.js +++ b/ui/routes/auth.js @@ -67,23 +67,61 @@ function createRouter({ auth, MOCK_USERS, authLimiter }) { // GET /login-token — редирект на /login (форма вставки токена там же) router.get('/login-token', (req, res) => res.redirect('/login')); - // POST /login-token — ручной вход по clientId (отладка) + // POST /login-token — вход по Bearer JWT токену (реальному) или clientId (отладка) router.post('/login-token', authLimiter, (req, res) => { - const { clientId, returnTo } = req.body; + const { token, clientId, returnTo } = req.body; + + // ── Вход по реальному JWT токену ────────────────────────────────────── + if (token && token.trim()) { + const raw = token.trim().replace(/^Bearer\s+/i, ''); + // Минимальная проверка: это JWT (3 части, разделённые точками) + const parts = raw.split('.'); + if (parts.length !== 3) { + return res.redirect('/login?error=Невалидный+JWT+токен'); + } + // Декодируем payload (без проверки подписи — API проверит) + let payload; + try { + const buf = Buffer.from(parts[1], 'base64url'); + payload = JSON.parse(buf.toString()); + if (!payload || typeof payload !== 'object') throw new Error('empty'); + } catch (_) { + return res.redirect('/login?error=Невалидный+payload+токена'); + } + + // Извлекаем данные пользователя из payload + const rawClientId = payload.ClientID || payload.client_id || payload.preferred_username || payload.sub || ''; + const allClientIds = rawClientId.split(',').map(s => s.trim()).filter(Boolean); + const activeClientId = allClientIds[0] || rawClientId; + + req.session.token = raw; + req.session.user = { + clientId: rawClientId, + allClientIds, + activeClientId, + email: payload.email || payload.login || activeClientId + '@unknown', + companyId: payload.company_id || payload.companyId || null, + companyName: payload.company_name || payload.companyName || activeClientId, + isAdmin: activeClientId === (process.env.ADMIN_CLIENT_ID || 'WZ01112'), + }; + return res.redirect(returnTo || '/'); + } + + // ── Вход по clientId (отладка, mock) ─────────────────────────────────── if (!clientId || !clientId.trim()) return res.redirect('/login?error=Введите+clientId'); const trimmed = clientId.trim(); const allClientIds = trimmed.split(',').map(s => s.trim()).filter(Boolean); const activeClientId = allClientIds[0]; - const token = auth.issueMockToken({ + const mockToken = auth.issueMockToken({ clientId: trimmed, companyId: '00000000-0000-0000-0000-000000000001', companyName: activeClientId, email: activeClientId + '@debug.local', }); - req.session.token = token; + req.session.token = mockToken; req.session.user = { clientId: trimmed, allClientIds, diff --git a/views/ui-login.ejs b/views/ui-login.ejs index 4359000..aee7c48 100644 --- a/views/ui-login.ejs +++ b/views/ui-login.ejs @@ -84,6 +84,19 @@
<% } %> + <%# ── Блок ручного ввода JWT токена ── %> +

Вход по Bearer токену

+
+ +
+ + +
+ +
+ +
+ <%# ── Блок ручного ввода clientId (отладка) ── %>

Ручной вход (clientId)