> ## Documentation Index
> Fetch the complete documentation index at: https://omniflow-vpn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Вход через OIDC

> Подключение провайдера OpenID Connect для операторов или для клиентов: поля, redirect URI, сопоставление claim-ов и защиты, которые не дают перехватить аккаунт

Omniflow говорит на OpenID Connect в общем виде. Оператор передаёт документ
discovery и учётные данные клиента, и нигде в приложении нет ветки под
конкретного провайдера: `admin_oidc_providers` и `customer_oidc_providers` — это
данные, и добавление Google, Keycloak или корпоративного провайдера идентичности
означает строку в таблице, а не релиз. Установка без единой строки
аутентифицирует паролями (операторы) и через Telegram (клиенты); это положение по
умолчанию и полностью поддерживаемое.

Реестра **два, и они независимы**, потому что у двух поверхностей разные модели
угроз и разные значения по умолчанию. Аккаунт оператора двигает деньги и читает
чужие записи; аккаунт клиента — это чья-то собственная подписка. Эти двое никогда
не делят ни строку провайдера, ни cookie сессии, ни cookie потока.

| | Оператор | Клиент |
| - | - | - |
| Таблица | `admin_oidc_providers` | `customer_oidc_providers` |
| Настраивается через | `PUT /v1/panel/settings/oidc` (в этой сборке только API) | `/admin/settings` → **Провайдеры входа для клиентов** |
| Право доступа | `settings.read` для чтения; `settings.write` **и** `admins.roles` (только владелец) для изменения | `settings.read` / `settings.write` |
| Redirect URI | `https://<host>/v1/panel/auth/oidc/<slug>/callback` | `https://<host>/v1/account/auth/oidc/<slug>/callback` |
| Сколько провайдеров можно включить одновременно | Сколько угодно; на экране входа для них нет кнопок | Сколько угодно, отрисовываются кнопками в порядке `sortOrder` |
| `requireVerifiedEmail` по умолчанию | `true` | `true` |
| `allowAutoProvision` по умолчанию | `false` | `true` |
| `nonce` в потоке | не выпускается | выпускается и проверяется |
| Отключение отзывает существующие сессии | нет | да |

Значения по умолчанию для автоматического создания аккаунтов различаются
намеренно. Панель оператора, которая выдаёт аккаунт любому, кого принял
провайдер, — это путь к повышению привилегий, поэтому по умолчанию неизвестный
субъект отклоняется. А клиентская панель, отказывающая всем, кто ещё не
пользовался ботом, — это не способ входа, это второй замок.

> [!NOTE]
>
> Обоим реестрам нужен `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`.

```bash theme={null}
curl -X PUT https://panel.example.com/v1/panel/settings/oidc \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: $CSRF" \
  -b "__Host-omniflow_admin=$SESSION" \
  -d '{
    "slug": "corp",
    "displayName": "Corporate SSO",
    "issuer": "https://sso.example.com",
    "discoveryUrl": "https://sso.example.com/.well-known/openid-configuration",
    "clientId": "omniflow-panel",
    "clientSecret": "…",
    "scopes": ["openid", "email", "profile"],
    "enabled": true,
    "requireVerifiedEmail": true,
    "allowAutoProvision": false
  }'
```

| Поле | Правила | Назначение |
| - | - | - |
| `slug` | `^[a-z0-9][a-z0-9-]{0,38}[a-z0-9]$`, уникальный | Появляется в redirect URI и в стартовом URL |
| `displayName` | 1–80 символов | Показывается везде, где называется провайдер |
| `issuer` | должен начинаться с `https://` | Сверяется с `iss` из ID-токена |
| `discoveryUrl` | должен начинаться с `https://` | Запрашивается на каждый поток за эндпоинтами и JWKS |
| `clientId` | 1–256 символов | Он же ожидаемая аудитория ID-токена |
| `clientSecret` | только на запись | Запечатывается при сохранении; пустое значение сохраняет прежнее |
| `scopes` | по умолчанию `openid email profile` | Отправляется в запросе авторизации |
| `enabled` | по умолчанию `false` | В списке и для запуска доступны только включённые провайдеры |
| `requireVerifiedEmail` | по умолчанию `true` | Отклоняет субъекта, у которого `email_verified` равно false |
| `allowAutoProvision` | по умолчанию `false` | Может ли неизвестный субъект создать аккаунт |
| `autoProvisionRole` | одно из `administrator`, `support`, `finance`, `marketing`, `auditor` | Обязательно, когда включено автоматическое создание |

Обычный 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`).

> [!WARNING]
>
> `issuer` и `discoveryUrl` провайдера, через которого привязаны операторы,
> **нельзя изменить**: сохранение отвечает `409 oidc_provider_bound`. Чтобы
> перейти на другого провайдера идентичности, удалите провайдера (это удалит
> его привязки) и создайте новый; затем каждый оператор привязывается заново.
> См. [Привязка к издателю](#привязка-к-издателю).

### Redirect URI

Зарегистрируйте у провайдера этот адрес, заменив `<slug>` на выбранное вами
значение:

```
https://<your-host>/v1/panel/auth/oidc/<slug>/callback
```

Omniflow пересобирает этот URL из заголовка `Host` входящего запроса в одном
месте, потому что он должен совпадать байт в байт между запросом авторизации и
обменом токена. Схема — `https`, если только `APP_ADMIN_COOKIE_SECURE` не равен
`false` **и** запрос не пришёл по TLS, — случай локальной разработки. Поэтому
развёртывание за обратным прокси обязано пробрасывать исходный `Host`; см.
[Обратный прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md).

### Вход

**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 при этом ничего не выдавало в редирект провайдера.

| `error` | Причина |
| - | - |
| `oidc_expired` | Cookie потока нет, она нечитаема, или slug не совпадает с тем, что в cookie |
| `oidc_denied` | Провайдер вернул параметр `error` |
| `oidc_state_mismatch` | Возвращённый `state` не тот, который выпустил этот поток |
| `oidc_unverified_email` | `requireVerifiedEmail` включён, а провайдер сообщил, что адрес не подтверждён |
| `oidc_no_account` | Субъект не привязан, а автоматическое создание аккаунтов выключено |
| `oidc_account_inactive` | Найденный аккаунт оператора не в статусе `active` |
| `oidc_failed` | Не удались discovery, обмен токена или проверка ID-токена |

При включённом автоматическом создании субъект, чей адрес уже принадлежит
оператору, всё равно получает отказ — адрес никогда не используется, чтобы занять
аккаунт. У созданного таким образом аккаунта **нет пароля вообще**
(`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. Начните с пресета или с пустой формы**

В панели поставляются три пресета. Пресет — это данные, которые предзаполняют
форму; ниже по потоку никто и никогда не спрашивает, из какого пресета
появился провайдер.

| Slug | Издатель | Scopes | Проверенный email по умолчанию |
| - | - | - | - |
| `google` | `https://accounts.google.com` | `openid email profile` | включено |
| `yandex` | `https://oauth.yandex.ru` | `openid login:email login:info` | выключено |
| `discord` | `https://discord.com` | `openid email identify` | включено |

У 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 у провайдера**

```
https://<your-host>/v1/account/auth/oidc/<slug>/callback
```

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

**4. Сохраните**

Форма отправляет `PUT /v1/panel/settings/customer-oidc`. Поле клиентского
секрета при загрузке формы всегда пустое, а пустое поле означает *оставить
то, что хранится*, поэтому оператор может пересохранить провайдера, не
набирая секрет заново.

> [!WARNING]
>
> Отключение провайдера **также завершает все установленные им клиентские
> сессии** с `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-ы одинаково, и сопоставление намеренно узкое.

| Claim | Для чего используется |
| - | - |
| `sub` | Идентичность. Обрезается по краям; пустой субъект отклоняется |
| `email` | Приводится к нижнему регистру, сохраняется в профиле и показывается обратно тому, кто только что доказал владение аккаунтом |
| `email_verified` | Проверяется, когда установлен `requireVerifiedEmail` |
| `name` | Отображаемое имя для автоматически созданного аккаунта оператора |

**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-ов, которую вернул провайдер идентичности, входит в перечень объявленных
изъятий при выгрузке персональных данных клиента — эта установка не вправе её
распространять. См. [Приватность клиента](https://omniflow-vpn.mintlify.site/ru/customer/privacy.md).

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

Клиентский секрет — это учётные данные, которые тратят от имени оператора в
чужом сервисе, поэтому он хранится так же, как любые другие учётные данные в
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` и — для потока привязки — аккаунт, к которому идёт привязка.

> [!WARNING]
>
> Ротация `APP_DATA_ENCRYPTION_KEY` делает нечитаемым каждое запечатанное
> значение: клиентские секреты OIDC, секреты TOTP операторов и контактные данные
> клиентов. Считайте ключ постоянным на всё время жизни установки и храните его
> резервную копию отдельно от базы данных.

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

> **[Панель администратора](https://omniflow-vpn.mintlify.site/ru/operations/admin-panel.md)**
>
> Сессии операторов, роли, второй фактор и журнал аудита.

> **[Безопасность](https://omniflow-vpn.mintlify.site/ru/operations/security.md)**
>
> Вся модель целиком: учётные данные, сессии, запечатывание секретов и заголовки.

> **[Веб-панель клиента](https://omniflow-vpn.mintlify.site/ru/customer/web-panel.md)**
>
> Где клиент управляет способами входа, сессиями и собственной историей.

> **[Конфигурация](https://omniflow-vpn.mintlify.site/ru/getting-started/configuration.md)**
>
> `APP_DATA_ENCRYPTION_KEY` и остальной справочник по окружению.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.