Corp/docs/guides/OIDC.md
Игорь Надеин 86fed54811
All checks were successful
Deploy production / deploy (push) Successful in 10m4s
Add OIDC back-channel logout for deactivated accounts
2026-10-27 20:01:26 +03:00

15 KiB
Raw Blame History

Корпоративный OpenID Connect

Back-Channel Logout при отключении сотрудника

Необязательный механизм включается настройкой OIDC_BACKCHANNEL_LOGOUT_ENABLED=true. Он не синхронизирует справочник сотрудников: провайдер запоминает только пары «сотрудник — OIDC-клиент», для которых прошёл вход. В админке суперпользователь может зарегистрировать для отдельного клиента HTTPS-адрес back-channel logout. Без регистрации адресов уведомления никуда не отправляются. Поддерживаются RS256-клиенты и выход всех сессий по sub; механизм с отдельными sid не реализован.

Раз в минуту задача Celery проверяет фактический статус запомненных сотрудников, включая кадровые события с будущей датой. Когда учётная запись отключена или увольнение вступило в силу, для каждого зарегистрированного клиента создаётся запись очереди. Отправляется подписанный JWT logout_token согласно OIDC Back-Channel Logout. Временные ошибки сети, 408, 429 и 5xx повторяются с увеличением интервала; остальные HTTP-ошибки отмечаются как неудачные в админке, где суперпользователь может запустить повтор. Если сотрудник восстановлен до отправки, устаревшее уведомление отменяется.

Для входов, состоявшихся до включения механизма, можно дополнить список по ещё сохранённым OIDC-токенам: команда python manage.py seed_backchannel_identities показывает количество, а с --apply добавляет отсутствующие записи. Это не восстанавливает информацию об уже удалённых или очищенных токенах; старые сессии таких пользователей требуют отдельной проверки или повторного входа.

Корректное закрытие сессий, собственных токенов и SSH-ключей зависит от каждого подключённого приложения; регистрация адреса не подтверждает его совместимость. При передаче уведомления в момент повторного приёма возможна гонка с новой сессией: после восстановления в случае её закрытия потребуется повторный вход.

Портал выступает доверенным OpenID Connect Provider для внутренних приложений. Первое подключение — Gitea; тот же контракт можно использовать для других клиентов с поддержкой OIDC Authorization Code Flow.

Контракт

  • issuer: https://corp.example.com/o
  • discovery: https://corp.example.com/o/.well-known/openid-configuration
  • flow: Authorization Code
  • подпись ID token: RS256
  • client authentication: client_secret_basic или client_secret_post
  • scopes: openid, profile, email, groups
  • идентификатор пользователя sub: неизменяемый Django PK сотрудника
  • рекомендуемый логин: claim preferred_username из LDAP username; fallback employee-<pk>

Пароль портала стороннему приложению не передаётся. Обычный JWT-вход портала создаёт короткую browser session только перед возвратом на /o/authorize/. Redirect из bridge ограничен собственным authorization endpoint и не может увести пользователя на произвольный сайт.

Неактивный, уволенный или не подтвердивший email сотрудник не может обменять authorization code, использовать access token либо обновить токен. Внешний клиент получает только claims из согласованных scopes.

Production-настройка портала

OIDC и callback клиентов должны работать по HTTPS. Ключ подписи хранится вне Git и резервируется как production secret.

Создать отдельный RSA-ключ:

install -d -m 700 /srv/eusrr/secrets
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 \
  -out /srv/eusrr/secrets/oidc-private.pem
chmod 600 /srv/eusrr/secrets/oidc-private.pem

Передать файл backend-контейнеру read-only и задать окружение:

SITE_URL=https://corp.example.com
PORTAL_FRONTEND_URL=https://corp.example.com
OIDC_PROVIDER_ENABLED=true
OIDC_RSA_PRIVATE_KEY_PATH=/run/secrets/eusrr_oidc_private_key.pem
# При ротации: OIDC_RSA_PRIVATE_KEY_PATHS_INACTIVE=/run/secrets/oidc-private-old.pem
OIDC_ISSUER_URL=https://corp.example.com/o
OIDC_PORTAL_LOGIN_URL=https://corp.example.com/login
OIDC_BROWSER_SESSION_SECONDS=28800
OIDC_ACCESS_TOKEN_SECONDS=3600
OIDC_ID_TOKEN_SECONDS=600
OIDC_REFRESH_TOKEN_SECONDS=43200
SESSION_COOKIE_SECURE=true

OIDC_ISSUER_URL — постоянный публичный идентификатор. Его нельзя менять после подключения клиентов без их перенастройки. Reverse proxy должен сохранять исходный HTTPS scheme и host.

Публичный reverse proxy должен отправлять весь prefix /o/ в Django так же, как /api/. Пример для Nginx:

location ^~ /o/ {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://django_backend;
}

После установки зависимостей выполнить миграции:

python manage.py migrate
python manage.py check --deploy

Регистрация Gitea в портале

Сначала выберите неизменяемое имя authentication source в Gitea, например corp. Оно входит в callback URL:

https://git.example.com/user/oauth2/corp/callback

Создать confidential-клиент и сохранить показанный secret в менеджер секретов:

python manage.py provision_oidc_client \
  --name Gitea \
  --client-id gitea \
  --redirect-uri https://git.example.com/user/oauth2/corp/callback

Повторный запуск обновляет параметры, не меняя secret. Для плановой ротации используйте --rotate-secret и сразу обновите Gitea. Клиент также доступен администраторам в Django Admin в разделе OAuth2 Provider, но management command безопаснее для первичного получения секрета.

Настройка Gitea

В app.ini включить закрытый режим экземпляра. Значение DISABLE_REGISTRATION = false здесь намеренное: Gitea 1.23 применяет ALLOW_ONLY_EXTERNAL_REGISTRATION только при разрешённом механизме регистрации. В результате самостоятельной формы регистрации нет, но LDAP и корпоративный OIDC могут создать связанную учётную запись:

[server]
LANDING_PAGE = login

[service]
DISABLE_REGISTRATION = false
ALLOW_ONLY_INTERNAL_REGISTRATION = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true
SHOW_REGISTRATION_BUTTON = false
FORCE_PRIVATE = true
DEFAULT_PRIVATE = private
DEFAULT_PUSH_CREATE_PRIVATE = true

[openid]
ENABLE_OPENID_SIGNIN = false
ENABLE_OPENID_SIGNUP = false

REQUIRE_SIGNIN_VIEW закрывает анонимный просмотр веб-страниц и неаутентифицированное чтение Git по HTTP даже для репозиториев, которые раньше были помечены публичными. FORCE_PRIVATE и DEFAULT_PRIVATE не меняют старые репозитории, но не позволяют создавать новые публичные. Перед публикацией экземпляра наружу отдельно проверьте список существующих публичных репозиториев и при необходимости переведите их в private.

Локальную форму входа пока оставить включённой: она нужна для существующих локальных и LDAP-учёток, первичной привязки и аварийного администратора.

Для OAuth-клиентов в том же app.ini:

[oauth2_client]
ENABLE_AUTO_REGISTRATION = true
USERNAME = preferred_username
ACCOUNT_LINKING = login
UPDATE_AVATAR = true
OPENID_CONNECT_SCOPES = profile email groups
REGISTER_EMAIL_CONFIRM = false

Затем в Site Administration → Identity & Access → Authentication Sources добавить источник:

Поле Значение
Authentication Type OAuth2
Authentication Name corp
OAuth2 Provider OpenID Connect
Client ID значение из provision_oidc_client
Client Secret одноразово показанный secret
Auto Discovery URL https://corp.example.com/o/.well-known/openid-configuration
Additional Scopes profile email groups
Group Claim Name groups

До переключения пользователей оставить локальный вход Gitea включённым и сохранить отдельную аварийную локальную учётную запись администратора. ACCOUNT_LINKING = login не удаляет и не разлогинивает существующие локальные учётки: при первом конфликте username/email Gitea попросит один раз подтвердить привязку существующим локальным входом.

LDAP-пользователь после успешного входа в портал получает в OIDC claim preferred_username свой sAMAccountName. Успешная LDAP-проверка также считает корпоративный адрес подтверждённым; обычная регистрация по-прежнему требует штатного подтверждения email.

Gitea 1.23.x не отправляет PKCE challenge для confidential OIDC-клиента, поэтому портал временно допускает Authorization Code без PKCE для совместимости. После обновления Gitea и проверки поддержки PKCE следует включить PKCE_REQUIRED в настройках провайдера.

Claims и группы

userinfo и ID token могут содержать:

Scope Claims
openid sub
profile name, given_name, family_name, preferred_username, nickname, picture, locale, zoneinfo, updated_at
email email, email_verified
groups Django groups, portal-users, а также portal-staff/portal-admins для служебных ролей

Не назначайте Gitea administrator по общей группе portal-users. Для автоматического администрирования заведите отдельную узкую Django group и явно сопоставьте её в Gitea.

Проверка

До включения кнопки входа проверить с хоста Gitea:

curl -fsS https://corp.example.com/o/.well-known/openid-configuration
curl -fsS https://corp.example.com/o/.well-known/jwks.json

Discovery должен публиковать один и тот же issuer, HTTPS endpoints и RS256; JWKS содержит только публичную часть ключа. После этого пройти вход тестовой активной учёткой, проверить создание/привязку пользователя, имя, email и avatar, затем проверить отказ для деактивированной тестовой учётки.

Сессия браузера и смена пользователя

Портал хранит отдельную HttpOnly Django-сессию для OIDC. После каждого успешного входа /api/auth/browser-session/ должен перепривязать её к текущему пользователю, включая обычный вход без параметра next. При выходе endpoint /api/auth/browser-session/logout/ очищает эту сессию независимо от состояния JWT: access/refresh token к этому моменту уже может истечь или быть удалён в другой вкладке. Это не позволяет внешнему приложению повторно войти под предыдущей учётной записью.

Сессии Gitea и портала остаются независимыми: выход только из Gitea не завершает сеанс портала. Выход из портала очищает его OIDC-сессию, поэтому следующий вход из Gitea потребует актуальной авторизации в портале.

Отзыв доступа и ротация

  • отключение сотрудника начинает блокировать access/refresh tokens при следующем обращении;
  • OAuth grants и токены можно отозвать через Django Admin;
  • client secret ротируется provision_oidc_client --rotate-secret;
  • при RSA rotation укажите старые ключи через OIDC_RSA_PRIVATE_KEY_PATHS_INACTIVE на переходный период; не удаляйте их до истечения ранее выданных ID token;
  • периодическая очистка выполняется командой python manage.py cleartokens.

Dynamic Client Registration и пользовательское создание OAuth applications публично не маршрутизируются. Новый клиент регистрирует только администратор.