research: auth-flow.md — полная документация SSO, KK endpoints, JWT claims, открытые вопросы
This commit is contained in:
@@ -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 <JWT>" \
|
||||||
|
-H "Origin: https://deck.ngcloud.ru" \
|
||||||
|
-H "Cookie: __ddg1_=...; __ddg8_=...; __ddg9_=...; __ddg10_=..." \
|
||||||
|
"https://lk-api-gateway.ngcloud.ru/api/v1/..."
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user