Skip to main content
Omniflow говорит на OpenID Connect в общем виде. Оператор передаёт документ discovery и учётные данные клиента, и нигде в приложении нет ветки под конкретного провайдера: admin_oidc_providers и customer_oidc_providers — это данные, и добавление Google, Keycloak или корпоративного провайдера идентичности означает строку в таблице, а не релиз. Установка без единой строки аутентифицирует паролями (операторы) и через Telegram (клиенты); это положение по умолчанию и полностью поддерживаемое. Реестра два, и они независимы, потому что у двух поверхностей разные модели угроз и разные значения по умолчанию. Аккаунт оператора двигает деньги и читает чужие записи; аккаунт клиента — это чья-то собственная подписка. Эти двое никогда не делят ни строку провайдера, ни cookie сессии, ни cookie потока. Значения по умолчанию для автоматического создания аккаунтов различаются намеренно. Панель оператора, которая выдаёт аккаунт любому, кого принял провайдер, — это путь к повышению привилегий, поэтому по умолчанию неизвестный субъект отклоняется. А клиентская панель, отказывающая всем, кто ещё не пользовался ботом, — это не способ входа, это второй замок.
Обоим реестрам нужен APP_DATA_ENCRYPTION_KEY — ключ AES длиной 32 байта в кодировке base64. Он запечатывает сохранённый клиентский секрет и короткоживущую cookie потока входа. Без валидного ключа сервисы идентичности отказываются стартовать.

OIDC для операторов

Что это и зачем

Необязательный вход через OpenID Connect для /admin по схеме Authorization Code с PKCE (S256). ID-токен проверяется по опубликованному провайдером JWKS, по издателю, по аудитории и по сроку действия, прежде чем хоть что-то из него будет принято на веру. PKCE применяется, хотя это конфиденциальный клиент, потому что он привязывает код авторизации к тому запросу, который начал поток: перехваченный на редиректе код никто другой не сможет обменять. Антиподменный state и верификатор PKCE путешествуют в запечатанной короткоживущей cookie, а не в строке базы данных, поэтому брошенный вход не оставляет ничего для уборки, а провайдер, который так и не сделал обратный редирект, ничего не стоит. Политика сопоставления — самая важная для понимания часть. Непривязанный субъект никогда не занимает существующий аккаунт оператора по совпадению адреса электронной почты. Иначе любой, кто может заставить провайдера подтвердить адрес, захватил бы аккаунт оператора. Оператора идентифицирует только пара (provider, subject).

Настройка провайдера

В этой сборке экрана панели для операторского OIDC нет. Эндпоинты реализованы и закрыты правами доступа; /admin/settings отрисовывает только коммерческие настройки, настройки установки и провайдеров входа для клиентов. Настройте провайдера аутентифицированным вызовом к /v1/panel — вам нужна cookie сессии и соответствующий X-CSRF-Token, оба из которых можно прочитать из GET /v1/panel/auth/session.
Обычный HTTP отклоняется и для издателя, и для URL discovery. Автоматическое создание аккаунта с ролью owner отклоняется наотрез, потому что позволило бы любому, кого принял провайдер, получить полный контроль над установкой. Изменять провайдера операторов может только владелец: PUT и DELETE требуют settings.write и admins.roles, а последнее есть только у роли owner. Провайдер решает, кто становится каким оператором — а при автоматическом создании и с какой ролью, — то есть это та же власть, что и выдача ролей. Администратор, который мог бы его править, направил бы его на издателя, которым управляет сам. DELETE /v1/panel/settings/oidc/{slug} удаляет провайдера и каскадно удаляет привязанные к нему идентичности. И сохранение, и удаление пишут строки аудита (admin.oidc.provider_saved, admin.oidc.provider_deleted, категория configuration).
issuer и discoveryUrl провайдера, через которого привязаны операторы, нельзя изменить: сохранение отвечает 409 oidc_provider_bound. Чтобы перейти на другого провайдера идентичности, удалите провайдера (это удалит его привязки) и создайте новый; затем каждый оператор привязывается заново. См. Привязка к издателю.

Redirect URI

Зарегистрируйте у провайдера этот адрес, заменив <slug> на выбранное вами значение:
Omniflow пересобирает этот URL из заголовка Host входящего запроса в одном месте, потому что он должен совпадать байт в байт между запросом авторизации и обменом токена. Схема — https, если только APP_ADMIN_COOKIE_SECURE не равен false и запрос не пришёл по TLS, — случай локальной разработки. Поэтому развёртывание за обратным прокси обязано пробрасывать исходный Host; см. Обратный прокси.

Вход

1

Получите список включённых провайдеров

GET /v1/panel/auth/oidc отвечает {"items":[{"slug","displayName"}]} и не требует учётных данных — он выполняется до того, как кто-либо вошёл.
2

Запустите поток

Переведите браузер на GET /v1/panel/auth/oidc/{slug}/start?next=/admin/finance. API запечатывает {slug, state, verifier, next} в cookie __Host-omniflow_oidc — на стеке без TLS это omniflow_oidc, поскольку cookie с префиксом __Host- без Secure браузер отбрасывает — (время жизни 10 минут) и отвечает 302 на провайдера. next принимается только как путь внутри /admin: он должен начинаться с /, но не с //, не может содержать обратную косую черту и управляющие символы (браузеры читают \ как / и вырезают табуляцию и переводы строк, поэтому /\evil.example иначе увёл бы с сайта), должен разбираться без схемы и хоста, а его нормализованный путь должен быть /admin или вложенным в него. Всё прочее отбрасывается, и обратный вызов ведёт на /admin.
3

Вернитесь через обратный вызов

Провайдер перенаправляет на /v1/panel/auth/oidc/{slug}/callback. Cookie потока очищается до обмена токена, поэтому повторно воспроизведённый обратный вызов нельзя обработать дважды. state сравнивается за постоянное время. При успехе устанавливается cookie сессии, и браузер попадает на next, по умолчанию /admin.
/admin/login в этой сборке не отрисовывает кнопки OIDC; он никогда не вызывает GET /v1/panel/auth/oidc. Добраться до провайдера можно, перейдя по URL /start напрямую — из закладки или по ссылке с вашей собственной внутренней страницы. Вход через OIDC выпускает полноценную сессию: pending_totp равен false, и сессия записывает auth_methods = {oidc}. Провайдеру доверяют применение собственного второго фактора, поэтому Omniflow не спрашивает код TOTP сверх этого.

Что происходит при отказе

Любая неудача приводит на /admin/login?error=<reason>, чтобы панель могла объяснить исход, а API при этом ничего не выдавало в редирект провайдера. При включённом автоматическом создании субъект, чей адрес уже принадлежит оператору, всё равно получает отказ — адрес никогда не используется, чтобы занять аккаунт. У созданного таким образом аккаунта нет пароля вообще (password_hash равен null), и он аутентифицируется только через провайдера; создание пишет строку аудита admin.oidc.auto_provisioned и отправляет уведомление в тему 🔐 Admin security в операторской группе Telegram.

Ограничения операторского потока в этой сборке

Это свойства текущего кода, а не настройки, которые вы можете изменить:
  • nonce не выпускается и не проверяется. PKCE привязывает код к потоку; клиентский поток дополнительно привязывает ID-токен к потоку через nonce, а операторский — нет.
  • Существующий оператор не может привязать провайдера к своему аккаунту. Процедура привязки в сервисе есть, но её не вызывает ни один маршрут, поэтому единственный путь к привязанной идентичности — автоматическое создание, а оно создаёт новый аккаунт.
  • Отключение или удаление провайдера не отзывает установленные им сессии. Эти сессии живут до своих собственных дедлайнов — вплоть до 12-часового абсолютного срока жизни. Чтобы завершить их прямо сейчас, приостановите соответствующие аккаунты операторов, что немедленно отзывает их живые сессии.
  • Discovery запрашивается на каждый поток и никогда не кешируется, поэтому сменившийся JWKS или переехавший эндпоинт не требуют перезапуска.

OIDC для клиентов

Что это и зачем

Тот же общий поток для /account с одним отличием, которое лежит в данных, а не в коде: одновременно может быть включено несколько провайдеров, и экран входа отрисовывает их все. Клиентский поток в дополнение к PKCE выпускает и проверяет nonce — PKCE привязывает код авторизации к этому потоку, а nonce привязывает к нему полученный ID-токен, чего PKCE сам по себе не делает. Без него токен, полученный где-то ещё для этого же клиента, можно было бы воспроизвести в этот обратный вызов.

Настройка провайдера

Откройте /admin/settings → Провайдеры входа для клиентов (чтобы увидеть раздел, нужно право settings.read, чтобы изменять — settings.write).
1

Начните с пресета или с пустой формы

В панели поставляются три пресета. Пресет — это данные, которые предзаполняют форму; ниже по потоку никто и никогда не спрашивает, из какого пресета появился провайдер.У Yandex по умолчанию выключено, потому что он не всегда возвращает email_verified; требование этого claim отклоняло бы каждый вход. Discord выпускает ID-токен только тогда, когда запрошен openid, а адрес несёт scope email.
2

Заполните учётные данные

slug, displayName, issuer, discoveryUrl, clientId, clientSecret, scopes, enabled, icon, sortOrder, requireVerifiedEmail, allowAutoProvision. icon — это имя иконки, которая поставляется вместе с панелью, проверяемое по ^[a-z0-9][a-z0-9-]{0,38}$, но никогда не URL: включение провайдера не должно заставлять страницу входа обращаться к стороннему хосту.
3

Зарегистрируйте redirect URI у провайдера

Для Google добавьте его в разрешённые redirect URI клиента OAuth до того, как включите провайдера.
4

Сохраните

Форма отправляет PUT /v1/panel/settings/customer-oidc. Поле клиентского секрета при загрузке формы всегда пустое, а пустое поле означает оставить то, что хранится, поэтому оператор может пересохранить провайдера, не набирая секрет заново.
Отключение провайдера также завершает все установленные им клиентские сессии с revoked_reason = 'provider_disabled'. У оператора, который только что выключил провайдера во время инцидента, его клиенты не должны оставаться залогиненными вплоть до 60-дневного абсолютного срока жизни сессии. Удаление провайдера делает то же самое.
Удаление провайдера требует ввести его slug для подтверждения, и диалог проговаривает оба последствия до того, как вы это сделаете: провайдер перестаёт предлагаться, а установленные им сессии завершаются. Удаление провайдера намеренно не трогает строки в identities. Они — свидетельство того, что клиент когда-то входил именно так, и их удаление незаметно отняло бы способ входа у клиентов, у которых может не быть другого. Пока у какого-либо клиента есть активная идентичность через провайдера, его issuer и discoveryUrl нельзя изменить — сохранение отклоняется с oidc_rejected и сообщением об этом. Чтобы перевести клиентов на другого провайдера идентичности, удалите этого и создайте новый; старые привязки остаются закреплёнными за старым издателем и никогда не совпадут с новым (см. Привязка к издателю), поэтому затронутые клиенты входят другим способом и привязывают нового провайдера в разделе Способы входа.

Как входит клиент

/account/sign-in вызывает GET /v1/account/auth/methods и отрисовывает только то, что установка действительно предлагает: виджет Telegram, доставляемую ботом ссылку, если она включена, и по одной кнопке на каждый включённый провайдер OIDC. Каждая кнопка ведёт на GET /v1/account/auth/oidc/{slug}/start; провайдер возвращает на /v1/account/auth/oidc/{slug}/callback, который устанавливает cookie сессии и перенаправляет на next, по умолчанию /account. Для next действует то же правило, что и в операторском потоке, но в пределах /account: значение вне него или с обратной косой чертой либо управляющим символом отбрасывается. Неудачи перенаправляют на /account/sign-in?error=<reason> с причинами sign_in_expired, sign_in_denied, unverified_email, account_unavailable и sign_in_failed. Непривязанный субъект у провайдера с выключенным автоматическим созданием аккаунтов получает sign_in_failed — намеренно ту же формулировку, что и при любом другом отказе, чтобы экран нельзя было использовать для выяснения, зарегистрирована ли идентичность.

Привязка провайдера к существующему аккаунту

Вошедший клиент открывает /account/security → Способы входа и запускает поток из GET /v1/account/sign-in-methods/oidc/{slug}/start. Аккаунт, к которому идёт привязка, запечатан в cookie потока, а не читается из сессии на обратном вызове, поэтому поток привязки никогда не может завершиться на другом аккаунте, чем тот, что его начал. Обратный вызов возвращает на /account/security?linked=<slug> либо на ?error=identity_taken|unverified_email|link_failed. Субъект, который уже принадлежит кому-то другому, — это identity_taken, и его никогда не объединяют. Два клиента — это два клиента, и тихое их слияние перенесло бы заказы, кошелёк и подписку одного человека в аккаунт другого без всякого пути назад. Привязка субъекта, уже привязанного к этому же клиенту, — операция без эффекта, поэтому двойная отправка безвредна.

Защита от удаления последнего способа входа

DELETE /v1/account/sign-in-methods/{identityID} отказывает с 409 last_sign_in_method, когда удаление убрало бы единственный оставшийся у клиента способ входа. Клиент, удаливший последний способ, закрыл себе доступ к аккаунту с оплаченной подпиской, и ничто в панели оператора не может вернуть его обратно. Правило касается количества оставшихся способов, а не того, какой именно: клиент с Telegram и Google может убрать любой из двух, а клиент с одним способом — ни одного. Количество считается внутри той же транзакции, которая выполняет удаление, поэтому две вкладки, отвязывающие два разных способа одновременно, не могут обе увидеть «остаётся два» и обе преуспеть. Считаются только идентичности telegram и oidc:* со статусом active. Удаление — это отзыв, а не удаление строки: строка остаётся со status='revoked'. Оно также требует недавнего входа — иначе 401 reauthentication_required — и пишет строку identity_unlinked в собственный журнал безопасности клиента, где указан slug провайдера и больше ничего. Панель делает кнопку Удалить неактивной и объясняет причину до попытки, а не после неё.

Сопоставление claim-ов

Обе поверхности сопоставляют claim-ы одинаково, и сопоставление намеренно узкое. Email никогда не используется для поиска аккаунта. Только пара (provider, subject). Из этого прямо следуют два вывода, и оба намеренны: непривязанный субъект не может захватить аккаунт, заявив его адрес, а смена электронной почты у провайдера не является сменой идентичности и не требует вообще никакого пути восстановления.

Привязка к издателю

sub уникален только в пределах одного издателя. Поэтому каждая привязка OIDC записывает издателя, который её подтвердил, — admin_oidc_identities.issuer для операторов, identities.oidc_issuer для клиентов, — и вход совпадает с привязкой, только когда проверенный iss ID-токена равен записанному значению. Текущий issuer строки провайдера роли не играет: иначе правка провайдера с переводом на издателя, которым управляет кто-то другой, позволила бы этому издателю выпустить субъект любого привязанного оператора или клиента и войти от его имени без второго фактора. Субъект, привязанный у другого издателя, отклоняется сразу — для оператора как oidc_no_account, для клиента как sign_in_failed, а для клиента, который привязывает провайдера в Способах входа, как identity_taken. Автоматически такой субъект никогда не создаётся, потому что пара (provider, subject) уже занята. Привязки, созданные до появления закрепления, при обновлении получили издателя своего провайдера; у клиентской привязки, чья строка провайдера к тому моменту уже была удалена, записанного издателя нет, и она не совпадает ни с чем. Субъекты клиентского OIDC попадают в единую таблицу identities под provider = "oidc:<slug>". Два провайдера могут законно выдать одинаковый sub, а ограничение UNIQUE (provider, subject) обязано и дальше означать «один внешний аккаунт — один клиент». Установленные так сессии записывают auth_method = 'oidc' и auth_provider = <slug>; таблица следит, чтобы эти два значения согласовывались. Ничего больше из токена для авторизации не сохраняется. Сырая полезная нагрузка claim-ов, которую вернул провайдер идентичности, входит в перечень объявленных изъятий при выгрузке персональных данных клиента — эта установка не вправе её распространять. См. Приватность клиента.

Как запечатывается клиентский секрет

Клиентский секрет — это учётные данные, которые тратят от имени оператора в чужом сервисе, поэтому он хранится так же, как любые другие учётные данные в Omniflow.
  • Он запечатывается AES-GCM под APP_DATA_ENCRYPTION_KEY до того, как попадёт в базу данных, поэтому один только дамп базы не даёт пригодного секрета.
  • У каждого поля своя метка связанных данных — admin.oidc.client_secret для операторского реестра, customer.oidc.client_secret для клиентского, admin.flow и customer.flow для cookie потоков входа. Шифротекст, перенесённый между колонками, не открывается, а не расшифровывается там, где ему не место.
  • Он никогда не возвращается. Оба эндпоинта списка сообщают только hasClientSecret и ничего больше, а это единственное безопасное представление секрета.
  • При сохранении пустой clientSecret оставляет сохранённый нетронутым. Именно это делает форму пересохраняемой: оператору, меняющему sortOrder, не нужно снова доставать секрет, а экрану никогда не приходится его показывать.
  • Это верно, только пока провайдер указывает туда, для чего вводили секрет. Сохранение, меняющее issuer, discoveryUrl или clientId без нового clientSecret, отклоняется (422 oidc_rejected с сообщением, что нужен секрет), а слой хранения сам удаляет сохранённый секрет у перенаправленного провайдера. Иначе тот, кто правит адрес, получил бы секрет на своём эндпоинте токенов.
Cookie потока запечатана тем же ключом и не читаема и не подделываема браузером, который её держит. Она несёт state, верификатор PKCE, nonce клиентского потока, путь next и — для потока привязки — аккаунт, к которому идёт привязка.
Ротация APP_DATA_ENCRYPTION_KEY делает нечитаемым каждое запечатанное значение: клиентские секреты OIDC, секреты TOTP операторов и контактные данные клиентов. Считайте ключ постоянным на всё время жизни установки и храните его резервную копию отдельно от базы данных.

Смежные страницы

Панель администратора

Сессии операторов, роли, второй фактор и журнал аудита.

Безопасность

Вся модель целиком: учётные данные, сессии, запечатывание секретов и заголовки.

Веб-панель клиента

Где клиент управляет способами входа, сессиями и собственной историей.

Конфигурация

APP_DATA_ENCRYPTION_KEY и остальной справочник по окружению.