> ## Documentation Index
> Fetch the complete documentation index at: https://veriqa.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Вход и привязка канала одним действием

> Одно действие — два результата: пользователь вошёл, а продукт получил живой канал для уведомлений. Что для этого передать и что сохранить у себя.

Одно действие пользователя даёт **два результата**: он входит, а ваш продукт получает **живой
канал** — MAX, Telegram, WhatsApp, [другой мессенджер](/docs/ru/guides/supported-channels) или почту,
смотря где подтвердили. Дальше по этому каналу можно слать уведомления, подтверждения и
сервисные сообщения, не выпрашивая отдельно контакт.

## Что отправляете

Обычный authorize-запрос, но со scope `channel`:

```http theme={null}
GET /connect/authorize
  ?client_id=my-app
  &response_type=code
  &scope=openid%20profile%20channel
  &redirect_uri=https://app.example.com/callback
  &state=…
  &code_challenge=…
  &code_challenge_method=S256
```

## Что получаете

После обмена кода на токены — два дополнительных claim:

| Claim | Значение |
| - | - |
| `channel_type` | Тип канала: `telegram`, `whatsapp`, `max`, `email` |
| `channel_user_id` | Идентификатор пользователя внутри этого канала |

**Без scope `channel` типизированных claims не будет.** Они не попадут ни в `access_token`, ни в
`id_token`, а значит не появятся и в ответе `/connect/userinfo` — он зеркалит access token.

<Warning>
  Гейт закрывает отдельные claims, но **не значение**. При резолвере идентичности по умолчанию
  `sub` формируется как `{channel_type}:{channel_user_id}`, то есть приложение, разрезав строку по
  двоеточию, получит те же две части, не запросив scope. Рассчитывайте на гейт как на способ **не
  работать** с идентификаторами канала там, где они не нужны, а не как на средство скрыть их.
</Warning>

<Note>
  Гейт действует **во всех режимах поставки, включая встроенный**. Даже когда Veriqa подключена
  библиотекой в ваш же процесс, приложение получает идентичность штатным обменом
  authorize → code → token, а не готовым принципалом. Отдельного «внутреннего» пути, отдающего
  claims канала мимо scope, не существует.
</Note>

## Как связать

`sub` — стабильный идентификатор субъекта; пара `channel_type` + `channel_user_id` — адрес
пользователя в конкретном мессенджере. Привязка хранится **на вашей стороне**: Veriqa не ведёт
учётных записей вашего приложения и не сопоставляет их с каналами.

Практическое следствие: `sub` для вас — ключ учётной записи. Так же поступают и готовые CMS —
Drupal и TYPO3 хранят его как имя внешней идентичности.

## Если аккаунт уже существует

Сценарий работает и как «дозаведение канала» к существующей учётной записи: пользователь,
вошедший обычным способом, проходит authorize со scope `channel`, а вы сохраняете полученную пару
рядом со своим аккаунтом. Отдельного API для этого нет — тот же самый запрос.

## Привязка с сервера без входа

Бывает, что привязку не к чему прицепить — редиректа браузера нет: ваш сервер сам хочет получить
канал для учётной записи — например, вошедший пользователь просит бота поддержки подключить
мессенджер. Это делается через server-to-server вход подтверждения и следующий за ним обмен на токен.

**Что включает интегратор** у клиента: `AllowClientCredentials`, `AllowConfirmationTokenGrant` и
`openid` в `AllowedScopes` (и `channel`, если нужны типизированные claims). Разрешение — интегратора:
ни один параметр вашего запроса раскрытие не расширяет, а claims не выходят за `AllowedScopes`.

1. **Создайте транзакцию подтверждения** — `POST /api/transaction/confirmation`, **без**
   `expected_identities`: вы ещё не знаете, какой аккаунт канала подтвердит, — именно это вы и хотите
   узнать. Покажите пользователю `channel_entry` из ответа.
2. **Дождитесь исхода** — `GET /api/transaction/{id}/result` до `confirmed`. Без ожидаемых
   идентичностей `matched_type` равен `null` — это не отказ, токен выдаётся всё равно.
3. **Обменяйте транзакцию на токен:**

   ```http theme={null}
   POST /connect/token
   Content-Type: application/x-www-form-urlencoded
   Authorization: Basic …

   grant_type=urn:veriqa:params:oauth:grant-type:confirmation
   &transaction_id=…
   &scope=openid%20channel
   ```

   Ответ — обычный ответ token endpoint: `access_token`, `token_type`, `expires_in` и `id_token`,
   без `refresh_token`. `scope` — по
   [RFC 6749 §5.1](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1): он возвращается, только
   если выданный набор scopes отличается от запрошенного. Обмен выдаёт ровно запрошенный набор, поэтому
   члена в ответе нет — не требуйте его.
4. **Сохраните `sub`** из `id_token` рядом со своей учётной записью.

Правила обмена:

* `scope` обязан содержать `openid`; `channel` добавляет `channel_type` и `channel_user_id`; всё
  остальное — в пределах `AllowedScopes`. `offline_access` отвергается с `invalid_scope`, как и
  `scope` без `openid`. Нет `transaction_id` — `invalid_request`.
* **Транзакция погашается однократно.** Повторный обмен получает `invalid_grant` — ту же ошибку с
  тем же описанием, что и неизвестный идентификатор, транзакция другого клиента, ещё не завершённая
  транзакция или уже удалённая по истечении `CompletedRetentionSeconds`. Ответы намеренно не различают
  эти причины, поэтому обменивайте сразу после `confirmed`. Из двух одновременных обменов токен
  получает ровно один.
* Однократность относится к погашению, а не к чтению claims: `access_token` того же ответа несёт те же
  claims и до истечения отдаёт их через `/connect/userinfo`.
* **Ключ привязки — `sub` целиком.** При резолвере идентичности по умолчанию он формируется как
  `{channel_type}:{channel_user_id}`, но хост со своим резолвером вправе составить его иначе — не
  разбирайте строку; если нужна пара, запрашивайте scope `channel`.

<Warning>
  **Что узнаёт приложение и кто предупреждает пользователя.** При включённом grant ваше приложение
  получает канальную идентичность подтвердившего в **каждом** подтверждении этого клиента, а не только
  там, где она нужна для дела. Отдельного вопроса «передать мои данные?» Veriqa пользователю не
  задаёт: предупреждение о передаче данных и контекст запроса живут в текстовке `action_type`
  интегратора и в слотах, которые заполняет ваш бэкенд. Veriqa не проверяет, что такое предупреждение
  есть во всех типах действий такого клиента, — это ответственность интегратора. Вход — на
  предъявителя: кто первым его откроет, тот и подтвердит, и `sub` будет его, — поэтому контекст
  запроса и должен стоять в текстовке.
</Warning>

Контекст запроса собирается из слотов типа `string` в контракте сообщения типа действия; каждый такой
слот обязан объявить `max_length`, потолок — 128 символов. Текстовку пишет интегратор, а слоты
(`slot_values`) при создании заполняет ваш бэкенд тем, что он видит об устройстве, — тот, кто
перешлёт QR, подделать эти значения не может: вашим бэкендом он не владеет. Например, шаблон:

```text theme={null}
Привязать {service} к этому аккаунту мессенджера? {service} получит идентификатор вашего аккаунта. Запрос с {device}, {location}.
```

Здесь `service`, `device` и `location` — слоты, которые заполняет ваш бэкенд. Слот `{app}`
подтверждения сервер заполняет сам — идентификатором приложения транзакции (`client_id`), а не
`DisplayName` клиента, — поэтому
человекочитаемое имя приложения попадает в текстовку только через ваш собственный слот. *Пример синтетический: таких имён слотов и такой текстовки в
поставляемой конфигурации нет.* Как объявить контракт и шаблон и как выглядят запрос и ответ целиком — в
[API серверного подтверждения](/docs/ru/reference/confirmation-api).

<Card title="Один вход, два канала" icon="shield-halved" href="/docs/ru/scenarios/step-up">
  Для чувствительных операций подтверждение можно потребовать в двух разных доверенных каналах.
</Card>


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