# 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/..." ```