Руководство по настройке 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 не задан, бэкенд берёт host входящего запроса —
годится для локальных тестов, но небезопасно за прокси без строгого
контроля Host. В production задавайте явно:
После смены перезапустите бэкенд. Диалог создания заранее
генерирует id провайдера (§3), поэтому точный callback-URL виден и копируется прямо во время заполнения формы — сначала
зарегистрируйте его у IdP, затем Create. Строка каждого провайдера
потом несёт тот же URL с кнопкой копирования, так что перерегистрация
не превращается в поиск идентификаторов.
2. Зарегистрируйте платформу как клиента у IdP¶
Универсальные шаги; точные названия меню — в заметках по вендорам ниже.
- Создайте OIDC / OAuth2 confidential client (не «public»/SPA —
платформа хранит
client_secret). - Выдайте ему grant type Authorization Code. PKCE платформа использует всегда в дополнение к client secret — если у IdP есть тумблер «PKCE required», включить можно и рекомендуется.
- Scopes: минимум
openid,email,profile. Добавьте scope, отдающий членство в группах/ролях (у Keycloakroles, у Azure ADgroups), если планируете role mapping (§5). - Финального redirect URI ещё нет (нужен
idпровайдера) — зарегистрируйте заглушку или вернитесь сюда после §3. Формат всегда:{PUBLIC_BASE_URL}/api/auth/oidc/{provider_id}/callback. - Запишите issuer URL, client ID и client secret — все
три нужны в §3. Issuer — база, по которой резолвится
{issuer}/.well-known/openid-configuration; у большинства IdP она видна прямо на странице клиента.
3. Создайте провайдера на платформе¶
Через UI: Organizations → (раскрыть org) → вкладка SSO / OIDC → + Add Provider.

Диалог показывает 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:
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) |