Skip to content

ADR-020: 1.0 UI Surfaces and Acceptance Criteria

This is a rendered copy of docs/adr/020-ui-surfaces-and-acceptance-criteria.md, read in place from the repository — see therecords index for how to read it. If this page and the source file ever disagree, the source file is correct.

ADR-020: 1.0 UI Surfaces and Acceptance Criteria

Status: Accepted 2026-10-11 — records the maintainer’s decisions of 2026-10-09 and 2026-10-10, together with the other 1.0 records. Amended 2026-10-19: the component foundation (§6).
Date: 2026-10-10
Authors: QAuth Team

Nothing below is built. This record answers the open question of ADR-018: which screens 1.0 ships. It names the UI surfaces, records seven design choices, lists what each surface contains and sets the bar a screen must meet before it counts as done. ADR-019 Decision 5 already fixes how the UIs are deployed; this record does not restate it.

Some decisions here change ADR-018’s scope or ADR-019’s topology: the device authorization grant, account selection, event delivery, the plugin API and the paired sandbox realm. Those records carry the changes. This one names them so the UI scope reads whole.

Amended 2026-10-19: an Authority Tree’s identifier is tree_id, not sid (ADR-014’s amendment of that date). The claims a mapper never writes and the search box of UX criterion 3 name tree_id beside the browser session’s sid.

Context

What exists today (checked 2026-10-10)

  • The auth server renders five HTML pages itself: /ui/login, /ui/consent, /ui/resume, /ui/wallet-login and /ui/wallet-link.
  • The developer portal (apps/developer-portal) is built on TanStack Start, React and Tailwind. It has sign-in, sign-up, verification, a dashboard, client management with API keys, and a consents page.
  • libs/ui holds five primitives: Button, Card, Input, Label and FormField.
  • There is no admin console and no account console. Most realm policy lives in environment variables, as if there were one realm.

The bar

The 1.0 goal is at least the breadth of an established open-source identity server, with good UX. Keycloak is the reference for breadth: its admin console, account console, login theme pages and email templates. The reference release is Keycloak 26.8.0 of 1 October 2026 (release notes). Its 26.7 and 26.8 releases also moved into QAuth’s area: an experimental OID4VP verifier, the receiving side of ID-JAG, and token-exchange delegation for agents in preview (26.7.0). QAuth’s own surfaces, such as the Authority Tree, OpenID Federation and PQC keys, have no Keycloak counterpart.

The maintainer went through the comparison in two passes. The seven forks (A to G below) were answered on 2026-10-09. Every remaining proposal was reviewed one by one on 2026-10-10. The full table is the Appendix.

Decision

1. Five surfaces

No surface holds a private key. Each is a separate, replaceable client of public APIs, as ADR-019 Decision 5 requires.

SurfaceAPISessionRuns
Ceremony appInteraction API (/interaction/v1)None of its own; the realm’s __Host- session cookie, same originOn the realm host under /ui/*, deployed separately
Account consoleaccount APIBFF, tokens server-sideAny host
Admin consoleadmin API (admin:read, admin:write, admin:security)BFF, a client of the operator realm, DPoP-bound tokens, passkey step-upAny host
Developer portaldeveloper APIBFF, signs in through /oauth/authorize (ADR-017 F0)Any host
EmailsRendered by the auth server——

Placement rules that follow from earlier decisions:

  • The ceremony app is the reference implementation. An operator may replace it with any app built on the Interaction API. A replacement shares the realm origin, so it joins the realm’s trusted computing base, as the reference app is (ADR-019 Decision 5).
  • The CIBA approval page of ADR-014 §14 is a ceremony-app page, because approval happens only on QAuth’s origin.
  • “The portal” in ADR-014 means the account console. An agent’s owner is a human account, and the owner’s actions belong to the account API. ADR-019 Decision 5’s “Authority Tree dashboard” is a section of the account console.
  • The consents page moves from the developer portal to the account console. Consents belong to the user, not to a client’s developer.
  • The OpenID Federation operator screens are a section of the admin console, not a separate app. The section opens when the console connects to a federation-operator deployment’s admin API. That deployment’s signer role signs; the console never sees the trust-anchor key.

2. Seven forks (decided 2026-10-09)

A. Where settings live. Settings are split.

  • Deployment-wide switches stay in environment variables. Examples: WALLET_FEDERATION_ENABLED, AUTHORITY_TREE_ENABLED, the signing mode, HYBRID_SIGNING_ENABLED and experimental features.
  • Realm policy lives in Postgres on the realm row. The admin console and the admin API change it. Examples: sign-up, the verified-account requirement (REQUIRE_VERIFIED_ACCOUNT), password and MFA policy, session and token lifetimes, branding, identity providers, the email sender, brute-force thresholds, the self-registration policy and event retention.
  • Environment variables only seed a new realm’s defaults.

B. Sign-in flows. 1.0 has a policy screen, not a flow editor.

  • Per realm and per client: the allowed methods (password, passkey, wallet, upstream), MFA (off, optional, required, required for admins), passkey first, and step-up rules.
  • The engine reads this policy on every round and enforces it after the first factor on every transport. An account without a usable second factor gets the set-up step, never a code (ADR-017 Decision 4).
  • The step grammar of ADR-017 Decision 3 stays fixed, so the stability promise stays narrow.
  • New methods arrive through the AuthMethod contract.
  • A Keycloak-style flow editor comes in 1.x.

C. Appearance. Brand settings only.

  • Logo, colours, a font from a safe list, per-locale strings, links and dark mode.
  • No uploaded templates, custom CSS or custom JavaScript. Each would weaken the strict CSP and open a surface outside the promise. Email templates are fixed too; their logo, colours and strings come from the brand settings.
  • Full control means replacing the ceremony app, the second and third tiers of ADR-019 Decision 5.

D. Roles, groups and organizations.

  • Realm roles and client roles. No composite roles.
  • Flat groups: members, attributes, roles and default groups. Groups carry roles. Both reach tokens as claims, so assigning a role or a group is a security operation (ADR-019 Decision 7).
  • Group hierarchy and organizations (domains, members, invitations, per-organization identity providers) come in 1.x. Realms already isolate tenants.

E. User profile and claim mapping. Two separate systems, as in Keycloak.

  • A user profile schema: fields, types, validators, required fields, and who may edit each field (the user or an admin). It drives sign-up, profile completion, the account console and the admin console’s user screens.
  • A mapper system with Keycloak’s breadth of types: attribute to claim, role or group to claim, audience, constant values. Identity-provider mappers use the same language in the other direction, from an upstream claim to an attribute.
  • No script mappers. A script mapper runs administrator-written code inside the process that signs tokens. Custom logic is written as a plugin (§4).
  • No mapper output is ever used to match accounts.
  • A mapper never writes a claim the server sets itself, among them iss, sub, exp, iat, nbf, jti, sid, tree_id, act, cnf, client_id, scope, amr, acr, auth_time, nonce, azp, at_hash, c_hash, email and email_verified. An audience mapper only adds a resource registered in the realm to aud.
  • Creating or changing a mapper changes what tokens assert, so it is a security operation (ADR-019 Decision 7).

F. Languages. 1.0 ships in English only.

  • The translation infrastructure and per-locale string overrides ship in 1.0. The community adds languages.
  • The language picker appears once a deployment has more than one language.

G. Configuration as code. In 1.0.

  • A declarative realm file that leaves out secret values.
  • Export and import through the admin API and the CLI. A dry run shows the diff before anything is applied.
  • Each change in the diff is authorized as the single operation it equals, and the strictest change gates the import (ADR-019 Decision 11).
  • The admin console has “Export” and “Review diff and import”.
  • Today’s seed manifest becomes a subset of this file.
  • The file format is inside the 1.0 stability promise.

3. Per-surface decisions (reviewed 2026-10-10)

Ceremony app

  • Sign-in asks for the identifier first. An unknown account gets the same next step as a known one, so the page does not reveal whether an account exists.
  • After the first factor, “try another way” lists the user’s other methods, and the strongest is the default. Before it, the page offers only what the uniform first step offers every account (a recovery code included; ADR-017 Decision 4).
  • prompt=select_account is in 1.0: several accounts can stay signed in in one browser. This widens the session model, which ADR-019 records.
  • The OAuth 2.0 device authorization grant (RFC 8628) is in 1.0, with a code-entry page. ADR-018 records the scope change. The page shows the client’s verified domain (ADR-019 Decision 6). That guards against an impersonated client; on its own it does not stop device-code phishing, where a real client’s code is passed to the victim (RFC 8628 §5.4). Against that:
    • the page always asks for an explicit confirmation, also when the user arrives through verification_uri_complete;
    • it warns that nobody else should have given the user the code, and asks the user to continue only for a device in front of them;
    • it shows where the request came from: the requesting device’s approximate location, network, user agent and start time;
    • a user code lives 5 minutes by default and is single use. Code entry is rate-limited per code, per session and per IP address;
    • the consent screen is always shown, even for a client that may skip consent elsewhere.
  • A forced password update comes when an admin asks for it or when the password appears on a breach list. The list’s source, a local list or a k-anonymity lookup, is a realm setting. A lookup goes through the egress client of ADR-019 Decision 8. There is no periodic expiry.
  • First sign-in through an upstream never merges accounts automatically. Email is only an attribute by default. An account is linked only when the user signs in to the existing account. The one exception is an enterprise ID-JAG subject, which an operator links (ADR-011 gate 14, ADR-019 Decision 7).
  • Error, info and expired pages share one design. Each says what happened and what to do, and shows a trace id. None reveals detail an attacker could use.
  • Sign-in with an X.509 client certificate and versioned terms acceptance come in 1.x. In 1.0 the sign-up page only links to the terms.

Account console

  • The user changes identifiers and the contact address after fresh authentication. A new address is verified by code, and the old one is notified. A realm can block username changes.
  • A security log shows the last 90 days: sign-ins, new devices and security changes. A “this wasn’t me” button ends that session and asks for new credentials.
  • A user can delete the account immediately, after fresh authentication and a typed confirmation. A realm can turn self-deletion off; then only an admin deletes.
  • “Download my data” comes in 1.x. In 1.0 the admin API has a user export endpoint.

Admin console

  • Every operation that ADR-019 Decision 7 defines as a security operation needs admin:security and a fresh passkey, wherever it sits in the console, and follows the two-admin rule (§4). Examples on the screens below: the mail transport and sender, upstream providers, every mapper, role and group assignment, the scope catalog, a client’s redirect URIs and first-party trust, admin roles, a realm-file import that holds any of these and every federation-operator action. Criterion 1 (§5) applies to each.
  • Environment posture (ADR-008) is edited in the console. It lives in the database: the realm ceiling max_environment_laxity and each client’s environment. Tightening is free. Loosening needs admin:security, a fresh passkey and, where the realm requires it, a second admin. Each change lands in the admin events with its diff.
  • Security headers are fixed. The console shows them read-only.
  • Brute-force thresholds are set per realm, with an increasing delay. There is no permanent lockout, because that would let an attacker lock anyone out. A floor keeps the thresholds safe. The disable after 100 consecutive failures in ADR-017 Decision 7 is not a permanent lockout: it ends at the owner’s next self-service reset or recovery code, and it is armed only while no third party can block that path.
  • There is no client-policy engine. PAR, JAR, DPoP and algorithm requirements are client settings.
  • There is no workflow engine. Automation is built outside QAuth on the event delivery of §4.
  • A type-first wizard adds an app. It asks what is being built: a web app with a backend, a single-page app, a mobile or desktop app, an MCP server, an agent, or a machine client. The client type, authentication method, redirect form and DPoP follow. The developer portal uses the same wizard.
  • Clients get the device grant capability in 1.0. General CIBA, for any client, comes in 1.x on the same approval infrastructure. mTLS client authentication (RFC 8705) comes in 1.x.
  • A client secret rotates with an overlap: at most two secrets are active, and use of the old one is visible.
  • A client’s token lifetimes can only be shorter than the realm’s.
  • The client screen lists the client’s sessions and refresh-token families, with single and bulk revocation. Bulk revocation needs admin:security and a passkey.
  • A self-registration screen holds the dynamic-registration switch, the scope ceiling and the CIMD allow and deny lists, on the realm row.
  • A scope catalog per realm holds each scope’s per-locale consent text, a sensitivity flag and the clients it is open to. System scopes such as openid, profile and agent:* cannot be deleted and keep their meaning. There are no parameterized scopes; fine-grained rights use RAR, whose types and consent texts the console manages.
  • An admin never sets a user’s password. The admin sends a set-up or reset link, or copies a one-time, short-lived link. A sent link goes only to the account’s own contact address. Copying a link, or changing that address, is a security operation: it needs admin:security and a fresh passkey, the user is notified, and the admin events record it (ADR-019 Decision 7).
  • An admin may delete a user’s passkey or TOTP: it needs admin:security and a fresh passkey, the user is notified, and the admin events record it.
  • Required actions (renew password, set up MFA, set up a passkey, complete the profile) are assigned per user or in bulk to a group.
  • A user-event viewer filters by user, client, IP, type and time.
  • Admin events are always on and show a diff with secret values masked.
  • Retention: user events 90 days by default; admin events one year by default and never less than 90 days. Retention removes only the oldest admin events, after the event routes have offered them; the admin log stays append-only (ADR-019 Decision 7).
  • TOTP is fixed at SHA-1, six digits, 30 seconds and one step of tolerance. Other values break common authenticator apps without a real security gain.
  • An account created through an upstream counts as a verified account. A per-provider switch turns that off; QAuth then asks for its own verification. Upstream claims are stored as attributes with their source.
  • Each upstream provider has a name, an icon, an order and a “hide on login page” switch. A hidden provider is still reachable through a direct-routing parameter.
  • A server-info screen shows the version, the switches, experimental features, installed plugins and role health. Operator-realm admins see all of it; a realm admin sees only what affects that realm.
  • Console import of users comes in 1.x. In 1.0 the db:import-users CLI of ADR-017 F1d does it. Inbound SCIM provisioning comes in 1.x, together with organizations.

Developer portal

  • A CIMD validator fetches a Client ID Metadata Document, runs every rule QAuth applies (realm policy included) and explains how to fix each problem. It is in the portal and the CLI, for signed-in users only, rate-limited, with the same SSRF-safe fetch as registration.
  • A developer can register an MCP server as a resource server, define its scopes and copy its protected-resource metadata and its mcp-guard configuration. Admin approval of a new resource is a realm setting.
  • A flow test console runs a real authorization-code flow with PKCE step by step and decodes the token in the browser. It works for development and staging clients. In production it runs only in the paired sandbox realm (§4).

Federation section (admin console)

  • A trust-chain resolver shows a chain step by step: entity configuration, authority hints, each subordinate statement, the signature checks, metadata policy, trust marks and the resulting metadata. It marks the step that breaks. It is also on the identity-provider screen and in the CLI.
  • Every federation-operator action is a security operation (ADR-019 Decision 7): the entity configuration and authority hints, subordinates, metadata policy and constraints, trust marks, explicit registration and the federation keys. The resolver only reads.

Emails

  • Security notices go out for a new-device sign-in, a password change, a password or TOTP disabled after too many attempts, a passkey or TOTP added or removed, a recovery code used, recovery codes regenerated or failed 100 times, codes requested past the per-address send ceiling, a wrong second factor after a correct password, an identifier change and a new agent grant. They are on by default. A realm may turn some off, but never the ones about credential changes or the ones that carry a reset pass: the send-ceiling and password-disable notices always go out (ADR-017 Decisions 5 and 7), and so does the notice of a wrong second factor after a correct password (ADR-017 Decision 7). An account with no contact address sees them only in its security log.

4. Cross-cutting decisions

Paired sandbox realm. Each live realm can have a sandbox twin.

  • The twin has its own host, issuer and keys, and its own test users.
  • Its environment ceiling is staging, so the flow test console works there.
  • The consoles have a Sandbox/Live switch.
  • Settings move from sandbox to live through the realm-file diff of fork G. Fields that describe the sandbox’s environment, such as its ceiling, http redirect URIs, test upstreams and test webhook receivers, move only when selected one by one (ADR-019 Decision 11).
  • Upstream providers in the sandbox use test accounts.
  • A sandbox token never passes at a live resource server, because the issuer differs. A same-issuer sandbox claim was rejected: it would depend on every resource server checking it.
  • ADR-019 records the topology.

Two-admin approval. Every admin:security operation recommends a second admin’s approval. A realm chooses where it is mandatory. A deployment with a single admin sees a warning. Once any operation is mandatory, changing the mandatory set, creating an admin, granting or removing admin:security, and changing an admin’s credentials, identifier or contact address through the admin API take two admins too; one admin alone can only add to the mandatory set. An admin’s change to its own account through the account API takes one admin, and that admin cannot approve for the next 7 days. ADR-019 Decision 7 says who may approve and how an approval is bound to the operation.

Event delivery. QAuth runs no automation itself, but every event is usable for automation. 1.0 delivers events four ways:

  • signed webhooks in the Standard Webhooks format, with retries, per-type subscriptions and a delivery history in the console;
  • a cursor-based events API, so a consumer can resume where it stopped;
  • Shared Signals (SSF) and CAEP streams for security events, which the Authority Tree’s CAEP transmitter is a part of;
  • OpenTelemetry logs, for the operator’s own collector.

Event types and their payload schema are inside the 1.0 stability promise. ADR-018 records this. Webhook and SSF deliveries go through the egress client of ADR-019 Decision 8, which checks each destination when it is registered and again at every delivery.

Plugins. The core stays provider-agnostic.

  • The core ships generic OIDC upstream only.
  • Google, Microsoft and Apple presets, GitHub (which is not an OIDC provider) and My Number are plugins. They are ready with 1.0.
  • Other providers become plugins on demand.
  • The plugin API (the AuthMethod contract of ADR-003 as ADR-018 extends it) is public and experimental in 1.0. It becomes stable in 1.x. ADR-018 records the change.

5. UX acceptance criteria

A screen is not done until it meets every criterion below. Those marked CI are measured in CI, so a regression fails the build.

  1. Diff before save. A security operation, as ADR-019 Decision 7 defines it, shows the old and new values side by side before it is saved, then asks for admin:security and a fresh passkey.
  2. A reason on every setting. Each setting has a one-sentence explanation and a link to docs.qauth.dev. A value that differs from the default is marked.
  3. Command palette and token inspector. One search box finds users, clients, sessions, jti, sid, tree_id and agents. A pasted token shows its realm, its signing key, its revocation state and its place in the Authority Tree.
  4. Empty states that work. The first client, provider or agent screen shows the next step and a copyable SDK snippet.
  5. Sign-in without JavaScript (CI). The password, TOTP and email-code steps work with JavaScript off. Each form carries the step’s CSRF value from the Interaction API (ADR-019 Decision 6). Passkeys and the wallet QR code use JavaScript. Each ceremony page has a size budget.
  6. Accessibility (CI). WCAG 2.2 AA: full keyboard use, screen readers and contrast, with automated checks.
  7. Phone first. The ceremony app and the account console are designed for phones first. The admin console is desktop-first but does not break on a phone.
  8. One design system (CI). libs/ui grows into the shared token and component library of all surfaces. The brand settings change its tokens. Components are documented and pass visual regression tests.
  9. Type-first wizard. Adding an app starts from what is being built (§3).
  10. Dark mode everywhere. It follows the system setting and can be switched by hand. A realm may turn it off on its login pages.
  11. One error-page design with a trace id (§3).

6. Stack

  • All four apps use TanStack Start, React, Tailwind and libs/ui.
  • A short spike checks that the ceremony app’s forms work without JavaScript (criterion 5), and that the app runs with no inline script under the CSP the proxy sets (ADR-019 Decision 5).
  • If the spike fails, only the ceremony app moves to React Router’s framework mode. The components stay shared.
  • The developer portal is served by a small srvx entry around TanStack Start’s server handler, with no Nitro. The maintainer chose this on 2026-10-10; a separate change implements it.
  • Each flow and screen is first written as a graph, which this stack implements (ADR-021).
  • Component foundation (decided 2026-10-19). The interactive components of libs/ui (checkbox, switch, select, combobox, dialog, menu, tabs, table, the command palette) are built on React Aria Components. It wraps real <input> and <select> elements, so a control still submits when JavaScript fails. It ships an accessible data table, which most admin console screens need. It carries internationalisation and right-to-left support.
  • libs/ui has two entry points. The static entry renders native elements only and imports no component library. The interactive entry wraps React Aria Components. Apps import libs/ui, never the library itself, so the library can be replaced later.
  • The ceremony app imports the static entry only, and an Nx tag rule enforces it. Its forms must work without JavaScript (criterion 5), and native elements meet that with nothing to hide.
  • React Aria Components render the hidden form input of a checkbox, switch or select with an inline style attribute, which the proxy’s policy blocks. libs/ui hides those inputs with a stylesheet rule that keeps them focusable (sr-only), never with display: none, which would take the real input out of the focus order and the accessibility tree. The visual regression checks catch a library update that breaks the rule’s selector.
  • A checkbox shows its checked state from the real input’s :checked state, not from a data attribute that JavaScript sets, so it renders correctly with JavaScript off.
  • React Aria’s toast is not stable yet. Until it is, libs/ui uses its own Alert region.

7. Delivery

Each area’s screens ship with that area’s 0.x beta (ADR-018 §2):

  1. the core: the ceremony app, the account console, the core sections of the admin console, and the developer portal;
  2. the Authority Tree: the agent screens of the account console and the admin console, and the approval page;
  3. federation: the federation section and the federation upstream.

Wallet and PQC screens ship in the beta that carries their back end.

Consequences

Positive

  • The UI scope is written down row by row, so the betas and the audit can check it.
  • The UX bar is testable. Three of the eleven criteria are measured in CI.
  • Realm policy can differ per realm, which custom domains and the sandbox twin need.
  • A sandbox token cannot pass at a live resource server, by construction.
  • Operators can automate on events without QAuth growing a workflow engine.
  • The core stays provider-agnostic, while the common providers still ship with 1.0.

Negative

  • The scope is large. Four apps, a design system and four event channels are mostly unbuilt.
  • Several items widen the promise: the realm-file format, the event schema and the device grant.
  • Multiple accounts in one browser and the sandbox twin complicate the session and realm models.
  • Five plugins must be ready with 1.0, on an API that is still experimental.
  • English only at 1.0 leaves non-English users waiting for community translations.
  • Without a flow editor, some Keycloak migrations will not map one to one in 1.0.

Neutral

  • Keycloak is the breadth reference, not the design reference. Rows marked “No” are deliberate.
  • The ceremony app is a reference. A replacement app on the Interaction API is a supported setup, and it is part of the realm’s trusted computing base.
  • The stack choice is reversible for the ceremony app until the spike runs.

Alternatives considered

  • Base UI for the consoles. Rejected on 2026-10-19. It has no data table, and its checkbox is an element with a checkbox role that cannot be ticked with JavaScript off. Its stable toast and smaller API did not outweigh those.
  • No component library. Rejected on 2026-10-19. The project would write and maintain an accessible table, combobox, dialog and menu itself.
  • A flow editor in 1.0. Rejected for 1.0. It would put every flow shape inside the promise and reproduce Keycloak’s hardest screen. It comes in 1.x.
  • Template overrides or custom CSS. Rejected. Both weaken the CSP and create an unpromised surface. Replacing the ceremony app gives full control instead.
  • Organizations in 1.0. Rejected for 1.0. Realms already isolate tenants; organizations come in 1.x with inbound SCIM.
  • Script mappers, sandboxed or not. Rejected. Code in the token-signing process is the largest surface of all. Plugins cover custom logic.
  • A same-issuer sandbox claim. Rejected. Safety would depend on every resource server checking the claim.
  • A separate federation app. Rejected. One console gives one design, one session model and one sign-in.
  • All UIs in the first beta. Rejected. The first beta would wait for every area.
  • All settings in environment variables. Rejected. Per-realm policy is impossible that way.
  • Three end-user languages in 1.0. Rejected. Each language is a standing maintenance cost; the infrastructure ships instead.
  • ADR-003 — the CredentialProvider interface that AuthMethod extends
  • ADR-008 — environment posture
  • ADR-014 — the Authority Tree, its approval page and dashboard
  • ADR-018 — the 1.0 scope; this record answers its open question
  • ADR-019 — UI deployment, the Interaction API and the API families
  • ADR-015 and ADR-016 — Authority Tree hardening and platform bindings
  • ADR-017 — first-party login: the step grammar, hosted pages and the phases F0 to F2b
  • Keycloak 26.8.0 release notes — the parity reference

Appendix: inventory

Every row compares one Keycloak feature with QAuth. “1.0” is in 1.0; “1.x” comes later; “No” is a deliberate omission. “Decision” rows follow an earlier record or one of the forks A to G; “Row review” rows were decided one by one on 2026-10-10. “QAuth only” marks a feature with no Keycloak counterpart. Keycloak locations are admin-console paths or login-theme template names.

Ceremony app

AreaFeatureKeycloak locationQAuth today1.0BasisNote
Sign-inUsername and passwordLogin theme · login.ftl/ui/login, server-rendered HTML1.0DecisionThrough the Interaction API. A password user whose account is not yet verified gets an email-code step (ADR-017).
Sign-inIdentifier firstAuthentication · Browser flow—1.0Row reviewFor passkey-only accounts and later routing by domain. An unknown account gets the same next step.
Sign-inPasskey sign-in (conditional and modal)Login theme · webauthn-authenticate—1.0DecisionADR-017 F2b, WEBAUTHN_ENABLED. The RP ID is the realm host.
Sign-inPasskey enrolmentwebauthn-register—1.0DecisionThe RP ID locks at the first enrolment. Recovery codes are issued with the first second factor, passkey or TOTP (ADR-017 Decision 8). On a password-only account, the first one also needs a code mailed to its address (ADR-017 Decision 8).
Sign-inTOTP sign-in and set-uplogin-otp, login-config-totp—1.0DecisionADR-018, human accounts.
Sign-inSign-in with a recovery codelogin-recovery-authn-code-input—1.0DecisionADR-018.
Sign-inTry another way (method choice)select-authenticator—1.0Row reviewThe strongest method is the default; after the first factor the user can pick passkey, TOTP or a recovery code. Before it, only what the first step offers every account, which on the hosted pages includes a recovery code (ADR-017 Decision 4).
Sign-inStep-up (re-authentication)Authentication · ACR / LoAFresh browser sign-in (evaluateStepUp)1.0DecisionWith a passkey. Follows the ADR-008 posture.
Sign-inBot challengeRegistration flow · reCAPTCHA—1.0DecisionADR-017 F1b, provider-neutral.
Sign-inAccount selection (prompt=select_account)——1.0Row reviewSeveral accounts in one browser. The session model widens; ADR-019 records it.
Sign-inDevice code entry (RFC 8628)login-oauth2-device-verify-user-code—1.0Row reviewAdded to the 1.0 scope (ADR-018). The verified domain, explicit confirmation, the requesting device’s context and a passed-on-code warning (§3).
Sign-inSign-in with an X.509 client certificatelogin-x509-info—1.xRow reviewThrough the AuthMethod contract if enterprise demand appears.
Sign-up and recoverySign-upregister.ftlPortal /register → /auth/register1.0DecisionHosted sign-up page, off by default (ADR-017 F2a). /auth/register is removed in F1c.
Sign-up and recoveryprompt=create——1.0DecisionADR-017 F2a.
Sign-up and recoveryForgot passwordlogin-reset-passwordNone1.0DecisionADR-017 F2a. Always on while hosted pages are on.
Sign-up and recoveryForced password updatelogin-update-password—1.0Row reviewWhen an admin asks for it, or when the password appears on a breach list. No periodic expiry.
Sign-up and recoveryAccount verificationlogin-verify-emailPortal /verify, by link1.0DecisionADR-017 email_code step. The text says “verified account”.
Sign-up and recoveryProfile completionlogin-update-profile—1.0DecisionAsked when a required profile-schema field is missing (fork E).
Sign-up and recoveryTerms acceptanceterms.ftl—1.xRow review1.0 has only a link on the sign-up page.
ConsentOAuth consent screenlogin-oauth-grant/ui/consent1.0DecisionTyped data from the server’s own records only. The verified domain leads; an unverified client is marked so, with its redirect host.
ConsentAgent root grant: ceiling, modes, agent:request, rights (QAuth only)——1.0DecisionADR-014 §11. The root’s locations and actions from QAuth’s typed record, escaped, one per line, never summarised by the purpose; the audience, the refresh family’s expiry, the depth cap and the binding code. A root beyond its type’s registered rights or its agent’s root ceiling never reaches the screen. A resumed session’s root is bound to the new process only after this screen is approved again (ADR-014 §1). Keycloak 26.8 has a delegation:client consent (preview).
ConsentRemote approval page (CIBA, passkey) (QAuth only)——1.0DecisionADR-014 §14. Only on QAuth’s origin. Binding code, typed delta, approve for a while, mute, always block.
Upstream and walletUpstream IdP buttonsLogin theme · social providers—1.0DecisionUpstream OIDC, including My Number.
Upstream and walletFirst upstream sign-in: review profile, link accountFirst broker login flowWallet linking exists1.0Row reviewNo automatic merge. Email is only an attribute by default; linking happens when the user signs in to the existing account.
Upstream and walletWallet sign-in (OID4VP)26.8: OID4VP verifier, experimental/ui/wallet-login1.0DecisionHAIP 1.0 is stable. The DC API comes in 1.1. Redesigned.
Upstream and walletWallet linking (QAuth only)—/ui/wallet-link1.0DecisionRedesigned.
Upstream and walletUpstream OP choice through federation (QAuth only)——1.0DecisionOpenID Federation RP side. A list of OPs with validated trust chains.
Upstream and walletOrganization selectionselect-organization—1.xDecisionOrganizations come in 1.x (fork D).
Logout and appearanceLogout confirmation (RP-initiated logout)logout-confirm—1.0DecisionADR-017 F0. No front-channel logout.
Logout and appearanceError, info and expired pageserror.ftl, info.ftlScattered1.0Row reviewOne design: what happened, what to do, a trace id.
Logout and appearanceLanguage choice and dark modeRealm settings · Themes—1.0Row reviewDark mode everywhere; a realm may disable it on its login pages. The language picker appears once more than one language is installed.
Logout and appearanceThemeThemes · login—1.0DecisionThrough brand settings. Full control means replacing the ceremony app (fork C).

Account console

AreaFeatureKeycloak locationQAuth today1.0BasisNote
ProfilePersonal infoAccount console · Personal info—1.0DecisionFields come from the profile schema (fork E).
ProfileIdentifiers and contact addressUpdate email—1.0Row reviewThe user changes them with fresh authentication. A new address is verified by code, and the old one is notified. A realm can block username changes.
ProfileLanguageAccount console · language—1.0Decision1.0 is English only. The picker appears once more than one language is installed (fork F).
SecurityChange passwordSigning in · Password—1.0Decision—
SecurityPasskeys: add, rename, deleteSigning in · Passkeys—1.0DecisionDeleting needs fresh authentication.
SecuritySet up and remove TOTPSigning in · Authenticator app—1.0DecisionEnrolment is confirmed by a first code; removal needs a fresh authentication with the strongest bound factor (ADR-017 Decision 8). On a password-only account, the first one also needs a code mailed to its address (ADR-017 Decision 8).
SecurityRegenerate recovery codesSigning in · Recovery codes—1.0DecisionNeeds a fresh authentication with the strongest bound factor; the old set stops working (ADR-017 Decision 8).
SecurityDevice activity and sign out everywhereDevice activity—1.0DecisionADR-017 F2a question 9. Sessions live in Postgres, with Redis as a cache.
SecuritySecurity log: recent sign-ins, new devices, changes—audit_logs (no UI)1.0Row reviewThe last 90 days, with a “this wasn’t me” button.
SecurityDelete accountDelete account (required action)—1.0Row reviewImmediate, with fresh authentication. A realm can turn it off.
SecurityDownload my data——1.xRow review1.0 has a user export endpoint in the admin API.
Links and applicationsLinked upstream accountsLinked accounts—1.0Decision—
Links and applicationsWallets (QAuth only)—/ui/wallet-link1.0Decision—
Links and applicationsApplications and consents, revokeApplicationsPortal /consents1.0DecisionMoves from the developer portal to the account console.
Links and applicationsGroups and organizationsGroups, Organizations—1.0DecisionGroups are visible. Organizations come in 1.x (fork D).
Links and applicationsResource sharing (UMA)Resources—NoDecisionUMA is not planned (ADR-018).
My agentsAgents: list, profile, disable, revoke (QAuth only)——1.0DecisionADR-014 §13.
My agentsLive tree: sessions, nodes, actions (QAuth only)——1.0DecisionADR-014 P3.
My agentsApprovals: pending, open windows, mutes, always-blocks (QAuth only)——1.0DecisionADR-014 §14.
My agentsRaise the root ceiling (QAuth only)——1.0DecisionADR-014 decision 17, with a passkey.
My agentsNotification channels: web push, webhook (QAuth only)——1.0DecisionADR-014 decision 16. No email.
My agentsPlatform bindings (for example GitHub) (QAuth only)——1.0DecisionADR-016. The upstream identity is assigned to the owner in configuration, or its key is uploaded by the owner with a passkey (ADR-014 §13). No route returns an uploaded key. A binding vends nothing once its upstream identity is no longer assigned to the agent’s owner (a reassignment or a transfer), and an upload of a configured identity’s key is refused (ADR-014 §13).
My agentsAgent transfer offers (QAuth only)——1.0DecisionADR-015.

Admin console

AreaFeatureKeycloak locationQAuth today1.0BasisNote
RealmCreate, list and switch realmsRealm selector · Create realmrealms table, no UI1.0DecisionOperator-realm admins. Each realm has its own host.
RealmHost and custom domainRealm settings · Frontend URL—1.0DecisionACME and DNS verification come later, with the hosted service.
RealmEnvironment postureRealm settings · Require SSLDB: realms.max_environment_laxity, oauth_clients.environment1.0Row reviewTightening is free. Loosening needs admin:security, a passkey and, if the realm requires it, a second admin.
RealmPaired sandbox realm and Sandbox/Live switch (QAuth only)——1.0Row reviewOwn host, issuer and keys; staging ceiling. Settings move to live through the realm-file diff.
RealmLogin settings: sign-up, verified-account requirementRealm settings · Loginenv (REQUIRE_VERIFIED_ACCOUNT)1.0DecisionStored on the realm row, changed from the console and the admin API. env only seeds new realms (fork A).
RealmEmail: SMTP and senderRealm settings · Emailenv1.0DecisionStored on the realm row, changed from the console and the admin API. env only seeds new realms (fork A). A change needs admin:security and a passkey.
RealmSession lifetimesRealm settings · Sessionsenv1.0DecisionStored on the realm row, changed from the console and the admin API. env only seeds new realms (fork A).
RealmToken lifetimesRealm settings · Tokensenv1.0DecisionStored on the realm row, changed from the console and the admin API. env only seeds new realms (fork A).
RealmTurning refresh-token rotation offTokens · Revoke refresh tokenAlways onNoDecisionOAuth 2.1. Not a setting.
RealmThemes and brandingRealm settings · Themes—1.0DecisionLogo, colours, a safe font list, per-locale strings, links, dark mode. No templates, custom CSS or JS (fork C).
RealmLocalization and stringsRealm settings · Localization—1.0DecisionTranslation infrastructure and per-locale string overrides. 1.0 ships English only; the community adds languages (forks F and C).
RealmSecurity headers (CSP, HSTS)Security defenses · HeadersFixed (security-headers plugin)NoRow reviewFixed; the console shows them read-only.
RealmBrute-force protectionSecurity defenses · Brute forceADR-017 disable ceiling1.0Row reviewThresholds per realm, increasing delay. No permanent lockout: the ADR-017 disable at 100 ends at the owner’s self-service reset.
RealmClient policies and profiles (FAPI)Realm settings · Client policies—NoRow reviewNo policy engine. PAR, JAR and DPoP requirements are client settings.
RealmUser profile schemaRealm settings · User profileuser_attributes table1.0DecisionFields, validators, who may edit (fork E).
RealmExport and importRealm settings · Partial export/importSeed manifest, db:import-users1.0DecisionA declarative realm file without secret values. Admin API and CLI, with a dry-run diff. The seed manifest is its subset (fork G).
RealmWorkflows (automation)Workflows (GA in 26.6)—NoRow reviewNot QAuth’s job. Every event is delivered outward; automation is built outside.
KeysKeys: active, next, retired; JWKS previewRealm settings · Keysenv, key file1.0DecisionPer realm and purpose, envelope-encrypted, 90 days. A manual rotation needs admin:security and a passkey.
KeysPQC mode and hybrid-signing state (QAuth only)—env1.0DecisionML-DSA-65 and the hybrid, off by default.
ClientsClient list, search, createClientsPortal shows only the developer’s own clients1.0Decision—
ClientsType-first “what are you building?” wizardCreate client—1.0Row reviewThe same wizard in the admin console and the developer portal.
ClientsRedirect and post-logout URIs, web originsClient · Access settingsPortal: redirect URIs1.0Decision—
ClientsFlows: code + PKCE, client_credentials, token exchange, ID-JAGClient · Capability config—1.0DecisionNo ROPC or implicit. PKCE S256 cannot be turned off, except ADR-017’s single exception.
ClientsGeneral CIBACapability config—1.xRow review1.x, on the same approval infrastructure. The device grant is in 1.0 (separate row).
ClientsDevice grant capability (RFC 8628)Capability config · OAuth 2.0 Device Authorization Grant—1.0Row reviewShips with the code-entry page in the ceremony app (ADR-018).
ClientsClient secret and private_key_jwt (JWKS, jwks_uri)Client · Credentials, KeysPortal: client secret1.0DecisionAgent types use private_key_jwt only.
ClientsClient secret rotation with two secretsClient secret rotation (GA in 26.8)—1.0Row reviewAt most two active secrets; use of the old one is visible.
ClientsmTLS client authentication (RFC 8705)Credentials · X509—1.xRow review—
ClientsConsent required, first-party trustClient · Login settingscanSkipConsent1.0DecisionADR-017 first_party_policy; writing it is an admin:security operation (ADR-019 Decision 7).
ClientsDisplay name, logo, verified domainClient · Display on screen—1.0DecisionInteraction API display rule.
ClientsBack-channel logout URLClient · Logout settings—1.0DecisionBack-Channel Logout 1.0. No front channel.
ClientsPer-client token lifetimesClient · Advanced—1.0Row reviewOnly shorter than the realm value.
ClientsPAR, JAR and DPoP requirements; ID token algorithmClient · Advanced—1.0DecisionPAR and JAR come with federation.
ClientsAllowed scopesClient · Client scopesvalidateScopes1.0Decision—
ClientsClient roles, service-account rolesClient · Roles, Service account roles—1.0DecisionFork D.
ClientsAuthorization tab (UMA resource server)Client · Authorization—NoDecisionRAR and AuthZEN instead.
ClientsThe client’s sessions and refresh familiesClient · Sessions—1.0Row reviewList, single and bulk revoke. Bulk revoke needs admin:security and a passkey.
ClientsDCR policies, CIMD allow and deny listsClient registration policiesenv (CIMD_*)1.0Row reviewOn the realm row, changed from the console. MCP 2026-07-28: CIMD is recommended and DCR is deprecated.
ClientsAgent types: is_agent, max_agent_mode, spawn_allowlist, registered_rights (QAuth only)—Seed manifest1.0DecisionADR-014 §1, §5. Only the grants a tree uses; with AUTHORITY_TREE_ENABLED on, no client_credentials, JWT-bearer or device grant.
Scopes and resourcesScope catalog, per-locale consent textClient scopesFixed list1.0Row reviewPer realm. System scopes cannot be deleted and keep their meaning.
Scopes and resourcesProtocol mappersClient scopes · Mappers—1.0DecisionA mapper system separate from the profile schema, with Keycloak’s breadth of types. No script mappers, because they run arbitrary code in the authorization server (fork E).
Scopes and resourcesParameterized scopes26.8 preview—NoRow reviewRAR (authorization_details) instead.
Scopes and resourcesResource servers: audience, RFC 9728, Bearer leaf, approval-only (QAuth only)—env1.0DecisionADR-014 and ADR-016.
Scopes and resourcesSTS adapters and policies (for example github) (QAuth only)——1.0DecisionADR-014 P0, ADR-016.
Scopes and resourcesRemote approval settings (QAuth only)CIBA policy—1.0DecisionREMOTE_APPROVAL_* (ADR-014 §14).
Roles, groups, organizationsRealm rolesRealm rolesroles, user_roles tables1.0DecisionNo composite roles. Flat groups carry roles (fork D).
Roles, groups, organizationsGroups: members, attributes, roles, default groupsGroups—1.0DecisionFlat groups. Hierarchy comes in 1.x (fork D).
Roles, groups, organizationsOrganizations: domains, members, invitations, IdPsOrganizations—1.xDecisionFork D.
UsersUser list, search, filterUsers—1.0Decision—
UsersCreate a user (no password, by invitation)Users · Add user—1.0DecisionADR-017: the admin API creates users without a password.
UsersAdmin sets a password (temporary password)Users · Credentials · Reset password—NoRow reviewAn admin only sends a set-up or reset link, or copies one. Copying needs admin:security and a passkey.
UsersCredentials: delete passkey and TOTP, labelsUsers · Credentials—1.0Row reviewadmin:security, a passkey, a notice to the user. A second admin’s approval is recommended, and a realm can require it.
UsersRequired actions: renew password, set up MFAUsers · Required user actions—1.0Row reviewPer user and in bulk for a group.
UsersAttributesUsers · Attributesuser_attributes1.0DecisionFollow the profile schema (fork E).
UsersConsents, linked IdPs, wallets, sessionsUsers · Consents, Identity provider links, Sessions—1.0Decision—
UsersThe user’s agents; revoke by agent (QAuth only)——1.0DecisionADR-014 §13, owner-or-admin rule.
UsersDisable and deleteUsers · Enabled, Delete—1.0Decision—
UsersImpersonationUsers · Impersonate—NoDecisionAct-chain delegation instead.
UsersBulk importPartial importdb:import-users (ADR-017 F1d, CLI)1.xRow reviewThe CLI exists in 1.0.
UsersInbound SCIM provisioningSCIM API (GA in 26.8)Read-only projection for agents1.xRow reviewTogether with organizations.
Sessions and eventsActive sessions, sign-out, revoke allSessionsSessions only in Redis1.0DecisionPostgres is the source of truth, Redis a cache. Back-channel logout delivery status is visible.
Sessions and eventsUser eventsEvents · User eventsaudit_logs, no UI1.0Row reviewFiltered list with readable descriptions.
Sessions and eventsAdmin events: who changed what, with a diffEvents · Admin events—1.0Row reviewAlways on. Secret values are masked.
Sessions and eventsEvent retentionRealm settings · Events—1.0Row reviewUser events 90 days; admin events 1 year, at least 90 days.
Sessions and eventsEvent delivery: signed webhook, cursor API, SSF/CAEP, OpenTelemetryEvent listeners—1.0Row reviewAll four in 1.0. Event types and payload schema are inside the 1.0 promise.
Sessions and eventsAgent actions (SET events) (QAuth only)——1.0DecisionADR-014 P3 agent_actions.
AuthenticationFlow editorAuthentication · Flows—1.xDecision1.0 has the policy screen; the editor comes in 1.x (fork B).
AuthenticationMFA requirement and allowed methodsRequired actions, Conditional OTP—1.0DecisionPer realm and per client: allowed methods, MFA, passkey first, step-up (fork B).
AuthenticationPassword policyPolicies · Password policyzxcvbn and length1.0DecisionNIST SP 800-63B-4: at least 15 characters, lowerable to 8. No composition rules and no expiry.
AuthenticationOTP policyPolicies · OTP policy—NoRow reviewFixed at SHA-1, 6 digits, 30 seconds.
AuthenticationWebAuthn policyPolicies · WebAuthn (passwordless)—1.0DecisionRP ID and its lock state are read-only. User verification is required. An AAGUID allow list comes in 1.x.
AuthenticationBot challenge providerRegistration flow · reCAPTCHA—1.0DecisionADR-017 F1b.
Identity providersGeneric OIDC upstreamIdentity providers · OpenID Connect—1.0DecisionThrough the AuthMethod contract.
Identity providersMy Number (Digital Agency authentication app) (QAuth only)——1.0DecisionA plugin, ready with 1.0; the core stays provider-agnostic. Needs private_key_jwt.
Identity providersOIDC presets: Google, Microsoft, AppleIdentity providers · Social—1.0Row reviewNot in the core; plugins ready with 1.0. The core has only generic OIDC.
Identity providersGitHub (not OIDC)Identity providers · Social—1.0Row reviewA plugin ready with 1.0. Other providers become plugins on demand.
Identity providersSAML upstreamIdentity providers · SAML—1.xDecisionADR-018.
Identity providersOpenID Federation upstream (QAuth only)——1.0DecisionADR-018.
Identity providersWallet verifier: HAIP, trusted issuers, status lists26.8 OID4VP, experimentalenv (WALLET_*)1.0Decision—
Identity providersIdP mappers (claim to attribute)Identity providers · Mappers—1.0Row reviewThe same language as the mapper system. Never used to match accounts.
Identity providersCounting an upstream account as verifiedFirst login flow, Trust email—1.0Row reviewAn upstream sign-in makes the account a verified account; a per-IdP switch turns this off. Upstream claims are kept as attributes with their source.
Identity providersHide on login page, order, iconIdentity providers · Display—1.0Row reviewName, icon, order, hide; a direct-routing parameter.
Identity providersLDAP and Active DirectoryUser federation · LDAP—1.xDecisionAn extension of the AuthMethod contract.
Identity providersKerberosUser federation · Kerberos—NoDecision—
Admins and serverAdmins and their permissionsFine-grained admin permissions v2—1.0DecisionOperator and realm admins. admin:read, admin:write and admin:security, intersected with the role catalog.
Admins and serverTwo-admin approval (QAuth only)——1.0Row reviewRecommended for every admin:security operation; the realm picks where it is mandatory. A warning on single-admin deployments.
Admins and serverServer info: version, switches, experimental featuresServer info—1.0Row reviewOperator-realm admins only. Installed plugins and role health are shown too.

Developer portal

AreaFeatureKeycloak locationQAuth today1.0BasisNote
PortalSign-in and sign-up—/auth/login, /auth/register1.0DecisionSign-in through /oauth/authorize (ADR-017 F0); sign-up redirects to the hosted page (F2a).
PortalMy clients: create, edit, rotate secretsDCRExists1.0Decision—
PortalStatic API keys—Exists1.0DecisionADR-008: non-production clients only.
PortalCIMD document validator (QAuth only)——1.0Row reviewIn the portal and the CLI, with SSRF-safe fetching.
PortalMCP server registration: resource and protected-resource metadata (QAuth only)——1.0Row reviewAdmin approval is a realm setting.
PortalFlow test console: code + PKCE, token decoder——1.0Row reviewDevelopment and staging clients. In production it runs in the paired sandbox realm.

Admin console — federation section

AreaFeatureKeycloak locationQAuth today1.0BasisNote
FederationEntity configuration and authority hints (QAuth only)——1.0DecisionLeaf.
FederationSubordinates, metadata policy, constraints (QAuth only)——1.0DecisionTrust anchor and intermediate roles.
FederationTrust marks: types, issuance, revocation (QAuth only)——1.0Decision—
FederationExplicit registration requests (QAuth only)——1.0Decision—
FederationTrust chain resolver (QAuth only)——1.0Row reviewIn the federation section, on the IdP screen and in the CLI.
FederationFederation keys (QAuth only)——1.0DecisionThe trust-anchor key lives in a separate deployment.

Emails

AreaFeatureKeycloak locationQAuth today1.0BasisNote
EmailsVerification code, reset, invitation and set-upEmail templatesVerification link1.0Decision—
EmailsSecurity notices: new sign-in, password changed, passkey addedEvent · login error, update password—1.0Row reviewOn by default. Notices about credential changes, those that carry a reset pass, and the notice of a wrong second factor after a correct password cannot be turned off (ADR-017).
EmailsTemplate editingThemes · email—NoDecisionTemplates are fixed. Logo, colours and strings come from brand settings (fork C).