> ## 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.

# Быстрый старт .NET

> Подключение Veriqa к ASP.NET Core как встроенного OpenIddict-коннектора.

Veriqa встраивается в ваш ASP.NET Core-хост и работает как auth-сервер OpenIddict: поднимает
OIDC-эндпоинты, страницу входа с QR и каналами, движок транзакций и адаптеры мессенджеров.
Отдельный процесс или Docker не нужны.

<Note>
  Хотите вовсе не тянуть Veriqa в свою сборку? Тот же сервер работает и **отдельно**, а приложение
  общается с ним по обычному OIDC — в облаке, в Docker или службой ОС; ни одного пакета Veriqa в
  дереве зависимостей и в SBOM. Что это меняет для .NET-клиента —
  [если Veriqa работает отдельно](#если-veriqa-работает-отдельно).
</Note>

<Note>
  В примерах — плейсхолдеры (`my-app`, токены ботов). Подставьте свои значения и никогда не
  коммитьте реальные секреты: держите их в user-secrets в разработке и в секрет-сторе в
  продакшне.
</Note>

<Tip>
  Готовый runnable-пример — `samples/dotnet/inproc/login/` в репозитории; его адрес берите на
  [veriqa.app/source](https://veriqa.app/source). Пример можно склонировать и взять за основу
  вместо ручной обвязки.
</Tip>

## 1. Установите пакеты

<CodeGroup>
  ```bash .NET CLI theme={null}
  dotnet add package Veriqa.Core.AuthServer
  dotnet add package Veriqa.Core.TransactionEngine

  # Каналы — один метапакет с базовым набором (Telegram, WhatsApp, Email):
  dotnet add package Veriqa.Core.BaseChannels
  # MAX в метапакет не входит — ставится отдельно, когда он нужен:
  dotnet add package Veriqa.Core.ChannelAdapter.Max

  # Только для EF Core-хранилищ на PostgreSQL — сборки миграций (шаг 2):
  dotnet add package Veriqa.Core.AuthServer.Migrations.PostgreSql
  dotnet add package Veriqa.Core.TransactionEngine.Migrations.PostgreSql
  ```

  ```xml PackageReference theme={null}
  <PackageReference Include="Veriqa.Core.AuthServer" Version="0.6.0" />
  <PackageReference Include="Veriqa.Core.TransactionEngine" Version="0.6.0" />

  <!-- Каналы: базовый набор одним метапакетом + MAX отдельно -->
  <PackageReference Include="Veriqa.Core.BaseChannels" Version="0.6.0" />
  <PackageReference Include="Veriqa.Core.ChannelAdapter.Max" Version="0.6.0" />

  <!-- Только для EF Core-хранилищ на PostgreSQL: -->
  <PackageReference Include="Veriqa.Core.AuthServer.Migrations.PostgreSql" Version="0.6.0" />
  <PackageReference Include="Veriqa.Core.TransactionEngine.Migrations.PostgreSql" Version="0.6.0" />
  ```
</CodeGroup>

<Tip>
  **Нужен один канал — ставьте только его пакет.** Каждый канал поставляется отдельно и приносит
  только свой SDK: `Veriqa.Core.ChannelAdapter.Telegram`, `.WhatsApp`, `.Max`, `.Email`. Тогда
  установке, которая пускает пользователей через Telegram, не приезжает почтовая библиотека.
  Пакет `Veriqa.Core.ChannelAdapter` приходит с любым из этих пакетов — отдельно его ставить не
  нужно. Поставить и метапакет, и отдельный канал — штатно: NuGet сведёт к одной копии.
</Tip>

## 2. Зарегистрируйте сервисы

`AddVeriqaAuthServer` читает конфигурацию из стандартного `IConfiguration` хоста. Внутри вы
выбираете хранилище OpenIddict, настраиваете хранилище транзакций и подключаете нужные каналы.

```csharp Program.cs theme={null}
using Microsoft.EntityFrameworkCore;   // UseNpgsql — только для продакшн-строк (Npgsql приезжает с пакетами *.Migrations.PostgreSql)
using Veriqa.Core.AuthServer.DependencyInjection;
using Veriqa.Core.AuthServer.Infrastructure;   // UseSecurityHeaders (шаг 3)
using Veriqa.Core.ChannelAdapter.DependencyInjection;   // AddTelegram, AddWhatsApp, AddMax, AddEmail
using Veriqa.Core.TransactionEngine.DependencyInjection;   // UseEfCoreStore — только для продакшн-строк

var builder = WebApplication.CreateBuilder(args);

// Продакшн-строки ниже берут строку подключения из конфигурации хоста:
// var connectionString = builder.Configuration.GetConnectionString("Veriqa");

builder.Services.AddVeriqaAuthServer(builder.Configuration, builder.Environment, authServer =>
{
    // Хранилище OpenIddict (клиенты, авторизации, токены). Умолчания нет — выбирайте явно.
    // Для разработки — волатильное in-memory:
    authServer.UseInMemoryOpenIddictStore();

    // Для продакшна — вместо строки выше реляционный провайдер (провайдер и строка подключения
    // принадлежат хосту; сборка миграций — пакетом Veriqa.Core.AuthServer.Migrations.PostgreSql):
    // authServer.UseOpenIddictDatabase(ef => ef.UseNpgsql(connectionString,
    //     npgsql => npgsql.MigrationsAssembly("Veriqa.Core.AuthServer.Migrations.PostgreSql")));

    // Хранилище транзакций
    authServer.ConfigureTransactionEngine(te =>
    {
        // Для разработки — InMemory:
        te.UseInMemoryStore();

        // Для продакшна — EF Core (сам метод приезжает пакетом
        // Veriqa.Core.TransactionEngine.EntityFrameworkCore; провайдер и строка подключения
        // принадлежат хосту; сборка миграций — пакетом
        // Veriqa.Core.TransactionEngine.Migrations.PostgreSql):
        // te.UseEfCoreStore(ef => ef.UseNpgsql(connectionString,
        //     npgsql => npgsql.MigrationsAssembly("Veriqa.Core.TransactionEngine.Migrations.PostgreSql")));
    });

    // Каналы подтверждения
    authServer.AddChannelAdapters(adapters =>
    {
        adapters.AddTelegram();
        adapters.AddWhatsApp();
        adapters.AddMax();
        adapters.AddEmail();
    });
});
```

<Warning>
  **У хранилища OpenIddict нет умолчания.** Хост, не вызвавший ни `UseInMemoryOpenIddictStore()`,
  ни `UseOpenIddictDatabase(...)`, вне окружения `Development` **не стартует**: проверка на старте
  останавливает приложение и называет лечение. В `Development` тот же текст пишется как `Warning`,
  и хост поднимается. Явный `InMemory` стартует в любом окружении, но на каждом старте
  предупреждает о волатильности: клиенты, токены и authorizations не переживают рестарт и не
  разделяются между репликами.
</Warning>

<Warning>
  **Вне `Development` обязательны и сертификаты токенов.** Это ключи OpenIddict, которыми сервер
  подписывает и шифрует `id_token` / `access_token`, — не HTTPS-сертификат. Без
  `Veriqa:OpenIddict:Server:SigningCertificatePath` (или `…SigningCertificateBase64`) и парного ключа
  `EncryptionCertificate…` хост не стартует; в `Development` вместо них генерируются dev-ключи. Ключи —
  в [справочнике конфигурации](/docs/ru/reference/configuration#server).
</Warning>

Подключайте только те каналы, что реально используете. Каждый адаптер активируется, только если
его секция в конфигурации включена (`"Enabled": true`) — иначе регистрируются лишь опции и
валидатор.

## 3. Соберите middleware pipeline

```csharp Program.cs theme={null}
var app = builder.Build();

app.UseSecurityHeaders();   // CSP, X-Frame-Options
app.UseStaticFiles();       // клиент SignalR для live-обновления статуса
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();

// Эндпоинты Veriqa: OIDC + webhooks каналов + статус транзакции + SignalR-хаб
app.MapVeriqaAuthServer();

app.Run();
```

<Note>
  Эндпоинты **Email-канала** (magic link и письмо в один тап) `MapVeriqaAuthServer` мапит автоматически —
  но только когда канал включён в конфигурации (`Veriqa:Channels:Email:Enabled: true`). Отдельный
  вызов не нужен.
</Note>

Каталог `wwwroot/locales` необязателен: без него сообщения каналов уходят на базовом языке, и
предупреждение на старте `Locale files directory not found …` ожидаемо. Переводить вручную ничего не
нужно: готовые локали (`en`, `ru`, `zh`, `pseudo`) поставляются пакетом
`Veriqa.Core.ChannelLocales` — как их подключить, описано в
[Локализации](/docs/ru/guides/customization#локализация). Так же ожидаемо на дефолтной конфигурации
`Initiator context geolocation is enabled but the GeoIP provider is unavailable`: вход работает, просто
гео-поля контекста инициатора остаются пустыми. В `Development` после старта идут ещё несколько десятков
диагностических строк `info` о разрешённой конфигурации — это нормально.

## 4. Настройте конфигурацию

Секреты каналов и OIDC-клиенты задаются в конфигурации. Всё, что принадлежит Veriqa, живёт
под корневым ключом `Veriqa`: секции каналов — под `Veriqa:Channels`, настройки auth-сервера —
рядом с ними в том же разделе.

```json appsettings.json theme={null}
{
  "Veriqa": {
    "Channels": {
      "Telegram": {
        "Enabled": true,
        "InboundVerification": "shared_secret",
        "BotToken": "YOUR_BOT_TOKEN",
        "UpdateMode": "Webhook",
        "WebhookBaseUrl": "https://your-domain.com",
        "WebhookSecretToken": "YOUR_SECRET"
      },
      "WhatsApp": {
        "Enabled": false,
        "InboundVerification": "signature",
        "Provider": "MetaCloudApi",
        "BusinessPhoneNumber": "+15551234567",
        "MetaCloudApi": {
          "PhoneNumberId": "YOUR_PHONE_NUMBER_ID",
          "AccessToken": "YOUR_ACCESS_TOKEN",
          "AppSecret": "YOUR_APP_SECRET",
          "WebhookVerifyToken": "YOUR_VERIFY_TOKEN"
        }
      },
      "Max": {
        "Enabled": false,
        "InboundVerification": "shared_secret",
        "BotToken": "YOUR_BOT_TOKEN",
        "UpdateMode": "Webhook",
        "WebhookBaseUrl": "https://your-domain.com",
        "WebhookSecretToken": "YOUR_SECRET"
      },
      "OutcomeNotice": {
        "DisplayIntent": "ReplacePrompt"
      }
    },
    "MessageTemplates": {
      "outcome-receipt-confirmed": {
        "Contract": {
          "Slots": [ { "Name": "app", "Type": "string", "Source": "ServerInitiatorContext" } ]
        },
        "ByType": {
          "login": {
            "BySurface": {
              "in-channel-messenger": {
                "Templates": [ "Signed in to {app} ✅", "Signed in ✅" ]
              }
            }
          }
        }
      },
      "outcome-receipt-declined": {
        "Contract": {
          "Slots": [ { "Name": "app", "Type": "string", "Source": "ServerInitiatorContext" } ]
        },
        "ByType": {
          "login": {
            "BySurface": {
              "in-channel-messenger": {
                "Templates": [ "Sign-in to {app} declined ❌", "Sign-in declined ❌" ]
              }
            }
          }
        }
      }
    },
    "TransactionEngine": {
      "TransactionTtlSeconds": 300
    },
    "OpenIddict": {
      "Clients": [
        {
          "ClientId": "my-app",
          "DisplayName": "My Application",
          "AllowedRedirectUris": [ "https://localhost:7020/signin-oidc" ],
          "AllowedScopes": [ "openid", "profile", "email", "offline_access" ],
          "AllowRefreshTokens": true
        }
      ]
    }
  }
}
```

<Warning>
  **Замените плейсхолдеры до первого запуска.** `YOUR_BOT_TOKEN` и ему подобные — не значения, с
  которыми стартуют. Telegram и MAX в режиме `UpdateMode: Webhook` — том, что показан выше —
  регистрируют вебхук при старте, поэтому с плейсхолдером токена хост не запустится. У WhatsApp
  случай тише: при `"Enabled": true` его креды проверяются на старте только на **наличие** —
  пропущенное или пустое значение старт как раз валит, а строка-плейсхолдер проходит, — поэтому хост
  поднимается чисто, а негодное значение вскрывается только при отправке первого подтверждения.
  Настоящие креды берутся в
  [Настройке каналов](/docs/ru/guides/channels) — поэтому выше `"Enabled": true` стоит ровно у
  одного канала. Остальные включайте, когда получите их креды.
</Warning>

<Note>
  **Две последние секции — то, что пользователь читает по завершении входа.**
  `Channels:OutcomeNotice:DisplayIntent` задаёт, где появляется квитанция исхода: `ReplacePrompt` —
  поставочное значение, выписанное здесь явно, чтобы настройка была видна, — ставит квитанцию на
  место сообщения с вопросом, а `NewMessage` оставляет вопрос в переписке и добавляет отдельное
  сообщение. Это намерение, а не обещание: Telegram и MAX его исполняют, WhatsApp всегда шлёт новое
  сообщение, а Email квитанцию исхода не показывает вовсе.

  `MessageTemplates` задаёт формулировки этих квитанций. Продукт везёт свои по адресу
  `ByType:login:BySurface:…`, а уровень перебирает все шаги адреса прежде, чем резолюция спустится
  на уровень ниже, — поэтому переобъявление достигается только по **тому же** адресу, отсюда и
  глубина `Templates` выше. `{app}` — серверный слот: для входа это `DisplayName` клиента. Последний
  шаг каждой лестницы не пользуется слотами, потому что ни один слот квитанции не гарантирован.
  Полные правила — в [Справочнике конфигурации](/docs/ru/reference/configuration#veriqamessagetemplates).
</Note>

<Note>
  WhatsApp работает через официальный **Meta Cloud API** (`Provider: MetaCloudApi`);
  `BusinessPhoneNumber` — в формате E.164 для deep link. Подключайте только используемые каналы —
  секция без `"Enabled": true` канал не активирует, поэтому две секции выше показаны выключенными, а
  не убраны. Полная настройка Email (SMTP для Pull и inbound для Push) — в
  [Настройке каналов](/docs/ru/guides/channels#email).
</Note>

<Warning>
  `InboundVerification` — **обязательный** ключ каждой канальной секции: он объявляет, как
  проверяется подлинность входящего события канала. Включённый канал без него не даст хосту
  стартовать. Значения выше — для показанных режимов (`UpdateMode: Webhook`); в режиме `Polling`
  ставится `outbound_fetch`. Таблица по всем каналам и режимам — в
  [Настройке каналов](/docs/ru/guides/channels).
</Warning>

<Tip>
  **Запускаете на `localhost`? Переключите канал в `Polling`.** Секции Telegram и MAX выше
  показаны в режиме `Webhook`, которому нужен публичный HTTPS-адрес: хост регистрирует вебхук на
  старте, и Telegram должен до него достучаться. До `https://localhost:7300` — адреса, на котором
  идёт [контрольная точка](#10-контрольная-точка) в конце этой страницы, — он не достучится.
  `Polling` публичного URL не требует, потому что Veriqa забирает апдейты сама:

  ```json theme={null}
  "Telegram": {
    "Enabled": true,
    "InboundVerification": "outbound_fetch",
    "BotToken": "YOUR_BOT_TOKEN",
    "UpdateMode": "Polling"
  }
  ```

  Оба ключа меняются вместе: `outbound_fetch` — это значение, идущее с `Polling`, а
  `shared_secret` в нём даёт `Warning` на старте. `WebhookBaseUrl` и `WebhookSecretToken` в этом
  режиме не используются, их можно не задавать. Годится, чтобы дойти до рабочего входа до того,
  как появились домен и прокси, но не для развёрнутой установки — да и там один опрашивающий на
  бота ([запуск более одного инстанса](/docs/ru/quickstart/self-hosted#запуск-более-одного-инстанса)).
</Tip>

`redirect_uri` сверяется с `AllowedRedirectUris` точным совпадением — wildcard не
поддерживаются. Валидатор при старте требует абсолютные URI; в продакшне используйте `https`
(`http` — только для loopback-адреса, например `localhost` или `127.0.0.1`, в разработке).

## 5. Подключите клиентское приложение

Всё, что выше, — сторона issuer'а. Клиент, который в него входит, — **обычный OIDC Relying
Party**: ничего специфичного для Veriqa в нём нет, это та же обвязка, что вы написали бы для
любого OpenID Connect провайдера. В ASP.NET Core эта обвязка приезжает одним пакетом:

```bash theme={null}
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
```

```csharp Program.cs (клиентское приложение) theme={null}
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        // Адрес хоста с Veriqa. Эндпоинты и ключи подхватываются через discovery
        // ({Authority}/.well-known/openid-configuration) — руками их прописывать не нужно.
        options.Authority = "https://localhost:7300";
        options.ClientId = "my-app";
        options.ResponseType = "code";
        options.UsePkce = true;

        // Должен совпадать с записью AllowedRedirectUris из шага 4 — сверка точная.
        options.CallbackPath = "/signin-oidc";

        // openid и profile хендлер запрашивает сам — здесь только дополнительные scope.
        // Каждый должен быть перечислен в AllowedScopes клиента на стороне issuer'а.
        options.Scope.Add("email");
        options.GetClaimsFromUserInfoEndpoint = true;
        options.SaveTokens = true;

        // Veriqa выдаёт имя пользователя коротким OIDC-claim `name`, а User.Identity.Name по
        // умолчанию ищет ClaimTypes.Name (длинный URI WS-Federation) — без этой строки страница
        // ниже после успешного входа поздоровается с пустым именем.
        options.TokenValidationParameters.NameClaimType = "name";
    });

builder.Services.AddAuthorization();
```

Неаутентифицированный визит отправляется на вход обычным `Challenge` — на страницу Veriqa с QR и
кнопками каналов:

```csharp Program.cs (клиентское приложение) theme={null}
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/", (ClaimsPrincipal user) =>
    user.Identity?.IsAuthenticated is true
        ? Results.Text($"Hello, {user.Identity.Name}!")
        : Results.Challenge());
```

Три значения должны сойтись с конфигурацией из шага 4. Симптом у каждого рассинхрона свой —
по нему и опознаётся причина:

| Клиент | Issuer (`Veriqa:OpenIddict:Clients[]`) | Если не сойдётся |
| - | - | - |
| `Authority` | адрес хоста, где поднят `MapVeriqaAuthServer` | клиент падает на discovery `{Authority}/.well-known/openid-configuration` — OIDC-ответа об ошибке не будет вовсе |
| `ClientId` | `ClientId` | `invalid_client` от OpenIddict |
| `CallbackPath` (на адресе клиента) | запись в `AllowedRedirectUris` | `invalid_request`: сверка буквальная, wildcard не поддерживаются |

`dotnet new web` выдаёт каждому проекту случайные порты, поэтому из коробки ни `Authority`, ни
`redirect_uri` не указывают на запущенный процесс — [шаг 10](#10-контрольная-точка) поднимает оба
на адресах, использованных здесь.

<Note>
  Клиент `my-app` из шага 4 задан **без** `ClientSecret` — значит, он публичный, и Veriqa требует
  от него PKCE (S256). Поэтому `UsePkce = true`, а `ClientSecret` не задаётся. Для
  конфиденциального клиента секрет прописывается **с обеих сторон**.
</Note>

<Tip>
  Запускаемая пара «issuer + клиент» лежит в репозитории
  ([veriqa.app/source](https://veriqa.app/source)): `samples/dotnet/inproc/login` (хост с
  Veriqa, порт 7300) и `samples/dotnet/inproc/login-client` (это приложение, порт 7020). Они
  сходятся по конфигурации из коробки — запустите оба и пройдите вход. Чтобы вход **дошёл до
  конца**, на issuer'е нужен включённый канал с кредами: сэмпл поставляется со всеми каналами
  `"Enabled": false`, и без единого включённого страница входа отвечает `no_channels_available`.
</Tip>

Veriqa не выставляет end-session-эндпоинт, поэтому federated sign-out не поддерживается: выход на
стороне клиента гасит его собственную cookie-сессию, но не сессию issuer'а.

## 6. Разберитесь в потоке входа

Пользователь начинает транзакцию на десктопе и подтверждает её в доверенном канале на телефоне.
Veriqa отслеживает транзакцию и только потом отдаёт выпуск результата OpenIddict.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant App as Клиент (desktop)
    participant V as Veriqa (в вашем хосте)
    participant OI as OpenIddict
    participant Ch as Доверенный канал (телефон)

    App->>V: Старт auth-транзакции (/connect/authorize)
    V-->>App: Страница входа + QR / кнопки каналов
    Ch->>V: Пользователь открывает канал, подтверждает вход
    V->>V: Обновление состояния транзакции
    App->>V: Live-статус (SignalR) или polling
    opt Подтверждение на веб-странице (по умолчанию для Email)
        V-->>App: Страница подтверждения (/connect/authorize/callback)
        App->>V: Пользователь подтверждает (/connect/authorize/callback/confirm)
    end
    V->>OI: Завершение аутентификации
    OI-->>App: code → /connect/token → токены
```

## 7. Эндпоинты

`MapVeriqaAuthServer` мапит стандартный OIDC-контракт и служебные эндпоинты:

| Endpoint | Метод | Назначение |
| - | - | - |
| `/connect/authorize` | GET | Вход в Authorization Code Flow |
| `/connect/authorize/callback` | GET | Callback после подтверждения в канале |
| `/connect/authorize/callback/confirm` | POST | Подтверждение входа кнопкой на веб-странице. `Veriqa:LoginConfirmation:Mode` по умолчанию — `ChannelDefault`: канал, который умеет подтверждать внутри себя (Telegram, WhatsApp, MAX), спрашивает там, Email — на этой странице; при `OnWebPage` спрашивает эта страница для любого канала |
| `/connect/authorize/callback/claims` (+ `/skip`, `/cancel`, `/email`) | POST | Действия страницы добора недостающих полей профиля: отправить поля, пропустить необязательные, отменить вход; `/email` — действия её блока email (отправить код или ссылку, проверить код, сохранить адрес, сменить способ, пропустить email) |
| `/auth/claims?session_id=…&form_token=…` | GET | Та же страница на устройстве, где подтвердили вход, — открывается ссылкой с токеном формы этого входа; её форма отправляет действия выше с этим токеном вместо cookie браузера |
| `/connect/token` | POST | Обмен `code` на токены |
| `/connect/userinfo` | GET | Claims пользователя |
| `/connect/revoke` | POST | Отзыв токенов (RFC 7009) — обрабатывается OpenIddict нативно |
| `/api/transaction/{id}/status` | GET | Polling статуса транзакции (fallback без WebSocket) |
| `/api/transaction/confirmation` | POST | Серверное создание транзакции подтверждения: приложение аутентифицируется по client credentials и получает `transaction_id` и точку входа для пользователя |
| `/api/transaction/{id}/result` | GET | Результат транзакции подтверждения (токен приложения обязателен) |
| `/api/collected-claims?sub=…` | DELETE | Удаление собранных claims субъекта (client credentials клиента с `AllowCollectedClaimsDeletion`); маршрут есть, только когда зарегистрировано [хранилище собранных claims](/docs/ru/guides/storage#хранилище-собранных-claims) |
| `/auth/transaction?session_id=…` | GET | Страница входа уже созданной транзакции подтверждения |
| `/auth/transaction/confirm` (+ `/accept`, `/decline`) | GET / POST | Страница вопроса подтверждения и ответ пользователя |
| `/hubs/auth` | WebSocket (SignalR) | Live-статус транзакции для страницы входа |
| `/api/channels/telegram/webhook` | POST | Webhook Telegram-адаптера |
| `/api/channels/whatsapp/webhook` | GET/POST | Верификация и webhook WhatsApp-адаптера |
| `/api/channels/max/webhook` | POST | Webhook MAX-адаптера |

Эндпоинты Email-канала мапятся тем же `MapVeriqaAuthServer` автоматически — только при
включённом канале (`Veriqa:Channels:Email:Enabled: true`):

| Endpoint | Метод | Назначение |
| - | - | - |
| `/auth/email/start` | GET/POST | Страница ввода email и отправка magic link (Pull) |
| `/auth/email/confirm` | GET/POST | Подтверждение по magic link (Pull) |
| `/auth/email/push/compose` | GET | Compose-страница Push-режима (письмо в один тап) |
| `/auth/email/claim` | GET/POST | Подтверждение адреса email по ссылке из письма добора claims |
| `/api/channels/email/inbound` | POST | Webhook входящих писем от inbound-провайдера (Push) |

## 8. Claims пользователя

Итоговый набор claims формирует маппер по умолчанию из завершённой транзакции:

| Claim | Значение |
| - | - |
| `sub` | Стабильный идентификатор субъекта (обязателен) |
| `amr` | Метод аутентификации — тип канала подтверждения (`telegram`, `whatsapp`, `max`, `email`). В `id_token` — **массивом строк** даже при одном методе: `"amr": ["telegram"]` (OIDC Core 1.0 §2). Тот же метод виден и в `access_token` / ответе `/connect/userinfo` |
| `auth_time` | Момент завершения аутентификации (Unix) |
| `name`, `email`, … | Дополнительные claims из resolved identity — зависят от канала и запрошенных scopes |
| `channel_type`, `channel_user_id` | Канал подтверждения и id пользователя в нём — **только со scope Veriqa `channel`** |
| `picture` | Аватар пользователя — **только со scope Veriqa `avatar`** |

`picture` приходит, только когда выполнены все три условия: клиент запрашивает scope `avatar`
(`options.Scope.Add("avatar")`), scope разрешён в `AllowedScopes` клиента и включён
`GetClaimsFromUserInfoEndpoint = true` — в `id_token` аватар не попадает никогда, его отдаёт только
userinfo. Как его забрать, не раздувая cookie, — см.
[OIDC + Veriqa: разбор](/docs/ru/concepts/oidc-explainer#resolved-identity-и-claims).

Маппинг переопределяется своим `IClaimsMapper` — см. [Настройку и кастомизацию](/docs/ru/guides/customization#claims-маппер).

## 9. Выбор канала через acr\_values

Клиент может ограничить вход конкретным каналом через стандартный OIDC-параметр `acr_values`
формата `channel:{тип}`:

```
GET /connect/authorize
  ?client_id=my-app
  &response_type=code
  &scope=openid profile
  &acr_values=channel:telegram
```

Несколько значений (`channel:telegram channel:whatsapp`) — пользователь выбирает из
перечисленных. Без параметра доступны все включённые каналы.

## 10. Контрольная точка

Поднимите оба процесса на адресах, которых ждёт конфигурация: issuer — на порту из `Authority`,
клиент — на порту своего `redirect_uri`:

```bash theme={null}
# В проекте issuer'а (хост с Veriqa):
dotnet run --urls "https://localhost:7300"

# В проекте клиента, во втором терминале:
dotnet run --urls "https://localhost:7020"
```

Годится и прописать те же адреса в `applicationUrl` файла `Properties/launchSettings.json` каждого
проекта. Клиент сам ходит к issuer'у по `https`, поэтому dev-сертификат ASP.NET Core должен
существовать и быть доверенным: создайте его командой `dotnet dev-certs https`, затем доверьте
командой `dotnet dev-certs https --trust`. На Linux команда доверия тоже работает и печатает значение
`SSL_CERT_DIR`, которое нужно выставить; строка
`There was an error trusting the HTTPS developer certificate.`, которой она заканчивается, там
**ожидаема** и провала шага не означает — экспортируйте напечатанное значение, оставив в списке
каталог сертификатов вашего дистрибутива, иначе перестанут проверяться чужие цепочки (`restore` с
nuget.org, любые исходящие HTTPS-вызовы):

```bash theme={null}
export SSL_CERT_DIR="$HOME/.aspnet/dev-certs/trust:$(openssl version -d | cut -d'"' -f2)/certs"
```

Второй элемент — каталог сертификатов вашего дистрибутива, и команда вычисляет его, а не
зашивает: `openssl version -d` печатает `OPENSSLDIR` — каталог конфигурации OpenSSL, а не сами
сертификаты, — а файлы CA с hash-ссылками, по которым OpenSSL и ищет, лежат в его подкаталоге
`certs`, отсюда `/certs` на конце. На Debian и Ubuntu получается `/usr/lib/ssl/certs`, на других
дистрибутивах префикс другой — поэтому он читается, а не выписывается. После этого сертификат
доверен. Подробности — в разделе «Trust HTTPS certificate on Linux» статьи
[Enforce HTTPS in ASP.NET Core](https://learn.microsoft.com/aspnet/core/security/enforcing-ssl).

<Steps>
  <Step title="Запустите приложение">
    Поднимите оба процесса, как показано выше, и откройте клиента по адресу `https://localhost:7020`.
  </Step>

  <Step title="Запустите поток">
    Выберите «Войти через Veriqa» и отсканируйте QR доверенным каналом на телефоне. У Email QR
    есть в режиме Push; в режиме Pull вместо него — поле адреса и кнопка «Get a link».
  </Step>

  <Step title="Подтвердите на телефоне">
    Подтвердите запрос в MAX / Telegram / WhatsApp / другие мессенджеры / Email. Десктоп обновит
    статус по SignalR или polling.
  </Step>

  <Step title="Подтвердите на десктопе">
    У Email — и у любого канала при `Veriqa:LoginConfirmation:Mode` = `OnWebPage` — страница входа
    затем спрашивает ещё раз: подтвердите вход там.
  </Step>

  <Step title="Проверьте результат">
    Убедитесь, что вы вошли и ожидаемые claims на месте.
  </Step>
</Steps>

## Если Veriqa работает отдельно

Всё выше — **встроенный** режим: Veriqa живёт пакетами внутри вашего хоста. Тот же сервер может
работать и **отдельно**, а приложение входит в него по обычному OIDC — облако Veriqa, ваш
контейнер Docker или служба (systemd либо служба Windows) на вашей машине. Для .NET-приложения это
меняет дерево зависимостей, а не код: остаётся клиентская половина
[шага 5](#5-подключите-клиентское-приложение), а все пакеты Veriqa уходят — ни один компонент
Veriqa не попадает в вашу сборку и в SBOM.

<Note>
  **Veriqa Cloud сейчас недоступен** — строка про облако ниже описывает контракт его API.
  Используйте self-hosted; подробности — в [Veriqa Cloud](/docs/ru/cloud/overview).
</Note>

| Режим | Какой `Authority` указывает клиент | Где заводится клиент |
| - | - | - |
| Облако | Адрес проекта `https://<хост облака>/p/{project}` — на экране Project settings, read-only | Раздел **Applications / OIDC clients** консоли — см. [подключение к облаку](/docs/ru/cloud/connect) |
| Self-hosted | Адрес вашего сервера, например `https://auth.your-domain.com` | Секция `Veriqa:OpenIddict:Clients` в конфигурации сервера — см. [шаг 4 self-hosted quickstart](/docs/ru/quickstart/self-hosted#4-объявите-свой-клиент) |
| Встроенный (всё выше) | Адрес хоста, в котором живёт Veriqa, например `https://localhost:7300` | Та же секция в конфигурации этого же хоста — [шаг 4](#4-настройте-конфигурацию) |

Обвязка — та же, что в [шаге 5](#5-подключите-клиентское-приложение), с одним отличием: адрес
issuer'а берётся из конфигурации, а не из кода, потому что это единственное значение, которое
меняется между режимами и между окружениями.

```csharp Program.cs (клиентское приложение) theme={null}
builder.Services
    .AddAuthentication(/* две схемы из шага 5 */)
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        // Единственное значение, которое отличается у облака, self-hosted и встроенного режима.
        // Остальное — эндпоинты, ключи, поддерживаемые scopes — клиент читает из discovery.
        options.Authority = builder.Configuration["Veriqa:Authority"];
        options.ClientId = builder.Configuration["Veriqa:ClientId"];
        options.ResponseType = "code";
        options.UsePkce = true;
        options.CallbackPath = "/signin-oidc";
        options.TokenValidationParameters.NameClaimType = "name";
    });
```

```json appsettings.json (клиентское приложение) theme={null}
{
  "Veriqa": {
    "Authority": "https://auth.your-domain.com",
    "ClientId": "my-app"
  }
}
```

Confidential-клиент добавляет `options.ClientSecret` — из user-secrets в разработке и из
секрет-стора в продакшне, но не из `appsettings.json`. В облаке секрет выдаётся отдельным явным
действием на экране Applications и показывается **один раз**.

Больше ничто на этой странице от режима не зависит: эндпоинты [шага 7](#7-эндпоинты), claims
[шага 8](#8-claims-пользователя) и ограничение канала через `acr_values`
([шаг 9](#9-выбор-канала-через-acr-values)) — это поведение issuer'а, и клиент видит его через
discovery, в каком бы режиме тот ни работал.

**Как именно поставлен отдельный сервер** — следующий выбор, и на клиентскую сторону он не влияет:
контейнер Docker — [быстрый старт self-hosted](/docs/ru/quickstart/self-hosted) — либо архив, который
ставится юнитом systemd или службой Windows, без Docker и без рантайма .NET на машине —
[установка службой ОС](/docs/ru/quickstart/os-service).

## Дальше

<CardGroup cols={2}>
  <Card title="Настройка и кастомизация" icon="sliders" href="/docs/ru/guides/customization">
    Дизайн страницы входа, локализация, точки расширения.
  </Card>

  <Card title="Hardening к продакшну" icon="shield-check" href="/docs/ru/guides/hardening">
    Что проверить перед выпуском в продакшн.
  </Card>

  <Card title="OIDC + Veriqa: разбор" icon="key" href="/docs/ru/concepts/oidc-explainer">
    Где Veriqa встаёт в стандартный OIDC-поток.
  </Card>

  <Card title="Архитектура" icon="cube" href="/docs/ru/concepts/architecture">
    Строительные блоки коннектора.
  </Card>

  <Card title="Veriqa отдельным сервером" icon="server" href="/docs/ru/quickstart/self-hosted">
    Тот же issuer вне вашей сборки — в Docker или службой ОС.
  </Card>
</CardGroup>


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