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 environment variable.
1. Install the packages
The recommended OIDC library isopenid-client (OpenID
Foundation certified, current line 6.x). It brings neither a web framework nor a session store,
and the code below needs both:
express is the framework the routes are written for; express-session is what gives them
req.session — where the PKCE code_verifier and the state are kept while the user is away.
Without either package, the first run fails on the import line with ERR_MODULE_NOT_FOUND, before
the application reaches Veriqa at all.
The example is ESM.
config.js awaits discovery at the top level of the module, which CommonJS
cannot do. Add "type": "module" to your package.json — without it node server.js fails with
Cannot use import statement outside a module.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.
Whichever you pick, the client must declare its allowed redirect URIs (matched exactly, wildcards
are not supported) and allowed scopes — including
channel if the application needs the channel
claims (step 7).
The redirect_uri is compared literally, so register precisely the URL your callback route serves.
For the server of step 6, started as in step 9, that URL
is http://127.0.0.1:3000/callback — register exactly that URL, and open the application at the same
host (127.0.0.1, not localhost).
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 the
application is server-side and you want to authenticate it with a secret; in the cloud the secret is
issued by a separate explicit action on the Applications screen and is shown once — store it
right away.
Keep the values in the environment, not in code. Set them in the shell you will start the
application from — the first two are what step 2 gave you, the last two are yours:
SESSION_SECRET is not issued by Veriqa — it is a random string of your own, the key
express-session 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 run fails while
config.js is still being imported — new URL(undefined) throws ERR_INVALID_URL — before any
route is served.
A .env file works just as well if you prefer one: Node.js 20.6 and newer reads it with
node --env-file=.env server.js.
Switching between deployments — cloud, Docker, OS service — comes down to changing
VERIQA_ISSUER alone.
3. Run discovery
The client is built once at process start: the library reads/.well-known/openid-configuration itself and remembers the endpoints.
The first argument is a
URL object, not a string. If discovery returns http endpoint addresses
while you access it over https, the client is not at fault: the self-hosted server sits behind a
proxy with no trusted headers — see
step 6 of the self-hosted quickstart.4. Start the sign-in
PKCE is mandatory andstate protects against CSRF. Both values must be stored in the session
until the user comes back: they are required in the callback.
login.js
redirect_uri
with a code.
5. Handle the return
callback.js
authorizationCodeGrant exchanges the code for tokens and validates the response itself; state is
checked through expectedState, so you do not verify it again by hand. The claims() method returns
the parsed id_token, or undefined if the server issued none.
The catch 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 /callback with error=access_denied and no code. authorizationCodeGrant
answers an error response by throwing AuthorizationResponseError — without the branch the return
ends in an unhandled exception instead of a page of your application.
/ and /signin-cancelled are where the callback finally lands the browser — the routes
step 6 adds.
6. Wire it into a server
The three modules above are handlers, not an application: something has to give themreq.session,
put them on routes and listen on a port. This is that file, and it is the one you start.
server.js
/callback is what
APP_REDIRECT_URI points at and what you registered in
step 2, and / shows what the sign-in produced — with
nothing in the session yet it sends you to /login, and after a completed sign-in it answers with
the stored claims as JSON, so the outcome is visible without a template.
7. 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.
sub: the library checks it against the response and will not let
the subject be swapped.
8. 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:
channel:telegram channel:whatsapp) let the user pick from the
listed ones. Without the parameter every channel enabled on the server is available.
9. Checkpoint
Start the application from the directory holdingserver.js, in the shell where the variables of
step 2 are exported:
http://127.0.0.1:3000 — the address APP_REDIRECT_URI 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:3000/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
/callback, which redirects it
to /.4
Check the claims
/ answers with the session as JSON: sub is filled in, and channel_type carries the channel
that confirmed the sign-in — it is absent 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.