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):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.
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 OIDCacr_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:
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 holdingapp.py, in the shell where the variables of
step 3 are exported:
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.