Consent
The per-user consent screen for non-first-party clients and the scope display-name catalog.
Non-first-party clients — dynamically registered, CIMD, and any manually created client without IsFirstParty — get an explicit per-user consent screen before SqlOS issues their authorization code. First-party clients never see it. Device authorization requests are also exempt: the device-approval page is already an explicit consent surface for the client and its scopes.
Consent is the first interstitial. It runs before invitation acceptance, organization selection, and MFA, so a denied request never advances authorization state.
prompt=consent, which forces the screen even with a covering grant.The screen shows the client name and each granted scope's display name and optional description from the scope display-name catalog. Scopes without a catalog entry fall back to the raw scope string, so the screen always renders. An empty granted set renders as default access.
Approval records one grant per user and client. Approving again unions the newly granted scopes into the active grant; a previously revoked grant is reactivated with exactly the newly granted set, so old revoked approvals never silently widen a fresh consent.
With an active grant covering every granted scope, the authorization completes silently — the same behavior first-party clients get. prompt=none returns consent_required only when no covering grant exists. Denying redirects to the client with the RFC 6749 access_denied error and cancels the request.
This preserves the protection the first-party boundary exists for: a client-controlled authorization URL still cannot ride an unrelated signed-in browser session, because the first grant always requires an explicit user gesture. When a CIMD client's security-sensitive metadata changes (redirect URIs, auth method, grant/response types, scopes), SqlOS revokes its remembered grants in the same save, so users re-consent on the next authorization.
The hosted AuthPage renders the consent view with Allow access and Deny request forms posting to POST /sqlos/auth/consent/approve and POST /sqlos/auth/consent/deny. These are hosted form handlers with the standard antiforgery requirement; see the HTTP API Reference.
Headless UIs receive a view model with view: "consent" and consentScopes (each entry has scope, displayName, and optional description). Drive it with @sqlos/headless (flow.consent.approve / flow.consent.deny). The routes POST /sqlos/auth/headless/consent/approve and /consent/deny are the HTTP wire contract; client code should use the package. See Build your own login and signup UI.
await flow.consent.approve();
// or flow.consent.deny()
if (flow.status === "redirect" && flow.redirectUrl) {
window.location.assign(flow.redirectUrl);
}The flow holds requestId and consentToken. Approval can still continue to organization selection or MFA; denial sets status === "redirect" with the access_denied error URL. The consent token is transaction-bound: it cannot be substituted across users, clients, or authorization requests, and it expires after ten minutes.
The catalog that maps raw scopes to consent-screen text is one domain model with three control planes:
builder.AddSqlOS<AppDbContext>(options =>
{
options.AuthServer.SeedScopeDisplayName(
"tasks.read",
"Read your tasks",
"See task titles, notes, and due dates.");
});GET/POST /sqlos/admin/auth/api/scope-display-names and PUT/DELETE /sqlos/admin/auth/api/scope-display-names/{id}, or SqlOSAdminService from trusted backend code.Startup reconciliation is deterministic and idempotent. Code-seeded entries are code-owned and reapplied authoritatively on every restart; edit them in source control. Dashboard- and API-created entries are dashboard-owned and are never overwritten or adopted by startup. A code-owned entry whose seed disappears is marked orphaned rather than deleted, and seeding a scope that a dashboard-owned entry already claims fails loudly instead of silently changing ownership. Reconciliation outcomes are audited as configuration.reconciled.
GET /sqlos/admin/auth/api/users/{userId}/grants and POST /sqlos/admin/auth/api/users/{userId}/grants/{grantId}/revoke.POST /sqlos/auth/account/grants and POST /sqlos/auth/account/grants/revoke. Like /logout, the refresh token is the credential; it scopes what the caller can see and revoke. The token must come from a first-party client's session: these surfaces manage the user's grants across every client, so a third-party client's refresh token is rejected with the same generic 401 as an invalid token.Revocation does not end existing sessions or tokens; it forces the consent screen on the user's next authorization for that client.
oauth.consent.granted, oauth.consent.denied, and oauth.consent.revoked record the consent lifecycle. Catalog changes record scope_display_name.created, scope_display_name.updated, and scope_display_name.deleted.
@sqlos/headlessid_token issuance for the granted openid scope