Skip to main content
Одно действие пользователя даёт два результата: он входит, а ваш продукт получает живой канал — MAX, Telegram, WhatsApp, другой мессенджер или почту, смотря где подтвердили. Дальше по этому каналу можно слать уведомления, подтверждения и сервисные сообщения, не выпрашивая отдельно контакт.

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

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

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

После обмена кода на токены — два дополнительных claim: Без scope channel типизированных claims не будет. Они не попадут ни в access_token, ни в id_token, а значит не появятся и в ответе /connect/userinfo — он зеркалит access token.
Гейт закрывает отдельные claims, но не значение. При резолвере идентичности по умолчанию sub формируется как {channel_type}:{channel_user_id}, то есть приложение, разрезав строку по двоеточию, получит те же две части, не запросив scope. Рассчитывайте на гейт как на способ не работать с идентификаторами канала там, где они не нужны, а не как на средство скрыть их.
Гейт действует во всех режимах поставки, включая встроенный. Даже когда Veriqa подключена библиотекой в ваш же процесс, приложение получает идентичность штатным обменом authorize → code → token, а не готовым принципалом. Отдельного «внутреннего» пути, отдающего claims канала мимо scope, не существует.

Как связать

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. Обменяйте транзакцию на токен:
    Ответ — обычный ответ token endpoint: access_token, token_type, expires_in и id_token, без refresh_token. scope — по RFC 6749 §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.
Что узнаёт приложение и кто предупреждает пользователя. При включённом grant ваше приложение получает канальную идентичность подтвердившего в каждом подтверждении этого клиента, а не только там, где она нужна для дела. Отдельного вопроса «передать мои данные?» Veriqa пользователю не задаёт: предупреждение о передаче данных и контекст запроса живут в текстовке action_type интегратора и в слотах, которые заполняет ваш бэкенд. Veriqa не проверяет, что такое предупреждение есть во всех типах действий такого клиента, — это ответственность интегратора. Вход — на предъявителя: кто первым его откроет, тот и подтвердит, и sub будет его, — поэтому контекст запроса и должен стоять в текстовке.
Контекст запроса собирается из слотов типа string в контракте сообщения типа действия; каждый такой слот обязан объявить max_length, потолок — 128 символов. Текстовку пишет интегратор, а слоты (slot_values) при создании заполняет ваш бэкенд тем, что он видит об устройстве, — тот, кто перешлёт QR, подделать эти значения не может: вашим бэкендом он не владеет. Например, шаблон:
Здесь service, device и location — слоты, которые заполняет ваш бэкенд. Слот {app} подтверждения сервер заполняет сам — идентификатором приложения транзакции (client_id), а не DisplayName клиента, — поэтому человекочитаемое имя приложения попадает в текстовку только через ваш собственный слот. Пример синтетический: таких имён слотов и такой текстовки в поставляемой конфигурации нет. Как объявить контракт и шаблон и как выглядят запрос и ответ целиком — в API серверного подтверждения.

Один вход, два канала

Для чувствительных операций подтверждение можно потребовать в двух разных доверенных каналах.