---
name: veriqa
description: Use when integrating Veriqa, an OpenID Connect provider (built on OpenIddict) that lets users sign in or confirm actions with one tap in a messaging app (Telegram, WhatsApp, other channels or Email) instead of passwords or SMS codes. Reach for it to add sign-in to an app on any stack, embed Veriqa in an ASP.NET Core host, run the self-hosted server (Docker or OS service), enable and configure channels, create server-to-server confirmation transactions (including step-up with expected_identities and AI-agent tool-call approvals), and diagnose startup failures and OIDC error codes.
compatibility: Relying party on any stack with a standard OIDC client. Embedded mode needs an ASP.NET Core host on .NET 10.
metadata:
  product: Veriqa
  docs: https://veriqa.app/docs
---

# Veriqa

## Product summary

Veriqa is a **standard OpenID Connect provider**. The user scans a QR code (or taps a channel button on their phone) and confirms with one tap in a messaging app or mailbox. The relying party stays a **plain OIDC client**: no Veriqa package, no SDK. On .NET, Veriqa extends OpenIddict: it runs the sign-in transaction between `/connect/authorize` and `/connect/token`; OpenIddict issues the tokens.

| Deployment | What it is | Start |
| --- | --- | --- |
| Embedded (NuGet) | Inside your ASP.NET Core host | [Quickstart .NET](https://veriqa.app/docs/quickstart/dotnet.md) |
| Self-hosted (Docker) | Its own OIDC server; HTTP on port 8080, TLS terminated at your proxy | [Quickstart self-hosted](https://veriqa.app/docs/quickstart/self-hosted.md) |
| OS service | Same server from an archive as a systemd unit or Windows service; no Docker, no .NET runtime | [OS service](https://veriqa.app/docs/quickstart/os-service.md) |

Eight channel adapters are built in. Do not hardcode a channel list: read [Supported channels](https://veriqa.app/docs/guides/supported-channels.md). A channel Veriqa does not ship can be connected over HTTP in any language ([custom adapter](https://veriqa.app/docs/guides/custom-channel-adapter.md)).

## When to use this skill

- Passwordless sign-in and sign-up, SMS-code replacement, step-up, channel linking.
- A backend asking a person to confirm an action, including an AI agent's tool call.
- `Veriqa:*` configuration, channels, clients; startup or OIDC errors.

## Two core mechanisms

| | Sign-in | Server-to-server transaction |
| --- | --- | --- |
| Started by | The user's browser, by a redirect | Your backend, by an API call |
| Protocol | `/connect/authorize` → `/connect/token` | Client Credentials + `POST /api/transaction/confirmation` |
| Who may confirm | Anyone (this is how new accounts are created) | Anyone, or only the people listed in `expected_identities` |
| Result | `id_token` (`sub`, `auth_time`, requested claims); you open the session | `outcome`: `confirmed`, `declined`, `expired`, `failed`; optionally the confirmer's `id_token` |

Rule: "the user is signed in" → sign-in; "this action is confirmed" and your server performs it → server-to-server. There is no "scenario" parameter: sign-in scenarios differ only in authorize parameters (`acr_values=channel:<type>`, `scope`, `ui_locales`).

## Recipes

### Sign-in, .NET embedded

1. Packages: `Veriqa.Core.AuthServer`, `Veriqa.Core.TransactionEngine`, and channels: the metapackage `Veriqa.Core.BaseChannels` or `Veriqa.Core.ChannelAdapter.<Channel>` per channel (channels outside the metapackage, e.g. `Veriqa.Core.ChannelAdapter.Max`, are always added explicitly).
2. Register:
   ```csharp
   builder.Services.AddVeriqaAuthServer(builder.Configuration, builder.Environment, authServer =>
   {
       authServer.UseInMemoryOpenIddictStore(); // production: UseOpenIddictDatabase(ef => ...)
       authServer.ConfigureTransactionEngine(te => te.UseInMemoryStore()); // production: UseEfCoreStore(...)
       authServer.AddChannelAdapters(adapters => adapters.AddTelegram());
   });
   // pipeline: UseSecurityHeaders, UseStaticFiles, UseAuthentication, UseAuthorization, UseRateLimiter
   app.MapVeriqaAuthServer();
   ```
3. Declare the client under `Veriqa:OpenIddict:Clients` (`ClientId`, `AllowedRedirectUris`, `AllowedScopes`). No `ClientSecret` = public client = PKCE `S256` required.
4. Client app: `AddOpenIdConnect` with `ResponseType = "code"`, `UsePkce = true`, `TokenValidationParameters.NameClaimType = "name"`, an `AccessDeniedPath` (the **Cancel sign-in** button returns `access_denied`) and an `OnRemoteFailure` handler (`server_error`); otherwise both end in HTTP 500.

### Sign-in, any stack against a self-hosted server

1. Run the server ([self-hosted](https://veriqa.app/docs/quickstart/self-hosted.md) or [OS service](https://veriqa.app/docs/quickstart/os-service.md)). Env vars use `__` for `:`. The image ships **no** clients: mount `/app/config/veriqa.json` (the path can be changed with `VERIQA_SETTINGS_FILE`) with `Veriqa:OpenIddict:Clients`.
2. Set `Veriqa:OpenIddict:Database:Provider` (`InMemory`, `PostgreSQL`, `SqlServer`), both token certificates, and `ForwardedHeaders:KnownProxies`/`KnownIPNetworks` for the TLS proxy.
3. Write a standard Authorization Code + PKCE client against `{issuer}/.well-known/openid-configuration`: Node `openid-client` 6.x ([Node](https://veriqa.app/docs/quickstart/node.md)), Spring Boot `spring-boot-starter-oauth2-client` + `spring-boot-starter-web` ([Java](https://veriqa.app/docs/quickstart/java.md)), Authlib ([Python](https://veriqa.app/docs/quickstart/python.md)).

### Enable a channel

1. Embedded: call `adapters.Add<Channel>()` (the self-hosted image already contains every built-in channel).
2. Set `Veriqa:Channels:<Channel>:Enabled: true` plus its credentials, from a secret store, never a committed file.
3. Set `InboundVerification` (required; allowed values `signature`, `shared_secret`, `outbound_fetch`, `none`) to match the real mode:

| Channel | Mode | Value |
| --- | --- | --- |
| Telegram, MAX | `UpdateMode: Webhook` / `Polling` | `shared_secret` / `outbound_fetch` |
| WhatsApp (Meta or Twilio), Instagram, Messenger, Viber | webhook | `signature` |
| WeChat | webhook | `shared_secret` |
| Email | Push with `Inbound:Provider: ImapPolling` / any other setup | `outbound_fetch` / `shared_secret` |

4. Webhook mode needs a public HTTPS `WebhookBaseUrl`; on `localhost` use `UpdateMode: Polling` with `outbound_fetch`. Details per channel: [Channel setup](https://veriqa.app/docs/guides/channels.md).

### Server-to-server confirmation

Client entry (the action type is declared **in the client entry**, not in the host section `Veriqa:MessageTemplates`):

```json
{ "ClientId": "payments-backend", "ClientSecret": "<secret store>",
  "AllowClientCredentials": true, "AllowConfirmationTokenGrant": true,
  "AllowedScopes": [ "openid", "channel" ],
  "MessageTemplates": { "approve-payment": {
    "Contract": { "Slots": [
      { "Name": "amount", "Type": "string", "MaxLength": 32, "Required": true },
      { "Name": "payee", "Type": "string", "MaxLength": 64 } ] },
    "Templates": [ "Approve a payment of {amount} to {payee}?", "Approve a payment of {amount}?" ] } } }
```

1. `POST /connect/token`, `grant_type=client_credentials` (Basic auth).
2. `POST /api/transaction/confirmation` with `action_type`, `slot_values`, an `idempotency_key`.
3. Show `channel_entry.qr` (PNG data URI) or open `channel_entry.url`; do not show after `response_valid_until`. The entry works as a bearer credential: show it only to the person you are asking.
4. Poll `GET /api/transaction/{id}/result` while `outcome` is `pending`.
5. Optional: exchange a `confirmed` transaction once with `grant_type=urn:veriqa:params:oauth:grant-type:confirmation&transaction_id=…&scope=openid channel`.

Contract rules: `MaxLength` is mandatory on caller `string`/`entity_ref` slots (ceiling 128); the last template step may use only `Required` (caller) or `Guaranteed` (server) slots; `slot_values` up to 16 KB; `ttl_seconds` clamped to 60…1800. `{app}` in a confirmation is the `ClientId`.

### Step-up

- **Before a sensitive operation in a signed-in session:** send a new authorize request and check `auth_time` in the new `id_token` against your own freshness window. Veriqa does not read `max_age`; your own cookie is not evidence. See [Step-up](https://veriqa.app/docs/scenarios/step-up.md).
- **Only the account owner may confirm:** declare the comparable type in the client entry, `"IdentityMatchComparableTypes": [ { "Name": "telegram_user_id", "ClaimName": "channel_user_id", "Normalization": "Exact" } ]` (default set is empty; normalizations `Exact`, `E164Phone`, `EmailDomainCaseFold`). Send `"expected_identities": { "telegram_user_id": "<stored channel_user_id>" }` (example values). Act only when `outcome` is `confirmed` **and** `matched_type` equals your type: a confirmation by someone else comes back `confirmed` with `matched_type: null`.

### AI-agent tool-call approval

Put the approval policy where tools are invoked, not in the prompt: a tool with side effects maps to a declared action type, and its arguments fill the slots. Create the confirmation, show its QR code, wait; run the tool only on `confirmed`, and on `declined` or `expired` return a refusal as the tool result. The person reads the declared wording, never agent-written text. Samples: `samples/dotnet/inproc/agent-approval`, `samples/claude-code` ([Approvals in Claude Code](https://veriqa.app/docs/research/claude-code-approvals.md)).

## Key configuration (root `Veriqa`)

| Key | Default / rule |
| --- | --- |
| `OpenIddict:Database:Provider` | No default; if unset, startup fails outside `Development`. Embedded: choose in code instead |
| `OpenIddict:Server:Signing…`/`Encryption…Certificate{Path,Base64,Password}` | Both required outside `Development` |
| `OpenIddict:Server:AccessTokenLifetimeSeconds` / `RefreshTokenLifetimeSeconds` / `AuthorizationCodeLifetimeSeconds` | `3600` / `1209600` / `300` |
| `OpenIddict:Clients[]` | `ClientId` (unique), `AllowedRedirectUris` (exact, absolute), `AllowedScopes`, `AllowRefreshTokens`, `AllowClientCredentials`, `AllowConfirmationTokenGrant`, `AllowIntrospection` (all `Allow*` off by default) |
| `TransactionEngine:TransactionTtlSeconds` / `CompletedRetentionSeconds` | `300` / `600` |
| `TransactionEngine:Store:ConnectionString` | Empty = in-memory store; `Provider` default `PostgreSQL` |
| `ScopesClaims:Scopes` | Empty = `openid`, `profile`, `email`, `phone`, `offline_access`, `channel`, `avatar`; non-empty replaces the set |
| `LoginConfirmation:Mode` | `ChannelDefault`; also `None`, `InChannel`, `OnWebPage` |
| `RateLimit` | Per route, per instance (`Authorize` 10/60s, `Token` 20/60s); no per-IP limits |
| `SignalR:RedisConnectionString`, `DataProtection:RedisConnectionString` or `:KeysDirectory` | Required to run more than one instance |

## Gotchas and error codes

| Symptom | Fix |
| --- | --- |
| Host does not start | Read the message: missing `InboundVerification`, no OpenIddict store, missing certificates, incomplete client entry, unknown `Provider` value. Config is validated fail-fast |
| `ID2029` / `ID2054` (no `code_challenge`) | Public client: add PKCE `S256` or make it confidential |
| `ID2032`, `invalid_target` `ID2190`, `ID2087` | `plain` PKCE or non-`code` `response_type`; a `resource` parameter (quirk `drop-resource-parameter`); credentials in header and body |
| Token request refused for `scope` | Do not send `scope` with `grant_type=authorization_code` |
| `ID2083` "This server only accepts HTTPS requests" | Issuer over HTTP: trust the proxy, forward `X-Forwarded-Proto` |
| `access_denied` / `server_error` at `redirect_uri` | Canceled or declined / failed on the Veriqa side (its log); offer to start again |
| `browser_nonce_mismatch` | Authorize page reloaded; retry without reloading |
| `no_channels_available`, `503 channel_display_failed` | No channel enabled, or a bad token in `Polling` mode (`/health/channels`) |
| `400 action_type_unknown` / `candidate_type_undeclared` | Action type not in the client entry / `expected_identities` type not declared |
| `401` / `403` / `404 transaction_not_found` | No token / user token / unknown, removed or another client's transaction |
| `Conflict: terminated by other getUpdates request` | Two pollers on one bot token |

Also: the access token is opaque (use `/connect/introspect`, not JWT parsing); no `end_session_endpoint`; `picture` needs the `avatar` scope and comes only from userinfo; Telegram never provides an email address; replacing a certificate invalidates live tokens; an EF Core `__EFMigrationsHistory` error on the first start against an empty database is normal.

## Before finishing a task

- [ ] Every channel section has a deliberate `InboundVerification`; webhook URLs are reachable over HTTPS.
- [ ] Secrets come from user-secrets or a secret store, not committed config.
- [ ] `redirect_uri` exactly matches `AllowedRedirectUris`; `http` only for loopback in development.
- [ ] Outside `Development`: a persistent OpenIddict store and both certificates.
- [ ] `{issuer}/.well-known/openid-configuration` returns `https` URLs; `/health/live` and `/health/ready` respond.
- [ ] A real confirmation from a phone completes the flow; server-to-server code handles every `outcome` and checks `matched_type` where it sent `expected_identities`.

## Links

Every page is served as Markdown at `<page>.md`. Index: https://veriqa.app/docs/llms.txt · full text: https://veriqa.app/docs/llms-full.txt · docs MCP server: https://veriqa.app/docs/mcp · source: https://veriqa.app/source

[Basic scenarios](https://veriqa.app/docs/basic-scenarios.md) · [Integration scenarios](https://veriqa.app/docs/guides/scenarios.md) · [Configuration](https://veriqa.app/docs/reference/configuration.md) · [Confirmation API](https://veriqa.app/docs/reference/confirmation-api.md) · [OIDC support](https://veriqa.app/docs/reference/oidc-capabilities.md) · [Limitations](https://veriqa.app/docs/reference/limitations.md) · [Channel setup](https://veriqa.app/docs/guides/channels.md) · [Customization](https://veriqa.app/docs/guides/customization.md) · [Hardening](https://veriqa.app/docs/guides/hardening.md) · [Troubleshooting](https://veriqa.app/docs/guides/troubleshooting.md) · [Reverse proxy](https://veriqa.app/docs/guides/reverse-proxy.md) · [Samples](https://veriqa.app/docs/samples.md) · [Confirmation samples](https://veriqa.app/docs/samples/confirmation.md)
