Skip to main content
This is a checklist of the things teams most often miss before going live. It is not a full deployment guide — it focuses on the easy-to-skip items. Checklist state is stored locally in your browser; use the browser’s print-to-PDF if you want a copy.
What the product collects, stores and keeps — the inventory, the collection defaults, the retention periods and what leaves your perimeter — is on one page of its own: Security and data handling. This checklist is about the decisions you make; that page is about the facts you make them on.

Issuer and certificates

  • Stable HTTPS issuer, current JWKS, a planned key replacement procedure.
The issuer must be a stable HTTPS address — it lands in the iss claim and in discovery. Clients must trust keys via discovery / JWKS, never pinned static keys: then replacing a key needs no change on their side. Outside Development the signing and encryption certificates are required in both delivery modes, embedded and self-hosted — without them the host does not start (keys: configuration reference).
Zero-downtime rotation is not supported. The configuration accepts one signing certificate and one encryption certificate, so JWKS carries a single signing key — old and new keys never overlap. Replacing the certificate invalidates live tokens signed with the previous key; the same warning appears in the self-hosted quickstart. Plan key replacement as an operation that invalidates sessions, not as a background task.

Client secrets and storage

  • client_secret and channel tokens — out of the repo, via the environment, encrypted and rotated.
Secrets (client client_secret, bot tokens, channel API keys) are passed only through environment variables / a secret store — not config files, not git. Encrypt at rest and rotate.

OIDC PKCE and state

  • PKCE (S256) required for public clients; validate state and nonce.
PKCE with the S256 method is mandatory for public clients. state guards against CSRF on the redirect, nonce binds the id_token to the session. Veriqa additionally checks a one-time browser nonce in the callback — do not disable these checks.

Redirect URIs

  • Exact-match allowlist; no wildcards in production; https only (except loopback in dev).
The redirect URI is matched exactly. No wildcards in production; http only for loopback addresses (localhost, 127.0.0.1, [::1]) during development.

Scopes and claims

  • Minimal scope set; openid required; do not request unnecessary PII.
Request the minimum: openid is required, the rest as needed. Do not request email unless needed; do not store PII beyond necessity. /userinfo returns only the claims permitted by scopes and configuration.

Rate limiting and anti-abuse

  • Core limits sized for the traffic of each route; a per-IP limiter on the reverse proxy / WAF.
The core throttles every public surface with one shared bucket per route — authorize, token, the callback, polling, webhooks, SignalR, the Email start page, the confirmation pages (defaults in configuration). On top of that it counts sign-in attempts per user (channel identity), so a confirmation cannot be brute-forced, and it gives each relying party its own budget on the server-to-server confirmation entry point. Size the shared buckets for the total traffic of the route: they protect availability, not one visitor from another. What the core does not do is key any limit by client IP or block addresses. A per-IP limit catches the one thing the other two keys cannot see — a horizontal spray, many accounts tried from one address, each staying under its own per-user limit — and that layer belongs at the edge you already run: limit_req on NGINX, a rate rule on your WAF, fail2ban on the 429s in the access log. The same deployment step configures ForwardedHeaders:KnownProxies (see self-hosted, step 6): the edge sees the real client address, the core only trusts what the edge forwards. An installation without an edge limiter is open to the spray, so put one in place before going live.

Refresh token rotation

  • Keep refresh-token rotation and revocation on.
Refresh-token rotation (EnableRefreshTokenRotation) and the /connect/revoke revocation endpoint (EnableRevocation) are on by default — keep them on. When an already redeemed refresh token is used again (replay), OpenIddict revokes the whole family itself: every token of that authorization.

Logging and audit

  • Never log tokens / secrets / PII; enable transaction auditing.
Tokens, secrets and personal data never reach logs. Enable the transaction audit trail — it makes sign-ins and confirmations traceable. The trail is written from transaction events — created, confirmed, completed, failed, expired — and from channel security events. Every record carries the timestamp, the actor, the event code (action), the target and the outcome (result).
Two known limitations of the trail — account for them when you plan reporting and incident analysis.Token issuance is not audited. Neither the initial issuance via authorization code nor refresh-token rotation creates an audit record: the trail is written from transaction events, and token issuance does not travel through them and is not tied to a transaction — even less so rotation, which happens outside the transaction lifecycle altogether. Do not build proof of token delivery on the audit trail: issuance and rotation are controlled by token revocation and family revocation on replay (see Refresh token rotation).The outcome of a channel security record is uninformative. For records produced by channel security events the outcome field is always the same — failure — for any channel code. A failure in such a row does not mean something broke: the meaning is carried by the code in the action field, and the codes are listed in the documentation of the specific channel. Filter and analyze these records by action, not by outcome.
Parameters of the confirmed operation — opt-in. By default the records of a confirmation transaction do not say what exactly was confirmed: the declared action kind and its slot values are business data of your application and may contain personal data. If the trail has to answer “which operation, with which parameters, was confirmed”, turn it on deliberately:
Program.cs
Once on, the parameters are copied into the record itself rather than referenced through the transaction: a completed transaction is swept within minutes, while the record lives for the whole retention period. Every audit record of such a transaction gets them — not the confirmation alone, but the creation, the completion, the failure and the expiry too: the question “which operation was this” is asked of those as well. They never leave the process — the external event bus does not carry them.

Inbound verification level of a channel

  • InboundVerification is declared for every channel section — and declared deliberately.
Every channel section must carry InboundVerification: a deployment with an enabled shipped channel and no value does not start. The startup check walks the very set of channels the core builds its enabled list from: all shipped channels are in it, a channel of a third-party adapter plugged in with a bare AddChannel call is not — you have to check its section manually. What matters is not whether the key is there but what it says: the value must match the mode the channel actually runs in (the table is in Channel setup). A mismatch here is silent — a channel in webhook mode declaring outbound_fetch starts without any warning. none is a deliberate choice, never a default: it says “the authenticity of an incoming event is not verified”, i.e. the confirmation endpoint of that channel accepts a request from anyone. Set it only if you have genuinely accepted that risk, and record the decision where your other risk decisions live.

Channel availability

  • Define behavior when a channel is unavailable.
Decide up front what happens when a channel is down: offer an alternative channel or show a clear error — never leave the user stuck in an unfinished transaction.

Localization and accessibility

  • WCAG 2.2 AA; locale files for your audience’s languages.
Sign-in and confirmation pages are designed to meet WCAG 2.2 AA. The standalone host built from build/docker/Dockerfile ships English, Russian and Chinese translations; in the embedded mode the locale catalog is supplied by your host, and without it the page is shown in English (see Localization). No formal accessibility audit with a published report has been carried out — if your regulations require one, plan it on your side.

Versioning and migrations

  • SemVer SDK; relational stores via migrations, not auto-create.
Follow SemVer and plan minor / major upgrades. Initialize relational stores with migrations (schema history), not auto-create.

Operational essentials

  • Health probe, encrypted DB connection, secret storage, external JS on the sign-in page off.
A host health probe, an encrypted database connection, secrets in a secret store. External JS on the sign-in page is disabled by default — enable it only deliberately (see customization).

Next steps

OIDC + Veriqa explained

Understand exactly where Veriqa sits in the OIDC flow.