Перейти к содержанию

Руководство по настройке OIDC / SSO

Как подключить корпоративный identity-провайдер (Keycloak, Okta, Azure AD / Entra ID, Google Workspace, GitLab или любой стандартный OIDC-провайдер) к платформе, чтобы пользователи входили без отдельного пароля платформы.

Подключение аддитивное: вход по паролю + TOTP-MFA продолжает работать для каждой организации, пока вы явно не включите «обязательный OIDC» (см. §6). Security-дизайн возможности (линковка аккаунтов, JIT-provisioning, композиция MFA, CSRF/PKCE) описан в ACCESS_CONTROL.md; этот документ — операционный how-to.

0. Org-scoped или платформенный — выберите сначала

Есть два независимых способа настройки OIDC; большая часть гайда общая — различаются владелец конфигурации и судьба пользователя, созданного через JIT-provisioning:

Org-scoped Платформенный
Где настраивается Organizations → (раскрыть org) → вкладка SSO / OIDC, или /api/orgs/{org_id}/oidc-providers System → SSO, или /api/system/oidc-providers
Кто настраивает owner/admin организации только platform_admin
План-гейт требуется возможность sso в плане org нет
Куда попадает JIT-пользователь в эту org, с ролью из role mapping/default_role ни в какую org — только глобальная роль user; менеджер позже назначит членство
«Require SSO» (отключение паролей) Поддерживается, per-org Не предоставляется — «все пользователи платформы» требует отдельного дизайна, а не переиспользования per-org тумблера
Целевая форма Несколько независимых клиентских организаций на одном экземпляре, каждая со своим IdP и границей изоляции (MSSP/SaaS) Одна компания, несколько внутренних команд под одной org — SSO просто работает на всю платформу, без отдельного OAuth-клиента на команду

Выбирайте платформенный, если это развёртывание одной компании (типовой случай): настройте один раз, и каждый новый сотрудник входит через корпоративный IdP с первого дня; кто-нибудь с правами org-админа потом разложит людей по org/командам из Organizations. Org-scoped — только если вы реально обслуживаете несколько разных клиентских организаций, которые должны быть изолированы друг от друга (MSSP, перепродающий платформу): каждый клиент настраивает своего провайдера, и JIT-пользователи никогда не окажутся в чужой org.

Способы сосуществуют: пользователь, вводящий @company.com на экране входа, видит кнопку каждого включённого провайдера, подходящего под домен. Провайдеры без email_domain — catch-all («вся компания, любой адрес»): платформенный виден на экране входа сразу, org-scoped сопоставляет любой введённый email или имя пользователя. Работает и ввод имени пользователя без @: бэкенд подставит email пользователя и сопоставит по его домену — операторы входят так, как привыкли думать.

Дальше описан org-scoped флоу; каждое поле и шаг идентичны для платформенного пути, кроме: нет default_role/role_mapping/enforced (§5, §8 не применимы), нет проверки плана, база эндпоинтов — /api/system/oidc-providers, и вызывать может только platform_admin.


0b. Предварительные требования

  • Org-scoped: вы owner или admin организации (org-роль, не глобальная), и план организации содержит возможность sso (тиры team/pro по умолчанию не содержат; enterprise содержит — админ может выдать sso и напрямую через PATCH /api/marketplace/plans/{org_id}). Без неё создание провайдера вернёт 403.
  • Платформенный: вы platform_admin. Проверки плана нет.
  • Определите PUBLIC_BASE_URL (env бэкенда) до регистрации redirect URI у IdP — см. §1. В multi-node HA задавайте идентичное значение на каждой app-ноде — см. HA_DEPLOYMENT.md. Иначе redirect_uri, который платформа отправляет при входе, может отличаться от того, куда реально приходит колбэк IdP, и exact-match OAuth по redirect_uri разорвёт круг.
  • Если IdP self-hosted в приватной/внутренней сети (недостижим из интернета) — см. заметку allow_private_issuer в §3: SSRF-guard платформы блокирует его по умолчанию.

1. Сначала задайте PUBLIC_BASE_URL

Платформа строит OIDC redirect_uri сама — он никогда не принимается ни от браузера, ни из конфига провайдера (это полностью закрывает open-redirect). Он выглядит так:

{PUBLIC_BASE_URL}/api/auth/oidc/{provider_id}/callback

Если PUBLIC_BASE_URL не задан, бэкенд берёт host входящего запроса — годится для локальных тестов, но небезопасно за прокси без строгого контроля Host. В production задавайте явно:

# .env
PUBLIC_BASE_URL=https://umbryx.example.com

После смены перезапустите бэкенд. Диалог создания заранее генерирует id провайдера (§3), поэтому точный callback-URL виден и копируется прямо во время заполнения формы — сначала зарегистрируйте его у IdP, затем Create. Строка каждого провайдера потом несёт тот же URL с кнопкой копирования, так что перерегистрация не превращается в поиск идентификаторов.


2. Зарегистрируйте платформу как клиента у IdP

Универсальные шаги; точные названия меню — в заметках по вендорам ниже.

  1. Создайте OIDC / OAuth2 confidential client (не «public»/SPA — платформа хранит client_secret).
  2. Выдайте ему grant type Authorization Code. PKCE платформа использует всегда в дополнение к client secret — если у IdP есть тумблер «PKCE required», включить можно и рекомендуется.
  3. Scopes: минимум openid, email, profile. Добавьте scope, отдающий членство в группах/ролях (у Keycloak roles, у Azure AD groups), если планируете role mapping (§5).
  4. Финального redirect URI ещё нет (нужен id провайдера) — зарегистрируйте заглушку или вернитесь сюда после §3. Формат всегда: {PUBLIC_BASE_URL}/api/auth/oidc/{provider_id}/callback.
  5. Запишите issuer URL, client ID и client secret — все три нужны в §3. Issuer — база, по которой резолвится {issuer}/.well-known/openid-configuration; у большинства IdP она видна прямо на странице клиента.

3. Создайте провайдера на платформе

Через UI: Organizations → (раскрыть org) → вкладка SSO / OIDC → + Add Provider.

SSO-провайдеры организации

Диалог показывает Callback URL с кнопкой копирования прямо под полем Issuer — URL окончателен ещё до сохранения. В строке каждого провайдера — тот же URL и кнопка Test sign-in, открывающая настоящий флоу входа в новой вкладке (в строке отметится last verified sign-in после успешного круга).

Поля:

Поле Примечания
Отображаемое имя Показывается на экране входа как «Sign in with {name}»
Issuer URL например https://your-org.okta.com — эндпоинты authorization/token/userinfo/JWKS обнаруживаются автоматически из {issuer}/.well-known/openid-configuration, вводить вручную не нужно
Client ID / Client secret Из §2. Секрет шифруется at rest и больше не показывается
Email domain Опционально — экран входа предложит этот провайдер при вводе @your-org.com ещё до аутентификации
Роль по умолчанию Org-роль (viewer, analyst, operator, …) для пользователя без подходящего group-mapping (§5)
JIT provisioning См. §4
Require platform MFA См. §6
Allow private issuer Выключен по умолчанию — включайте только для self-hosted IdP во внутренней сети (ниже). Только platform-admin, даже в org-scoped форме.

Или через API:

curl -X POST https://umbryx.example.com/api/orgs/$ORG_ID/oidc-providers \
  -H "Content-Type: application/json" -b cookies.txt \
  -d '{
    "name": "Corp Okta",
    "issuer_url": "https://your-org.okta.com",
    "client_id": "0oa1b2c3d4e5",
    "client_secret": "...",
    "email_domain": "your-org.com",
    "default_role": "viewer",
    "jit_provisioning_enabled": true,
    "require_platform_mfa": false
  }'

Платформенный вариант (без default_role/role_mapping, без $ORG_ID, нужна сессия platform_admin):

curl -X POST https://umbryx.example.com/api/system/oidc-providers \
  -H "Content-Type: application/json" -b cookies.txt \
  -d '{
    "name": "Corp GitLab",
    "issuer_url": "https://gitlab.your-company.com",
    "client_id": "...",
    "client_secret": "...",
    "email_domain": "your-company.com",
    "jit_provisioning_enabled": true,
    "require_platform_mfa": false
  }'

Если discovery падает (400 OIDC discovery failed: ...) — проверьте issuer URL: это должна быть точная база, под которой живёт документ .well-known/openid-configuration, без лишних сегментов пути. Issuer (и всё, что находит discovery — эндпоинты token/JWKS/userinfo) проверяется SSRF-guard платформы; issuer, резолвящийся в приватный/внутренний адрес, по умолчанию отвергается.

Self-hosted IdP в приватной сети: если GitLab/Keycloak/… достижим только через внутренний DNS и резолвится в RFC1918 (нормально для on-prem IdP за VPN/офисным файрволом), discovery падает с Target hostname resolves to private/internal address. Поставьте "allow_private_issuer": true в payload создания/обновления (или галочку в UI) — точечный opt-out конкретного провайдера из SSRF-guard. Это намеренно узкий per-provider override, а не глобальная настройка: выбор, куда платформа отправляет исходящие запросы — именно та граница доверия, ради которой guard существует на multi-tenant- экземпляре, поэтому выставление всегда требует сессии platform_admin, даже при работе через /api/orgs/{org_id}/oidc-providers — org owner/admin в одиночку получит 403. Проверьте и путь DNS-резолва, если discovery ведёт себя неожиданно даже с этим флагом: контейнерный DNS-резолвер имеет значение — если используется публичный DNS, а хост IdP резолвится только (или иначе, split-horizon) на внутреннем резолвере, discovery может падать или молча попадать не туда. Сервис backend.dns из docker-compose.yml принимает DNS_SERVER_1/ DNS_SERVER_2 именно для этого — укажите внутренний резолвер.

Ответ содержит id нового провайдера. Теперь вернитесь к IdP (§2 шаг 4) и проставьте реальный redirect URI:

{PUBLIC_BASE_URL}/api/auth/oidc/{provider_id}/callback

4. Новые и существующие пользователи (JIT и линковка аккаунтов)

  • JIT provisioning ВКЛ: первый вход человека без подходящего аккаунта создаёт его — глобальная роль всегда user (никогда platform_admin); org-scoped провайдеры дополнительно вступают в org с ролью из group/role mapping (§5) или default_role; платформенные не вступают ни в какую org (§0). Имя пользователя нового аккаунта берётся из стандартного OIDC-клейма preferred_username, если IdP его присылает (GitLab, Keycloak и Okta присылают), с запасным вариантом — local-part email. Это важно: два человека с одинаковым началом адреса иначе дрались бы за один base-username с произвольным суффиксом name2/name3, хотя у IdP уже был нормальный уникальный handle.
  • JIT provisioning ВЫКЛ: первый вход без аккаунта отвергается («no account found and self-signup is disabled») — используйте, если хотите создавать аккаунты сами (например, через POST /api/orgs/{org_id}/users) и пускать по SSO только известных.
  • Существующий аккаунт с тем же email: никогда не линкуется автоматически, независимо от JIT. Пользователь видит экран подтверждения и обязан один раз ввести пароль этого аккаунта — это осознанно: иначе любой, чей email принимается за легитимный у менее доверенного IdP, мог бы захватить существующий аккаунт по совпадению email.

5. Маппинг групп/ролей в org-роли (опционально)

Если IdP присылает claim групп/ролей (Keycloak: roles под кастомным правилом сопоставления; Azure AD: groups — включается опцией Group Claims в регистрации приложения; Okta: настройте claim groups на authorization server), конкретные значения можно смапить в org-роли, чтобы JIT- и заново линкованные пользователи сразу получали нужные права:

{
  "claim_mapping": {"email": "email", "name": "name", "groups": "groups"},
  "role_mapping": {"platform-admins": "admin", "analysts": "analyst"},
  "default_role": "viewer"
}

В claim_mapping нужны только claims, чьи имена у IdP отличаются от OIDC-стандартных (email, name, groups) — совпадающее не пишите.

Группа из токена, которой нет в role_mapping, никогда не даёт больше default_role — запасного правила «дадим что-нибудь разумное» для неизвестных групп by design нет.


6. Платформенная MFA поверх OIDC

Корпоративные IdP обычно требуют свою MFA — нужна ли дополнительно платформенная TOTP-MFA после успешного OIDC-входа — это per-provider тумблер (require_platform_mfa), а не глобальная политика:

  • Выкл (по умолчанию): валидного OIDC-входа достаточно — IdP доверено провести свою MFA.
  • Вкл: после успеха OIDC пользователь с уже включённой платформенной MFA (Settings → Security → Enable MFA) проходит обычный шаг /api/auth/mfa/verify до получения сессии — тот же механизм, что у MFA при входе по паролю. Пользователь без настроенной платформенной MFA этим тумблером не блокируется — второй фактор добавляется только тем, кто его уже включил.

7. Проверка перед доверием

Просто войдите через него один раз — с экрана входа: введите @your-domain.com и нажмите предложенную кнопку «Sign in with {name}», или откройте напрямую {PUBLIC_BASE_URL}/api/auth/oidc/{provider_id}/login. Любой полный успешный круг (любой пользователь, не специальный «тестовый режим») записывает на провайдере verified-отметку — видна во вкладке SSO/OIDC как «last verified sign-in: ...».

Эта отметка — жёсткое предусловие для §8, шаг пропустить нельзя.


8. Обязательный SSO («obligatory OIDC»)

Включение Require SSO (disable passwords) у провайдера запрещает парольный вход всем участникам организации (platform_admin всегда исключены — платформу нельзя запереть).

Тумблер отвергается с 400, пока через этого конкретного провайдера не прошёл хотя бы один verified-вход из §7 — это не ошибка: сломанная конфигурация (неверный client secret, недостижимый issuer, неверный redirect URI у IdP) иначе запирала бы всю org в момент щелчка.

Откат (например, IdP лежит и никто не входит) — выключите тумблер через API (или попросите platform admin, которому org всегда доступна), парольный вход вернётся сразу:

curl -X PATCH https://umbryx.example.com/api/orgs/$ORG_ID/oidc-providers/$PROVIDER_ID \
  -H "Content-Type: application/json" -b cookies.txt \
  -d '{"enforced": false}'

Заметки по вендорам

IdP Форма issuer URL Где искать
Keycloak https://{host}/realms/{realm} Realm settings → General → Endpoints → OpenID Endpoint Configuration (issuer — база, без /.well-known/...)
Okta https://{your-org}.okta.com или https://{your-org}.okta.com/oauth2/{authServerId} для кастомного authorization server Security → API → Authorization Servers
Azure AD / Entra ID https://login.microsoftonline.com/{tenant-id}/v2.0 App registration → Endpoints → OpenID Connect metadata document (issuer — база)
Google Workspace https://accounts.google.com Фиксирован, одинаков для всех тенантов
GitLab (gitlab.com или self-hosted) https://gitlab.example.com (self-hosted) или https://gitlab.com User Settings → Applications (или Admin Area → Applications для клиента на весь экземпляр) — создайте application со scope openid, email, profile, отметьте Confidential. GitLab присылает preferred_username — реальное имя пользователя GitLab (см. §4) и groups/groups_direct для role mapping.

Групповые claim — самое различающееся между вендорами:

  • Keycloak: добавьте правило сопоставления «Group Membership» или «User Realm Role» в dedicated scope клиента, назовите claim groups (или укажите имя в claim_mapping.groups).
  • Azure AD: App registration → Token configuration → Add groups claim. Azure по умолчанию присылает object ID групп, а не имена — сопоставляйте ID в role_mapping или переключите claim на display names, если тенант позволяет.
  • Okta: Security → API → Authorization Servers → ваш сервер → Claims → добавьте claim groups с фильтром по нужным группам.
  • Google Workspace: групповые claims требуют Cloud Identity/Admin SDK и через обычные OIDC-scopes не отдаются — role mapping с Google практически нереален; используйте default_role и управляйте org-ролями вручную.

Устранение неполадок

Симптом Вероятная причина
OIDC discovery failed при создании/обновлении Неверный issuer, или discovery-эндпоинт IdP недостижим из бэкенда (проверьте сеть/egress — исходящие запросы идут через SSRF-guard, адрес из приватной сети отвергается)
Target hostname resolves to private/internal address Issuer (или найденный эндпоинт) резолвится в RFC1918/loopback — либо это реально self-hosted внутренний IdP (поставьте allow_private_issuer, §3 — нужен platform_admin), либо DNS бэкенда отдаёт неверный (публичный) адрес для split-horizon хоста (проверьте DNS_SERVER_1/DNS_SERVER_2)
redirect_uri mismatch у IdP или колбэк попадает не на ту ноду в HA PUBLIC_BASE_URL не идентичен на всех app-нодах — см. §0b
Вход редиректит обратно с oidc_error=... во фрагменте URL Сообщение в фрагменте человекочитаемо; причина — в audit-логе бэкенда (события oidc.failed): state_expired, state_cookie_mismatch, ошибка на стороне IdP и т.п.
«No account found and the organization does not allow self-signup» JIT выключен и под email нет аккаунта — включите JIT или создайте аккаунт заранее
Новому JIT-пользователю не та роль Сверьте role_mapping с фактическим значением claim — при сомнении один раз залогируйте raw claims (временно): значения групп (особенно object ID Azure AD) редко совпадают с тем, что видно в админке IdP
Не включается «Require SSO» Сначала пройдите один успешный вход именно через этого провайдера — шаг не пропускается (§7/§8)