Skip to main content
For non-.NET stacks Veriqa is an ordinary OpenID Connect provider. There is no Veriqa package for Python, and none is needed: you plug in a standard OIDC client, and Veriqa handles all the specifics (sign-in page, QR code, confirmation in the messaging app) on its own side.
One codebase, any deployment. The application never depends on where Veriqa runs — Veriqa Cloud, a Docker container of your own, or a systemd unit / Windows service on a machine of your own. The deployments differ only in the issuer address and in where the client is registered; the code below is identical for all of them, and switching comes down to a single server_metadata_url.

1. Install the library

The recommended library is Authlib (current line 1.8.x):
Authlib pulls in neither the web framework nor an HTTP client. flask is the framework the example below is built on; requests is what Authlib’s Flask and Django integrations are built on — the Starlette and FastAPI ones use httpx instead, so install the one your framework needs. Without either package, the very first run fails on the import block at the top of app.py with ModuleNotFoundError, before the application reaches Veriqa at all. Python 3.10 or newer is required — that is the minimum declared by the package. Ready-made integrations exist for Flask, Django, Starlette and FastAPI; Flask is shown below, the others use the same OAuth registry and the same calls.

2. Register a client on the Veriqa side

Veriqa Cloud is coming soon, and the cloud rows already describe its API contract. Until then, use self-hosted; details are in Veriqa Cloud.
Self-hosted comes in two delivery options, and this page is the same for both. The server runs in a Docker container — self-hosted quickstart — or is installed from an archive as a systemd unit or a Windows service, with no Docker and no .NET runtime on the machine — Install as an OS service. The choice decides how the server is delivered, not the client registration, the issuer address or any of the code below. A secret is not mandatory. The minimal working set is issuer, client_id, redirect_uri: a public client with PKCE, and that is the cloud default. A confidential client is only needed when you want to authenticate a server-side application with a secret; in the cloud the secret is issued by a separate explicit action on the Applications screen and is shown once. The redirect_uri is matched exactly — wildcards are not supported. Register precisely the URL your callback route serves. For the Flask example below, started as in step 7, that URL is http://127.0.0.1:5000/authorize — register exactly that URL, and open the application at the same host (127.0.0.1, not localhost): the callback URL is built by url_for(..., _external=True) from the host of the incoming request, and the comparison on the Veriqa side is literal.

3. Register the provider

server_metadata_url turns on discovery: Authlib reads /.well-known/openid-configuration itself and finds the remaining endpoints. The code takes its values from the environment, so set them in the shell you will start the application from. The first three are what step 2 gave you; the fourth is yours:
FLASK_SECRET_KEY is not issued by Veriqa — it is a random string of your own, the key Flask signs the session cookie with. Keep it out of version control and use a separate value in every environment. The public variant needs no VERIQA_CLIENT_SECRET: a public client has no secret at all. If a variable your variant reads is missing, the very first run fails with KeyError while app.py is still being imported, before any route is served. Each variant below is the complete top of app.py: the Flask application, the session secret Authlib needs, and the provider registration. The routes of step 4 go into the same file, below it.
PKCE in Authlib does not turn itself on — unlike the authentication method. The client authentication method is derived automatically: with a client_secret it is client_secret_basic, without one it is none, so a public client is simply the absence of a secret. PKCE, however, is an independent switch: with no code_challenge_method in client_kwargs a public client will run without PKCE. Set it explicitly.
Switching between deployments — cloud, Docker, OS service — comes down to changing VERIQA_ISSUER alone.

4. Define the sign-in routes

app.py
authorize_access_token() exchanges the code for tokens and parses the id_token itself — the parsed claims are in token["userinfo"], with no separate call to the userinfo endpoint. The PKCE code_verifier is generated and stored by the library, so you never pass it by hand. The except branch handles a sign-in that did not complete. The sign-in page has a Cancel sign-in button, on by default; a user who clicks it comes back to /authorize with error=access_denied and no code. authorize_access_token() answers an error response by raising OAuthError — without the branch the return ends in an HTTP 500 instead of a page of your application. Between /login and /authorize Veriqa does its work: the user sees the sign-in page with a QR code and the channel list and confirms the sign-in in a messaging app on their phone. / is where the callback finally lands the browser: with nothing in the session yet it sends you to /login, and after a completed sign-in it shows what was stored — a dict returned from a Flask view is serialized to JSON, so the outcome is visible without a template.

5. Read the claims

Veriqa adds the base set of claims to the tokens itself: Channel claims are issued only with the channel scope: Without the requested channel scope these two claims end up in no token — and therefore do not appear in the userinfo response either, which mirrors the access token. An application uses exactly these claims to tell linked channels apart, so the scope is mandatory for account linking.

6. Restrict sign-in to a specific channel

The standard OIDC acr_values parameter in the channel:{type} form narrows the choice on the sign-in page. Extra keyword arguments to authorize_redirect go into the authorize request as-is:
Several space-separated values (channel:telegram channel:whatsapp) let the user pick from the listed ones. Without the parameter every channel enabled on the server is available.

7. Checkpoint

Start the application from the directory holding app.py, in the shell where the variables of step 3 are exported:
Flask serves it on http://127.0.0.1:5000 — that is the address the redirect_uri registered in step 2 is built from.
1

Check discovery

curl $VERIQA_ISSUER/.well-known/openid-configuration returns metadata with https URLs.
2

Start a sign-in

Open http://127.0.0.1:5000/login — the browser should land on the Veriqa page with the QR code and the channel list.
3

Confirm on the phone

Confirm the request in a trusted channel; the browser returns to your /authorize route, which redirects it to /.
4

Check the claims

/ shows the session as JSON: sub is filled in, and channel_type carries the channel that confirmed the sign-in — it stays null if the channel scope was not requested.

Next steps

Integration scenarios

Channel linking, re-confirmation and action confirmation — at the HTTP level.

Self-hosted quickstart

Deploy your own issuer in Docker.

Install as an OS service

The same issuer without Docker — systemd or a Windows service, from an archive.

OIDC + Veriqa explained

Where Veriqa fits into the standard flow.

Preparing for production

What to check before shipping.