15 KiB
Корпоративный 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; fallbackemployee-<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 публично не маршрутизируются. Новый клиент регистрирует только администратор.