Федеративная аутентификация и провижининг пользователей (Auth Federation)
Дата: 2026-10-16. Отвечает на блокер «500 пользователей»: список, структура, массовые операции, синхронизация с корпоративными каталогами и интеграция с корпоративной аутентификацией (Keycloak, AD / LDAP, IdP по SAML/OIDC).
#1. Цель
Подключить Temporality к корпоративной аутентификации одним интеграционным слоем навсегда: независимо от того, что стоит у заказчика — Keycloak, Active Directory, ADFS, Entra ID или зоопарк из нескольких IdP — на нашей стороне код не меняется.
Ключевой принцип документа: ядро говорит только на OIDC. Всё остальное — протоколы, маппинг атрибутов, ротацию сертификатов, quirks конкретных IdP — решает либо существующий IdP заказчика, либо identity-брокер (Authentik, Keycloak), подключённый рядом как конфигурация, а не как наш код.
Второй принцип: список пользователей — проекция IdP, а не мастер-копия. Пользователи появляются при первом входе (JIT-provisioning), роли и подразделения выводятся из клеймов по редактируемой таблице маппинга, исчезновение из каталога деактивирует учётную запись. Ручное заведение сотен записей исчезает как класс.
#2. Проблематика, которую решала фаза 1 (история)
До фазы 1: аутентификация была только локальной (env-токены, токены
workspace-пользователей, POST /v1/auth/login name-or-email + bcrypt), SSO
не было, провижининг — ручной, ListUsers без пагинации и массовых
операций. Фаза 1 закрыла SSO/JIT (см. §4–§5); пагинация/фильтры и массовые
операции остались на будущее (§8).
- Секреты IdP негде хранить. Скоро появится client secret — ему место в
существующем контуре секретов (
docs/secrets-management.md), а не в конфиг-файле рядом с кодом.
#3. Принцип: один протокол на нашей стороне
#3.1. Разделение проблем
Вопрос «интеграция с корпоративной аутентификацией» — это три разные проблемы с разными решениями:
| Проблема | Решение | Где живёт |
|---|---|---|
| Аутентификация (кто ты) | OIDC: Authorization Code + PKCE | Один слой в ядре |
| Зоопарк протоколов (LDAP, SAML, несколько IdP) | Identity-брокер перед нами | Конфигурация стенда, не наш код |
| Провижининг (создание/жизненный цикл, 500 юзеров) | JIT + reconcile | Небольшой слой в ядре |
#3.2. Топологии — один и тот же код
- Малая команда без IdP. Как сегодня: локальные пароли/токены, ноль конфигурации. SSO просто не настроен.
- Корпорация с OIDC-IdP (Keycloak, Entra ID, Okta, Google). Temporality — обычный OIDC-клиент этого IdP. Брокер не нужен.
- Корпорация с AD/LDAP или смешанной средой. Рядом ставится брокер (рекомендуем Authentik, см. §10), который федерит источники и предъявляет нам единый OIDC. Вся боль LDAP-схем, SAML-метаданных и сертификатов — в его админке.
#3.3. Чего сознательно нет в ядре
- Коннекторов LDAP/SAML в нашем коде. SAML — XML, метаданные, различия ADFS/Okta/Ping; LDAP — своя схема у каждого каталога, и он плохо дружит с API-аутентификацией. Всё это уже решено брокерами; каждый нативный коннектор — это матрица тестов × IdP навсегда.
- OAuth2-proxy / auth-proxy-сайдкары. API потребляют наш SPA и агенты напрямую; валидация на Gate чище и универсальнее, чем доверять заголовкам прокси.
- Хранения корпоративных паролей. При включённом SSO локальный пароль остаётся только у break-glass админа (§5.6).
- SCIM-сервера в первых фазах. Дорого ради того, что JIT закрывает бесплатно; фаза 3, только по требованию реального заказчика.
#4. Основные понятия
- IdP (Identity Provider) — система, аутентифицирующая пользователя и выдающая токены: Keycloak, Entra ID, ADFS, Authentik.
- Identity-брокер — сервис-прослойка (Authentik, Keycloak), который федерит несколько источников (LDAP, SAML, OIDC) и предъявляет приложению один OIDC-интерфейс.
- OIDC — протокол поверх OAuth2:
authorization_code+ PKCE для браузерных клиентов; идентификация поid_token(JWT), ключи IdP — JWKS. - JIT-provisioning — создание записи
workspace_userпри первом успешном входе: IdP уже подтвердил личность, нам остаётся связать её с внутренней моделью. - External identity — пара (
issuer,subjectиз клеймов) — единственный ключ связи пользователя с IdP; хранится вworkspace_user.external_id/external_issuer. - Маппинг клеймов — редактируемая таблица «значение клейма → роль Temporality / подразделение». Продуктовая логика Temporality, о которой брокер не знает.
#5. Архитектура аутентификации
#5.1. Поток входа SPA (Authorization Code + PKCE)
SPA ──1. GET /v1/auth/oidc/start ──────────────► ядро
◄─ 2. 302 {issuer}/authorize?client_id&PKCE&state&nonce
SPA ──3. логин на IdP/брокере (AD/Keycloak/…)
IdP ──4. 302 redirect /v1/auth/oidc/callback?code&state ──► ядро
ядро: 5. обмен code на токены (backchannel)
6. валидация id_token (issuer, aud, nonce, подпись по JWKS)
7. JIT + маппинг (§6) + аудит входа
◄─ 8. Set-Cookie: httpOnly-сессия; 302 в SPA
SPA ──9. дальше все запросы с cookie-сессией
Ядро — единственная сторона, знающая client secret; SPA секретов не видит.
state/nonce одноразовые, живут в коротком окне в store (таблица
auth_request или переиспользуемый кэш с TTL).
#5.2. Сессии
- Таблица
auth_session: id (случайный, ≥256 бит), user_id, created_at, last_seen_at, expires_at, ip, user_agent. Cookietm_session— httpOnly, SameSite=Lax, Secure за TLS. - TTL доступа 12 ч, скользящее продление; абсолютный потолок 7 дней, после которого — повторный вход через IdP.
- Выход:
POST /v1/auth/logout— удаление сессии + (опционально) redirect наend_session_endpointIdP. - SSE/EventSource работают с cookie автоматически (same-origin) — хак
?token=остаётся только для статических токенов API.
#5.3. Gate: четвёртый тип принципала
Gate.Authenticate сейчас различает env-токены, DB-токены и loopback.
Добавляется проверка cookie-сессии: валидная сессия → Principal из
workspace_user по тому же пути, что PrincipalForUser
(workspace/tokens.go) — роли, проекты, орг-цепочка переиспользуются как
есть. Порядок проверки: env/DB Bearer → loopback → сессия. JWT-валидация
на каждый запрос не нужна: токен проверяется один раз при обмене code,
далее живёт наша сессия.
Опционально (фаза 2+) — приём access-токенов IdP как Bearer (валидация по JWKS) для не-браузерных клиентов, которым выдаёт токены сам IdP. Для CLI и агентов по умолчанию остаются статические/DB токены: они уже есть и не требуют участия человека.
#5.4. JIT и клеймы
Минимальный контракт клеймов: sub (обязателен), preferred_username или
email (имя для отображения), опционально groups / roles (массив или
строка — нормализуем). Нестандартные имена клеймов настраиваются в
конфиге IdP-подключения (§5.5).
#5.5. IdP конфигурация
Таблица auth_idp_config (фаза 1 — одна запись):
enabled,issuer,client_id,client_secret_ref— секрет как reference в контуреdocs/secrets-management.md(secret://…), значение разрешается на границе использования;display_name(кнопка «Войти через …»),scopes(по умолчаниюopenid profile email);claim_names— переопределения имён клеймов (groups/username/email);default_roleдля JIT-пользователей без маппингов (reader; вариантdeny— вход успешен, доступ запрещён до ручного маппинга);jit_enabled(bool) — можно ли создавать пользователей автоматически;- discovery по
{issuer}/.well-known/openid-configuration, JWKS-кэш с refresh поcache-control/ при неизвестномkid(ротация ключей без рестарта ядра).
Редактирование — admin-only API + UI, изменение пишет аудит-событие.
#5.6. Break-glass
Локальный вход паролем не отключается, а ограничивается: при включённом SSO
он остаётся доступен только пользователям с ролью admin. Это спасает при
недоступности IdP. У admin-пользователя локальный пароль задаётся заранее.
Пароль SSO-пользователя без локального он может установить сам
(POST /v1/auth/password с пустым текущим) — принципал уже аутентифицирован
IdP, повторной проверки нечему проводить.
#6. Провижининг и жизненный цикл
#6.1. Создание: JIT
Первый успешный вход:
- Ищем
workspace_userпо (external_issuer,external_id). - Не нашли и
jit_enabled→ создаём: имя/email из клеймов, роль =default_role,sourced='federated', активен. Аудит-событиеauth.user.jit_created. - Нашли → обновляем имя/email из клеймов (руками их больше не правят), проверяем активность.
#6.2. Роли и подразделения: маппинг клеймов (фаза 2, не реализовано)
Таблица auth_claim_mapping (claim, value, install_role,
org_unit_id): правило «если в клеймах есть значение — дать роль /
привязать к подразделению». Пока реализованы только переименования клеймов
(username_claim/email_claim/groups_claim).
- Эффективная инсталляционная роль = максимум по всем сработавшим правилам,
но не ниже
default_role; орг-привязка — последнее сработавшее правило (при конфликте правит админ вручную, событие в аудите). install_roleможет быть пустым — правило только привязывает подразделение (сотрудник отдела), роль возьмётся из другого правила.- Маппинги редактируются в UI (admin) и применяются при каждом входе:
смена группы в каталоге → смена роли при следующем логине. Ручная смена
роли у
sourced='federated'пользователя запрещена (аудит объяснит, из какой группы пришла роль). - Дополнительные роли на подразделениях (
org_unit_roles) остаются ручными — Temporality-специфика.
#6.3. Деактивация: reconcile
Раз в час (настраиваемо) — фоновая задача:
- Пользователи
sourced='federated'соlast_login_atстарше порога (по умолчанию 30 дней) перепроверяются у IdP (если есть userinfo/introspection или групповый клейм недоступен — помечаются кандидатами на деактивацию). - Исчезнувший из каталога (по groups-клейму или явному ответу IdP) →
active=false, аудитauth.user.deactivated, сессии гасятся. - Удаление не выполняется никогда: на пользователей ссылаются раны, аудит, inbound-сообщения. Реактивация — повторным входом.
Практика: пока IdP не отдаёт групповое членство по API, reconcile ограничивается expire-политикой сессий и ручной деактивацией — это честно фиксируется в критериях фазы 2.
#6.4. Локальные поля
Каналы уведомлений, preferred channel, подписи в чатах остаются локальными
полями workspace_user, ключ — external_id. Прямое редактирование
федеративных полей (имя/email/роль) в UI скрывается.
#7. Модель данных
Таблицы auth_* живут в squashed baseline (migrations/workspace/ 0001_baseline_v2026.2.21); номера ниже — исторический план:
auth_oidc:auth_idp_config— см. §5.5, одна строка (id=1).auth_session— сессии (§5.2).auth_request— одноразовые state/nonce (TTL-очистка).
- 000068
auth_claim_mapping— маппинги клеймов. - 000069
workspace_user_federation:external_issuer,external_id(уникальная пара, NULL для локальных);sourced(local|federated);last_login_at.
Secret для client_secret_ref — обычный Secret ресурс
(docs/secrets-management.md §3): значение разрешается ядром на границе
использования — та же механика, что у креденшелов провайдеров моделей
(resolveProviderAPIKey).
#8. API
GET /v1/workspace/auth/config(admin) /PUT— конфиг IdP;GETвозвращает всё, кроме значения секрета.POST /v1/workspace/auth/config/test— проверка discovery + JWKS доступности (без полного логина).GET /v1/auth/oidc/start— редирект на IdP (генерирует state/nonce).GET /v1/auth/oidc/callback— обмен кода, создание сессии, редирект в SPA.POST /v1/auth/logout— выход (сессия + опционально end_session IdP).GET /v1/auth/methods— какие способы входа доступны (sso: вкл/выкл, display_name; local: вкл/выкл) — для экрана логина.- Пагинация пользователей:
GET /v1/workspace/usersполучаетq,role,org_unit,active,limit/offset… ответ —{users, total}(фаза 2, покаListUsersвозвращает всех без фильтров). Массовые операции (admin):POST /v1/workspace/users/bulk—{action: activate|deactivate|set_role, ids: […]}.
Все новые события пишутся в access audit (auth.login.succeeded,
auth.login.failed, auth.user.jit_created, auth.user.deactivated,
auth.config.updated, auth.logout).
#9. UI
- Экран входа: кнопка «Войти через {display_name}» (если SSO включён) +
локальный вход (admin break-glass или SSO выключен). Источник —
/v1/auth/methods. - Onboarding: опциональный шаг «Подключить корпоративный SSO» после создания первого проекта — как чек-лист с проверкой discovery, не блокирует онбординг.
- Страница Users: пагинация/поиск/фильтры (роль, подразделение, активность, источник local/federated); массовые операции чекбоксами; редактирование маппингов клеймов отдельным блоком (таблица правил) с кнопкой «проверить на текущем пользователе».
- Настройки SSO (admin): форма конфига IdP, статус (discovery ок, JWKS обновлены), тестовый логин.
- SPA после OIDP-входа работает на cookie-сессии;
authHeaders()вdebugger/src/api.tsостаётся для переходного периода (статические токены).
#10. Брокеры: что рекомендовать
| Когда | Почему | |
|---|---|---|
| Authentik | Дефолт для стендов без готового IdP | Лёгкий, простая админка, из коробки федерит LDAP/SAML/OIDC-источники с шедульным синком, MIT |
| Keycloak | Корпоративный стандарт «только утверждённый софт» | Часто уже развёрнут — тогда брокер не нужен вовсе; JVM/тяжелее, но админы его знают |
| Zitadel | Go-стек, мультитенантность | Технически хорош, в корпорациях менее заезжен |
Мы не тащим брокера в свой compose как обязательность: дефолт — «подключите к вашему IdP», опция — «вот готовый Authentik рядом» (пример compose-профиля в docs, не в основном стеке).
#10.1. Dev-стенд с Authentik
docker-compose.authentik.yml в корне репозитория — опциональный спутник
для приёмки SSO на дев-стенде: свой postgres/valkey, своя сеть, к стеку
Temporality не привязан вовсе. Запуск:
docker compose -f docker-compose.authentik.yml up -d
# админка: http://localhost:9000 (akadmin; пароль — в логах server при
# первом старте: grep "Generated password")
Дальше в админке Authentik:
- Applications → Create → Web application: redirect URI
<origin стенда>/kernel-api/v1/auth/oidc/callback. - Скопировать client id/secret в Temporality: issuer
http://localhost:9000/application/o/<slug>/, client secret — через secret reference (secret://authentik/client). - Клеймы: Authentik из коробки отдаёт
preferred_usernameиemail— дефолты конфига Temporality менять не нужно.
Фаза 1 принимает стенд и с Keycloak: с точки зрения ядра это тот же OIDC-флоу, меняется только issuer.
#11. Безопасность и аудит
- Токены: подписи RS256/ES256 (
none/HS256 с открытым ключом — отвергаются), проверкаiss,aud,exp(clock skew ±60 c),nonce. - JWKS: кэш с TTL, принудительный refresh при неизвестном
kid— ротация ключей IdP не требует рестарта ядра. - Секреты: client secret только через secret reference; в событиях и логах — редация по контуру secrets-management.
- Сессии: httpOnly + Secure + SameSite; CSRF — SameSite=Lax + проверка Origin на мутирующих запросах; logout Everywhere при деактивации.
- Аудит: все события §8; события входа содержат issuer, способ (sso/ local), результат.
#12. Фазы
#Фаза 1 — OIDC-вход и JIT (реализовано, закрывает SSO-блокер)
- Миграции auth-таблиц (см. §7).
coreos/go-oidc(+golang-jwt): discovery, обмен code, валидация id_token, JWKS-кэш.- Сессии + cookie в
Gate.Authenticate; logout. - JIT +
default_role;auth/methods; экран входа SPA. - Admin API конфига IdP; client secret через secret reference.
- Dev-стенд: Authentik в отдельном compose-профиле для приёмки.
#Фаза 2 — жизненный цикл и масштаб (закрывает «500 пользователей»)
- Пагинация/поиск/фильтры
ListUsers+ UI-таблица. - Массовые операции (activate/deactivate/set_role).
- Маппинг клеймов → роли/подразделения + UI редактора.
- Reconcile-деактивация (порог last_login + группы, когда IdP даёт).
- CSV-импорт как переходный путь до включения SSO у заказчика.
- Локальные поля — только у локальных пользователей; у федеративных — только каналы.
#Фаза 3 — enterprise-хвосты (по требованию)
- Приём access-токенов IdP как Bearer на Gate (JWKS-валидация на запрос).
- SCIM 2.0 server (users/groups) — pre-provisioning без JIT.
- Несколько IdP одновременно (мультистрочность
auth_idp_config). - Нативный SAML-SP через
crewjam/saml— только под конкретного заказчика без права поставить брокера.
#13. Критерии готовности
#Фаза 1
- Вход через Keycloak и через Authentik (за LDAP-источником) на dev-стенде; пользователь создаётся при первом входе, роль = default_role.
- Повторный вход того же пользователя не создаёт дубль (ключ issuer+external_id).
- Сессия: TTL, logout, недоступность IdP не роняет уже выданные сессии (только новые входы).
- Локальный вход при включённом SSO работает только для admin.
- События входа в аудите; client secret не появляется ни в событиях, ни в ответах API.
- SSO выключен → система полностью как раньше (обратная совместимость).
#Фаза 2
- 500+ пользователей: список открывается < 1 c (пагинация), поиск по имени/email, фильтры работают.
- Массовая деактивация 50 пользователей — одна операция с аудит-записью.
- Смена группы в каталоге меняет роль при следующем входе (приёмка на Authentik: перенесли пользователя в группу → перелогин → роль обновилась).
- Ручная смена роли федеративного пользователя в UI невозможна.
#Фаза 3
- Access-токен IdP принимается как Bearer для API-вызова (без сессии).
- SCIM-create пользователя появляется в списке до первого входа.
- Два IdP одновременно: выбор на экране входа.
#14. Открытые вопросы
- Отдаёт ли целевой IdP групповое членство в клеймах (или нужен userinfo/groups API) — определяет честность reconcile в фазе 2.
- Нужен ли
prompt=loginдля критичных операций (re-auth перед сменой auth-конфига) — или достаточно текущей сессии + аудита. - Мультиарендность (несколько организаций на одной установке) — пока вне поля; конфиг IdP на установку один.
#15. Архитектурные ограничения
- Ядро не реализует LDAP- и SAML-коннекторы; единственный протокол федерации — OIDC. Изменение этого решения — отдельная спека с обоснованием под конкретного заказчика.
- Провижининг без Broker-specific кода: только стандартные OIDC-клеймы и редактируемый маппинг; SCIM — отдельная фаза, не подмешивается в ядро.
- Пользователи не удаляются физически; деактивация — единственный путь вывода из строя (целостность аудита и ранов).
- Client secret и прочие креденшалы федерации живут только в контуре secrets-management (reference, redaction); plaintext-секреты в конфиг-таблицах запрещены.
- Cookie-сессии не заменяют статические токены для машинных клиентов (триггеры, CLI, агенты) — это параллельные механизмы одного Gate.