All checks were successful
Deploy production / deploy (push) Successful in 10m4s
236 lines
15 KiB
Markdown
236 lines
15 KiB
Markdown
# Корпоративный 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-ключ:
|
||
|
||
```bash
|
||
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 и задать окружение:
|
||
|
||
```dotenv
|
||
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:
|
||
|
||
```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;
|
||
}
|
||
```
|
||
|
||
После установки зависимостей выполнить миграции:
|
||
|
||
```bash
|
||
python manage.py migrate
|
||
python manage.py check --deploy
|
||
```
|
||
|
||
## Регистрация Gitea в портале
|
||
|
||
Сначала выберите неизменяемое имя authentication source в Gitea, например `corp`. Оно входит в callback URL:
|
||
|
||
```text
|
||
https://git.example.com/user/oauth2/corp/callback
|
||
```
|
||
|
||
Создать confidential-клиент и сохранить показанный secret в менеджер секретов:
|
||
|
||
```bash
|
||
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 могут создать связанную учётную запись:
|
||
|
||
```ini
|
||
[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`:
|
||
|
||
```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:
|
||
|
||
```bash
|
||
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 публично не маршрутизируются. Новый клиент регистрирует только администратор.
|