Skip to main content
Veriqa embeds in your ASP.NET Core host and runs as an OpenIddict auth server: it hosts the OIDC endpoints, the sign-in page with a QR code and channel buttons, the transaction engine and the channel adapters. No separate process or Docker required.
Prefer to keep Veriqa out of your build entirely? It also runs as a separate server your app talks to over plain OIDC — in the cloud, in Docker or as an OS service — with no Veriqa package in your dependency tree or SBOM. Veriqa running elsewhere covers what that changes for a .NET client.
Samples use placeholders (my-app, bot tokens). Replace them with your own values and never commit real secrets: keep them in user-secrets in development and a secret store in production.
A runnable sample lives at samples/dotnet/inproc/login/ in the repository — the repository address is at veriqa.app/source. Clone it and start from there instead of wiring everything by hand.

1. Install the packages

Need a single channel? Install just that one. Every channel ships on its own and brings only its own SDK: Veriqa.Core.ChannelAdapter.Telegram, .WhatsApp, .Max, .Email. A deployment that signs users in through Telegram then never receives an email library. The Veriqa.Core.ChannelAdapter package arrives with any of these packages — you do not install it separately. Installing both the metapackage and a separate channel is fine: NuGet resolves it to one copy.

2. Register the services

AddVeriqaAuthServer reads configuration from the host’s standard IConfiguration. Inside it you choose the OpenIddict store, configure the transaction store and add the channels you need.
Program.cs
The OpenIddict store has no default. A host that calls neither UseInMemoryOpenIddictStore() nor UseOpenIddictDatabase(...) fails to start outside the Development environment: the startup check stops the application and names the fix. In Development the same text is logged as a Warning and the host starts. An explicit InMemory starts in any environment but warns on every startup that the store is volatile: clients, tokens and authorizations do not survive a restart and are not shared between replicas.
Outside Development the token certificates are required too. These are the OpenIddict keys the server signs and encrypts id_token / access_token with — not the HTTPS certificate. Without Veriqa:OpenIddict:Server:SigningCertificatePath (or …SigningCertificateBase64) and the matching EncryptionCertificate… key the host fails to start; in Development development keys are generated instead. The keys are in the configuration reference.
Add only the channels you actually use. Each adapter activates only when its configuration section is enabled ("Enabled": true) — otherwise only its options and validator are registered.

3. Build the middleware pipeline

Program.cs
MapVeriqaAuthServer maps the Email channel endpoints (magic link and the one-tap email) automatically — but only when the channel is enabled in configuration (Veriqa:Channels:Email:Enabled: true). No separate call is needed.
The wwwroot/locales directory is optional: without it channel messages go out in the base language, and the startup warning Locale files directory not found … is expected. You do not have to translate anything by hand: ready-made locales (en, ru, zh, pseudo) ship in the Veriqa.Core.ChannelLocales package — Localization explains how to put them to work. Also expected with the default configuration is Initiator context geolocation is enabled but the GeoIP provider is unavailable: sign-in works, the geo fields of the initiator context simply stay empty. In Development the startup is also followed by a few dozen diagnostic info lines about the resolved configuration — that is normal.

4. Configure

Channel secrets and OIDC clients are set in configuration. Everything Veriqa owns lives under the root Veriqa key: channel sections under Veriqa:Channels, auth-server settings alongside them in the same root.
appsettings.json
Replace the placeholders before the first run. YOUR_BOT_TOKEN and the other placeholders are not values the host can start with. Telegram and MAX in UpdateMode: Webhook — the mode shown above — register their webhook at boot, so a placeholder token keeps the host from starting. WhatsApp is the quieter case: once "Enabled": true, its credentials are checked at startup for presence only — a missing or empty one does keep the host from starting, a placeholder string passes — so the host comes up clean and the unusable value surfaces only when the first confirmation is sent. Get the real credentials in Channel setup — that is also why exactly one channel is "Enabled": true above. Switch the others on when you have their credentials.
The last two sections are what the user reads when the sign-in ends. Channels:OutcomeNotice:DisplayIntent says where the outcome receipt appears: ReplacePrompt — the shipped value, stated here so the setting is visible — puts the receipt in place of the message that asked the question, and NewMessage leaves the question standing and adds a message of its own. It is an intent rather than a promise: Telegram and MAX honor it, WhatsApp always sends a new message, and Email shows no receipt at all.MessageTemplates words those receipts. The product ships its own wordings at ByType:login:BySurface:…, and a level exhausts every step of the address before the resolution drops to the level below — so a redeclaration is reached only at the same address, which is why Templates above sits that deep. {app} is a server slot: for a sign-in it is the DisplayName of the client. The last step of each ladder uses no slot, because no slot of a receipt is guaranteed. The full rules are in Configuration reference.
WhatsApp uses the official Meta Cloud API (Provider: MetaCloudApi); BusinessPhoneNumber is in E.164 format for the deep link. Wire only the channels you use — a section without "Enabled": true does not activate the channel, which is why the two sections above are shown switched off rather than removed. Full Email setup (SMTP for Pull and inbound for Push) is in Channel setup.
InboundVerification is a required key of every channel section: it declares how the authenticity of an incoming event of that channel is verified. An enabled channel without it keeps the host from starting. The values above are for the modes shown (UpdateMode: Webhook); in Polling mode the value is outbound_fetch. The table for every channel and mode is in Channel setup.
Running on localhost? Switch the channel to Polling. The Telegram section above is shown in Webhook mode, which needs a public HTTPS address: the host registers the webhook at boot, and Telegram has to be able to reach it. It cannot reach https://localhost:7300 — the address the checkpoint at the end of this page runs on. Polling needs no public URL, because Veriqa fetches the updates itself:
The two keys change together: outbound_fetch is the value that goes with Polling, and shared_secret there produces a startup Warning. WebhookBaseUrl and WebhookSecretToken are unused in this mode and can be left out; any other bot channel switches over on the same two keys. Good for reaching a working sign-in before a domain and a proxy exist, not for a deployed instance — and even then, one poller per bot (running more than one instance).
redirect_uri is matched against AllowedRedirectUris exactly — wildcards are not supported. The startup validator requires absolute URIs; use https in production (http only for a loopback address such as localhost or 127.0.0.1, in development).

5. Wire up the client application

Everything above is the issuer side. The client that signs in against it is an ordinary OIDC relying party: nothing Veriqa-specific goes into it — this is the same wiring you would write for any OpenID Connect provider. In ASP.NET Core that wiring comes from one package:
Program.cs (the client application)
An ordinary Challenge sends an unauthenticated visitor to sign-in — onto the Veriqa page with the QR code and channel buttons:
Program.cs (the client application)
The second route is where a sign-in that did not happen ends. The sign-in page has a Cancel sign-in button, on by default; a user who presses it comes back to /signin-oidc with error=access_denied. AccessDeniedPath is what turns that answer into a redirect to this route — the handler has no default for it, and without the line the refusal goes to OnRemoteFailure, that is, to the third route. The third route takes every other failed return. A sign-in that breaks on the Veriqa side comes back with error=server_error — see Troubleshooting. AccessDeniedPath covers access_denied only: for the rest the handler raises OnRemoteFailure, and with no handler for that event the return ends in an HTTP 500. The page offers to start again and does not show the error text; the handler writes it to the application log, which names the error code and explains failures on the client side, while the reason for a server_error is in the Veriqa log and in the audit trail. Three values have to agree with the configuration from step 4. Each mismatch has its own symptom, which is how you tell them apart: dotnet new web gives every project random ports, so out of the box neither Authority nor the redirect_uri points at a running process — step 10 starts both on the addresses used here.
The my-app client from step 4 is declared without a ClientSecret, which makes it a public client — and Veriqa requires PKCE (S256) from it. Hence UsePkce = true and no ClientSecret. For a confidential client the secret is set on both sides.
A runnable “issuer + client” pair lives in the repository (veriqa.app/source): samples/dotnet/inproc/login (the host with Veriqa, port 7300) and samples/dotnet/inproc/login-client (this application, port 7020). Their configuration matches out of the box — start both and walk through the sign-in. For it to reach the end, the issuer needs one enabled channel with credentials: the sample ships with every channel "Enabled": false, and with none enabled the sign-in page answers no_channels_available.
Veriqa exposes no end-session endpoint, so federated sign-out is not supported: signing out on the client clears its own cookie session but not the issuer’s.

6. How the sign-in flow works

The user starts the transaction on the desktop and confirms it in a trusted channel on their phone. Veriqa tracks the transaction and only then lets OpenIddict issue the result.

7. Endpoints

MapVeriqaAuthServer maps the standard OIDC contract and the service endpoints: The Email channel endpoints are mapped by the same MapVeriqaAuthServer call automatically — only when the channel is enabled (Veriqa:Channels:Email:Enabled: true):

8. User claims

The default mapper builds the final claim set from the completed transaction: picture arrives only when all three hold: the client requests the avatar scope (options.Scope.Add("avatar")), the scope is listed in the client’s AllowedScopes, and GetClaimsFromUserInfoEndpoint = true — the avatar is never put into the id_token, only userinfo returns it. To read it without inflating the cookie, see OIDC + Veriqa explainer. Override the mapping with your own IClaimsMapper — see Configuration & customization.

9. Channel selection via acr_values

A client can restrict sign-in to a specific channel with the standard OIDC acr_values parameter in channel:{type} form:
Multiple values (channel:telegram channel:whatsapp) let the user pick from the listed ones. Without the parameter, all enabled channels are available.

10. Checkpoint

Start both processes on the addresses the configuration expects — the issuer on the Authority port, the client on the port of its redirect_uri:
Putting the same addresses into applicationUrl of each project’s Properties/launchSettings.json works too. The client calls the issuer over https itself, so the ASP.NET Core development certificate has to exist and be trusted: create it with dotnet dev-certs https, then trust it with dotnet dev-certs https --trust. On Linux the trust command works as well and prints the SSL_CERT_DIR value to set; the There was an error trusting the HTTPS developer certificate. line it ends with is expected there and does not mean the step failed — export the value it printed, keeping your distribution’s own certificate directory in the list so that other chains (a restore from nuget.org, any outbound HTTPS call) still verify:
The second entry is the certificate directory of your distribution, and the command works out where it is instead of hardcoding it: openssl version -d prints OPENSSLDIR — the OpenSSL configuration directory, not the certificates themselves — and the CA files with the hash links OpenSSL looks up sit in its certs subdirectory, hence the /certs on the end. On Debian and Ubuntu that resolves to /usr/lib/ssl/certs; on other distributions the prefix differs, which is why it is read rather than written out. After that the certificate is trusted. Background is in the “Trust HTTPS certificate on Linux” section of Enforce HTTPS in ASP.NET Core.
1

Start the app

Start both processes as shown above and open the client at https://localhost:7020.
2

Trigger the flow

Choose “Sign in with Veriqa” and scan the QR code with a trusted channel on your phone. Email shows a QR code in Push mode; in Pull mode it shows an address field and a “Get a link” button instead.
3

Confirm on the phone

Confirm the request in Telegram / WhatsApp / other channels / Email. The desktop updates via SignalR or polling.
4

Confirm on the desktop

With Email — and with any channel when Veriqa:LoginConfirmation:Mode is OnWebPage — the sign-in page then asks once more: confirm the sign-in there.
5

Inspect the result

Check that you land authenticated and that the expected claims are present.

Veriqa running elsewhere

Everything above is the embedded mode: Veriqa is a set of packages inside your own host. The same server can instead run separately, and your application signs in against it over plain OIDC — Veriqa Cloud, a Docker container of your own, or a systemd unit / Windows service on a machine of your own. For a .NET application that changes the dependency tree, not the code: you keep the client half of step 5 and drop every Veriqa package, so no Veriqa component enters your build or your SBOM.
Veriqa Cloud is coming soon, and the cloud row below already describes the contract of its API. Until then, use self-hosted; details are in Veriqa Cloud.
The wiring is the one from step 5, with a single difference: the issuer address comes from configuration instead of code, because it is the one value that changes between the modes and between environments.
Program.cs (the client application)
appsettings.json (the client application)
A confidential client adds options.ClientSecret — from user-secrets in development and a secret store in production, never from appsettings.json. In the cloud the secret is issued by a separate explicit action on the Applications screen and shown once. Nothing else on this page becomes mode-specific: the endpoints of step 7, the claims of step 8 and channel restriction through acr_values (step 9) are issuer behavior, and the client sees them through discovery whichever mode it talks to. How the separate server is delivered is a further choice, and it changes nothing on the client side: a Docker container — self-hosted quickstart — or an archive installed as a systemd unit or a Windows service, with no Docker and no .NET runtime on the machine — Install as an OS service.

Next steps

Channel setup

Telegram, WhatsApp, other channels and Email — end to end.

Configuration reference

Every section, key and default in one place.

Configuration & customization

Sign-in page design, localization, extension points.

Preparing for production

What to check before shipping to production.

OIDC + Veriqa explained

Where Veriqa sits in a standard OIDC flow.

Architecture

The building blocks behind the connector.

Veriqa as a separate server

The same issuer outside your build — in Docker or as an OS service.