From 032b41b3f55d0737f709d0816b2df4862482eb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 14:21:42 +0300 Subject: [PATCH] =?UTF-8?q?research:=20auth-flow.md=20=E2=80=94=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B0=D1=8F=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8F=20SSO,=20KK=20endpoi?= =?UTF-8?q?nts,=20JWT=20claims,=20=D0=BE=D1=82=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B2=D0=BE=D0=BF=D1=80=D0=BE=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- research/auth-flow.md | 209 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 research/auth-flow.md diff --git a/research/auth-flow.md b/research/auth-flow.md new file mode 100644 index 0000000..a9f15a7 --- /dev/null +++ b/research/auth-flow.md @@ -0,0 +1,209 @@ +# SSO / Auth — исследование и контекст + +> Дата: 2026-05-30 +> Актуально для ветки `sonnet` + +--- + +## Исходные данные от команды + +### Ссылки (получены 2026-05-30) + +| Что | URL | Примечание | +|---|---|---| +| Пример SSO на ColdFusion | https://gitea.services.ngcloud.ru/smishchuk/payg-report.git | Написан «на коленке», но рабочий | +| Конфигурация сервиса в Deck | https://deck.ngcloud.ru/services/instance/detail/523413d2-1ffd-457b-b0e3-0a8185fe9358 | Там видно как кладутся env-переменные | + +### Файлы из payg-report сохранены локально + +``` +research/payg-sso/ + Application.cfc — основной SSO flow (редирект, обмен code→token, refresh, logout) + lib_oauth2.cfc — Authorization Code Flow клиент + lib_jwt.cfc — валидация JWT RS256 +``` + +--- + +## Keycloak — эндпоинты + +Realm: `cloud` +Base: `https://keycloak.nubes.ru/realms/cloud` + +| Назначение | URL | +|---|---| +| Авторизация (браузер → KK) | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth` | +| Обмен code → token | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token` | +| Logout | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/logout` | +| JWKS (публичные ключи) | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs` | +| OpenID конфигурация | `https://keycloak.nubes.ru/realms/cloud/.well-known/openid-configuration` | + +--- + +## Реальная схема аутентификации + +``` +Браузер + │ + │ 1. GET / (без токена) + ▼ +наше приложение + │ + │ 2. redirect → KK /auth?client_id=...&redirect_uri=...&scope=openid + ▼ +Keycloak (keycloak.nubes.ru, realm=cloud) + │ + │ 3. Пользователь логинится + │ 4. KK redirect → наш /callback?code=... + ▼ +наше приложение + │ + │ 5. POST /token (code + client_secret) → access_token + refresh_token + │ 6. Валидация JWT через JWKS (certs endpoint) + │ 7. Извлечение claims из JWT → req.user + ▼ + Работаем: req.user.ClientID = "WZ01325" и тд +``` + +**Важно из примера payg-report:** +- Тип flow: Authorization Code Flow (не Implicit, не PKCE — обычный) +- `client_secret` передаётся при обмене code→token (confidential client) +- Сертификат берётся с JWKS endpoint при старте, кэшируется в application scope +- При истечении части времени жизни — автоматический refresh через refresh_token + +--- + +## JWT — структура токена + +JWT выпускает **auth-api** (не Keycloak напрямую). +Алгоритм: **RS256**. +JWKS для валидации: из auth-api (уточнить URL у DevOps). + +> ⚠️ В payg-report используют `keycloak.nubes.ru/certs`, но наш портал deck.ngcloud.ru +> идёт через auth-api.ngcloud.ru — нужно уточнить чей именно JWT будет у нас. + +### Claims в JWT (из реального токена, secrets.txt) + +| Claim | Тип | Пример | Назначение | +|---|---|---|---| +| `iss` | string | `"auth-api"` | Издатель | +| `sub` | string | `"0199e325-1cdf-..."` | UUID пользователя | +| `exp` | number | `1795627443` | Срок действия | +| `iat` | number | `1780075443` | Выпущен | +| `ClientID` | string | `"WZ01325"` | **WZ-номер компании** ← ключевой claim | +| `company_id` | string (UUID) | `"3e64aac6-dcfc-..."` | UUID компании | +| `company_name` | string | `"Тест"` | Название компании | +| `email` | string | `"tazet@narod.ru"` | Email (для аудита) | +| `login` | string | `"tazet@narod.ru"` | Логин | +| `firstname` | string | `"Наиль"` | Имя | +| `lastname` | string | `"Тазетдинов"` | Фамилия | +| `token_type` | string | `"tech"` | Тип токена | + +**Пример декодированного payload** (из secrets.txt — не секрет, payload JWT не зашифрован): +```json +{ + "iss": "auth-api", + "sub": "0199e325-1cdf-7cda-9319-e5302a85e291", + "ClientID": "WZ01325", + "company_id": "3e64aac6-dcfc-4082-88dc-da19c86555a5", + "company_name": "Тест", + "email": "tazet@narod.ru", + "login": "tazet@narod.ru", + "token_type": "tech" +} +``` + +--- + +## Конфигурация сервиса (env-переменные) + +Из примера payg-report и Deck — конфиги кладутся в переменные окружения: + +```bash +# Нужно получить от DevOps: +KC_CLIENT_ID=white.nodejsk8s.dev.nubes.ru # имя нашего клиента в KK (уточнить) +KC_CLIENT_SECRET=<секрет> # из Deck / у DevOps +JWKS_URL=https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs +# ИЛИ если через auth-api: +# JWKS_URL=https://auth-api.ngcloud.ru/.well-known/jwks.json + +# В payg-report переменная называется: +IDP_CLIENT_SECRET=<секрет> +``` + +--- + +## Что сейчас в нашем коде + +Файл: `src/auth.js` + +| Режим | Условие | Поведение | +|---|---|---| +| **DEV_MODE** | `DEV_MODE=true` и не production | Авторизация пропущена, юзер захардкожен из `MOCK_USERS` | +| **JWKS_URL задан** | `JWKS_URL != ''` | Валидирует Bearer-токен из заголовка через JWKS | +| **Мок RS256** | Ничего из выше | Генерирует собственную RSA-пару, выдаёт и валидирует JWT сам | + +**Чего не хватает для продакшена:** +- Authorization Code Flow (редирект на KK и обратно) +- Обмен `code` → `access_token` через `/token` +- Session/cookie для хранения токена между запросами +- Refresh токена + +--- + +## Пример SSO flow из payg-report (ключевые строки) + +```javascript +// Настройка (из окружения): +const client_id = process.env.IDP_CLIENT_SECRET; // их название +const auth_endpoint = 'https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth'; +const token_endpoint = 'https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token'; +const certs_url = 'https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs'; + +// 1. Нет сессии → redirect на KK: +location(idp.buildRedirectToAuthURL({scope: 'openid profile email', state: guid})); + +// 2. KK вернул ?code=... → меняем на токен: +const resp = idp.makeAccessTokenRequest(url.code); + +// 3. Парсим JWT из ответа: +const token_data = jwt.decode(resp.access_token, idpCertificate, 'RS256'); +session.auth.wz = token_data.ClientID; // WZ-номер компании +session.auth.login = token_data.preferred_username; + +// 4. Refresh когда прошло > 2/3 времени жизни (только на GET): +if (timeSinceIat > refresh_expires_in / 1.5 && method === 'GET') { + const resp = idp.refreshAccessTokenRequest(session.auth.refresh_token); + // ... обновляем сессию +} +``` + +--- + +## Открытые вопросы (нужен ответ от DevOps/KK-команды) + +| # | Вопрос | Влияет на | +|---|---|---| +| 1 | Какой `client_id` выдан нашему сервису в KK realm `cloud`? | Конфиг | +| 2 | Где взять `client_secret`? | Конфиг | +| 3 | Токен от KK или от auth-api? Соответственно — какой JWKS_URL? | `src/auth.js` | +| 4 | Claim для admin-роли: чек-бокс — это `realm_roles`, `resource_access`, attribute? | `src/auth.js` requireAdmin | +| 5 | `ClientID` в токене — строка или массив (multi-company)? | `src/queries.js`, UI | +| 6 | `/export` — нужна ли авторизация или только сетевое ограничение по IP? | `src/routes/export.js` | + +--- + +## API Gateway (для справки) + +Из `docs/auth-architecture.md`: +- Gateway: `lk-api-gateway.ngcloud.ru` +- JWT из localStorage портала: `authApiTokens.access_token` +- **DDOS-Guard cookies обязательны** при curl-тестировании: `__ddg1_`, `__ddg8_`, `__ddg9_`, `__ddg10_` + +```bash +# Пример рабочего curl (2026-05-29): +curl -H "Authorization: Bearer " \ + -H "Origin: https://deck.ngcloud.ru" \ + -H "Cookie: __ddg1_=...; __ddg8_=...; __ddg9_=...; __ddg10_=..." \ + "https://lk-api-gateway.ngcloud.ru/api/v1/..." +```