proposed core.consent

Ask the signed-in person to allow a client's access, as a step of the Interaction API

When decideAuthorization answers that a request needs consent and INTERACTION_UI_ENABLED is on, a consent interaction starts for the signed-in browser session. Its step holds the client as the server's own records describe it, the scopes the client's allowlist holds (each marked when it is dangerous), the resources or else the client's audience, for an AI agent a line in QAuth's own words, and "Remember this decision", checked by default; a request the step cannot show is refused at the start. The answer carries only allow or deny and the remember choice. The interaction finishes before anything is decided; the request is checked again, deny sends access_denied to the client, and allow checks the account, the agent scope cap, the QAuth API audiences, whether the client is still what the step said, the shown set against the client as it is now, and the step-up gate. Then one transaction mints the code for exactly the shown set, saves the grant when the person asked to remember it, and records the events; the browser goes back to the client with the code, state and iss.

What this view shows

the consent step of an authorization request with INTERACTION_UI_ENABLED on: its start, the step and personal-data reads, and the answer from its grammar check to the response.

Open world: what is not drawn here is not ruled out. Only the guarantees below are binding.

Left out of this view:

The main story

When: At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember true, as the checked box of a form post sends it, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks.

  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the ceremony page reads the personal data, with the flow cookie
  7. Return the consent step's personal data: the person allows or denies, with the CSRF value of the step view; the answer takes the turn
  8. Check the answer against the consent step's grammar: the answer passes the grammar
  9. End the interaction before anything is decided: the interaction is over
  10. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  11. Read the decision: the person allowed
  12. Find the session again, with its account: the session is live and its account may use it
  13. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  14. Refuse a QAuth API audience the client no longer lists: every QAuth API audience among the resources is one the client lists
  15. Check that the client is still what the step said: the client is what the step said
  16. Grant only the scopes the step showed, while the client may still have them: the shown scopes are still grantable
  17. Grant only the resources the step showed: the shown resources are the request's
  18. Ask for a fresh sign-in when the grant needs one: the sign-in is fresh enough
  19. Mint the authorization code, in the mint transaction: the code is written, and the person asked to remember the decision
  20. Save the grant, in the same transaction: the grant is written
  21. Record the grant, in the same transaction, and commit: the events are recorded and the transaction commits

Result: Send the authorization code to the client.

The flow

decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request the display cannot describe where the redirect URI goes, at the finish of an interaction the display cannot describe where the redirect URI goes, at /oauth/authorize the client is described the step cannot carry the scopes, at the finish of an interaction the step cannot carry the scopes, for a client an operator provisioned the step cannot carry the scopes, for a client no operator provisioned the step cannot carry the resources, at the finish of an interaction the step cannot carry the resources, for a client an operator provisioned the step cannot carry the resources, for a client no operator provisioned the step cannot carry the client's audience, at the finish of an interaction the step cannot carry the client's audience, for a client an operator provisioned the step cannot carry the client's audience, for a client no operator provisioned the step can show the request the interaction is stored the ceremony page reads the step by the interaction id, for an agent client the ceremony page reads the step by the interaction id the ceremony page reads the personal data, with the flow cookie the person allows or denies, with the CSRF value of this view; the answer takes the turn the ceremony page reads the personal data, with the flow cookie the person allows or denies, with the CSRF value of this view; the answer takes the turn the ceremony page reads the step again the ceremony page reads the step again the person allows or denies, with the CSRF value of the step view; the answer takes the turn the answer does not pass the step grammar the answer passes the grammar the interaction is over the client, or its redirect URI, no longer passes the client may no longer use the endpoint the request still passes, or only its dpop_jkt is refused the person denied; consent.denied { error: access_denied } is recorded the person allowed, and the request is refused for its dpop_jkt the person allowed the account was disabled since: the request needs a sign-in the session is live and its account may use it an agent scope is above the cap; consent.denied { error: invalid_scope, reason: agent_scope_mode_exceeded } is recorded no agent scope is above the cap a resource names a QAuth API audience the client does not list; consent.denied { error: invalid_target, reason: reserved_audience_not_listed } is recorded every QAuth API audience among the resources is one the client lists the step named an AI agent, and the client is no agent now; nothing is granted the step named no agent, and the client is an AI agent now; nothing is granted the client is what the step said the decision asks for consent: a new consent interaction continues the request the client may no longer have every shown scope; consent.denied { error: invalid_scope, reason: consent_scope_mismatch } is recorded the shown scopes are still grantable the shown resources are not the request's; consent.denied { error: invalid_target, reason: consent_resource_mismatch } is recorded the shown resources are the request's the sign-in is too old for what is asked; step_up.required { reason: fresh_authentication_required or native_profile_login_required, dangerousScopes?, prompt?, maxAge? } is recorded the sign-in is fresh enough the code is written, and the person asked to remember the decision the code is written, for this request only the grant is written the events are recorded and the transaction commits the sign-in interaction is stored, for a step-up the sign-in interaction is stored, for a disabled account the sign-in interaction is stored, for a session that ended under the request start idle Wait for a request that needs consent idle · server step clientDisplay(client, { redirectUri }) (#593, ADR-019 Decision 6), computed once, as the interaction starts, and kept in the interaction record (its optional member client; the record version stays): kind (cimd, admin_registered or unverified), the verified domain, where the code goes (a host, this_device for a loopback address, or an app's scheme), the declared name, sanitised and marked declared, and the newly registered badge (isDynamicClientWithinBadgeWindow). Never a logo, never a URL the client supplied. A redirect URI the display cannot describe refuses the start (InvalidRequestError), as the page does, and is never redirected to. Describe the client from the server's own records describingClient · server step consentStep: the requested scopes the client's allowlist holds (filterRequestedScopes), in request order, each { scope, text?, sensitive: false, dangerous } (text is describeScope's for the scopes QAuth defines, dangerous is isDangerousScope); the request's resource indicators; when the request named none, the token's aud from the audience an operator configured for the client, without QAuth API audiences (userGrantDefaultAudience); agent ({} or { max_lifespan } from the realm's sso_max_lifespan) for an AI agent client only; remember_default true. A request the step cannot carry is refused with authorization.failed (consent_step_unshowable) recorded, to the redirect_uri unless the client registered itself and the person has seen no QAuth page yet. Build the consent step from the request buildingStep · server step An interaction record (#592) holding the validated request, the consent step and the client display, at most 32 KiB, with a transaction of the browser session's user, sid and sign-in time, and that session recorded as carried in (#964); interaction.created is recorded. At /oauth/authorize a new secret in the __Host-qauth_flow cookie binds it to the browser; at a finish, continueInteraction keeps the finished interaction's absolute expiry and adds the new secret to the flow secrets the finish left. The exact scope set shown is the record's step params. Start the consent interaction startingConsent · server step The redirect carries the interaction id and nothing else, as core.authorization's atConsentCeremony. Send the browser to the ceremony page of the consent interaction atConsentCeremony · server step The step read needs the interaction id only and holds no personal data. Its data is the record's: the scope set the answer will grant is the one listed here, and nothing in it is HTML or a URL the client supplied. Return the consent step showingStep · server step As showingStep, with params.agent (ADR-014 §11, ADR-019 Decision 6): the screen says "This app is an AI agent." and how long it may keep working, in QAuth's own words, from this typed data only, never from the name the client declared. Return the consent step of an AI agent client showingStepForAgent · server step Only to the browser whose flow cookie binds the interaction, and never from another origin. Return the consent step's personal data showingPersonal · server step { type: urn:qauth:ia:consent:v1-rc.0, csrf, decision: allow | deny, remember?: boolean, default false }, nothing else: a member the grammar does not declare is refused, so no answer field can add a scope or a resource. A refusal gives the turn back; the step and its CSRF value stay. Check the answer against the consent step's grammar checkingAnswer · server step As every finish (#616, #963): the browser session the consent was for must still be live, the record is deleted unless the expiry sweep claimed it first, the interaction's secret leaves the flow cookie, and interaction.answered (outcome finished) is recorded. No user.signed_in and no last-login update: nobody signed in. Only now is the answer decided (finishConsent), so no code is minted while the interaction can still be answered. End the interaction before anything is decided takingFinish · server step checkAuthorizationRequest on the request the record held: the client may have been deleted, disabled or changed since the step was shown. A refusal is thrown, so nothing goes to a redirect URI the client no longer holds. Check the request again, as the endpoint checks it recheckingRequest · server step Deny is answered before anything else of the request is looked at. Read the decision readingDecision · server step liveBrowserSession, by the sid the interaction holds. Find the session again, with its account findingSession · server step ADR-007 §2. findExceedingAgentScopesForClient on the request's scope and the client as it is now. Refuse an agent scope above the client's cap, where the code is minted checkingAgentCap · server step unlistedReservedAudiences (#509), on the client as it is now. Refuse a QAuth API audience the client no longer lists checkingAudience · server step params.agent present against isAgentClient(client) now (#845): the person must not allow an agent that the step did not name as one, nor see an agent line for a client that is no agent any more. Check that the client is still what the step said checkingAgentKind · server step decideAuthorization, as core.authorization runs it at a finish, logged at warn and with no event of its own. It normally asks for consent again, and a new consent interaction starts, whose step names the client as it is now; its other outcomes are core.authorization's. Decide the request again for the client as it is now decidingAgain · server step The shown scopes, filtered by the client's allowlist as it is now, must be the shown set. The grant is the shown set, from the record: never the request's scope parameter, never a field of the answer. Grant only the scopes the step showed, while the client may still have them checkingScopes · server step RFC 8707. The shown resources must be the request's. Grant only the resources the step showed checkingResources · server step ADR-007 §2. evaluateStepUp with the shown scopes, no prior consent scopes (every scope about to be granted counts as new, so a stored grant cannot skip the gate), the request's prompt and max_age, the session's sign-in time, the two-minute window, the client's policy (agentStepUpEnforced) and forcesLoginPrompt(client) for a native_backend client. Ask for a fresh sign-in when the grant needs one checkingStepUp · server step One READ COMMITTED transaction (MINT_TRANSACTION) begins with mintAuthorizationCode: the shown scopes, the shown resources, the request's code_challenge and method, nonce, state, redirect_uri and dpop_jkt, the session's sign-in time, assurance level, expiry and sid. It locks the session's row and refuses a session that has ended; nothing is written then, and authorization.failed { error: login_required, reason: session_ended_during_authorization } is recorded. Mint the authorization code, in the mint transaction mintingCode · server step oauthConsents.upsertGrant(realm, user, client, the shown scopes, the shown resources): one active oauth_consents row, merged with a prior grant of the client. A later request that fits it skips the step through canSkipConsent. Save the grant, in the same transaction savingConsent · server step step_up.elevated { scopes, dangerousScopes, scopeMode?, authenticatedAt } when a dangerous scope is granted (scopeMode only for an agent client), then consent.granted { scopes, remembered, dangerousScopes? }. The code, the grant and both events commit together. Record the grant, in the same transaction, and commit recordingGrant · server step continueInteraction at the identifier step, carrying the session in (#964) so that "Not you?" can end it, within the finished interaction's absolute expiry. The person signs in afresh, the request is decided again and the consent step is shown again. No code. Continue the request in a new sign-in interaction continuingToSignIn · server step code_issued Send the authorization code to the client redirect with code + state + iss sign_in_continues.fresh_login Send the browser to sign in afresh before the grant no redirect, page shown sign_in_continues.account_disabled Send the browser to sign in, for an account disabled since the step was shown no redirect, page shown sign_in_continues.session_ended Send the browser to sign in, for a session that ended under the request no redirect, page shown access_denied.user_denied Tell the client the person denied the request redirect, error=access_denied invalid_request.dpop_jkt_unreadable Tell the client its dpop_jkt is malformed, now that DPoP is on redirect, error=invalid_request invalid_scope.agent_scope_mode_exceeded Tell the client an agent scope is above its cap redirect, error=invalid_scope invalid_target.unlisted_reserved_audience Tell the client it named a QAuth API audience it does not list redirect, error=invalid_target invalid_scope.consent_scope_mismatch Refuse a shown scope set the client may no longer have redirect, error=invalid_scope invalid_target.consent_resource_mismatch Refuse shown resources that are not the request's redirect, error=invalid_target invalid_scope.consent_not_shown Tell the client the consent step cannot show its scopes redirect, error=invalid_scope invalid_scope.consent_not_shown_shown Show, rather than redirect, that error for a client no operator provisioned no redirect, page shown invalid_scope.consent_not_shown_at_finish Tell the client the consent step cannot show its scopes, at the finish of an interaction redirect, error=invalid_scope invalid_target.consent_not_shown Tell the client the consent step cannot show its resources redirect, error=invalid_target invalid_target.consent_not_shown_shown Show, rather than redirect, that error for a client no operator provisioned no redirect, page shown invalid_target.consent_not_shown_at_finish Tell the client the consent step cannot show its resources, at the finish of an interaction redirect, error=invalid_target invalid_target.audience_not_shown Tell the client the consent step cannot show its audience redirect, error=invalid_target invalid_target.audience_not_shown_shown Show, rather than redirect, that error for a client no operator provisioned no redirect, page shown invalid_target.audience_not_shown_at_finish Tell the client the consent step cannot show its audience, at the finish of an interaction redirect, error=invalid_target invalid_request.redirect_uri_not_shown Refuse a redirect URI the consent step cannot describe, at /oauth/authorize no redirect, page shown invalid_request.redirect_uri_not_shown_at_finish Refuse a redirect URI the consent step cannot describe, at the finish of an interaction no redirect, page shown The interaction stays at the step, with its CSRF value; nothing is finished or granted. Refuse an answer the consent step's grammar does not accept no redirect, page shown No redirect goes to a URI the client may no longer hold, whatever the decision was. Refuse an answer whose client or redirect URI no longer passes no redirect, page shown unauthorized_client.client_changed Refuse an answer whose client may no longer use the endpoint no redirect, page shown
decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request the display cannot describe where the redirect URI goes, at the finish of an interaction the display cannot describe where the redirect URI goes, at /oauth/authorize the client is described the step cannot carry the scopes, at the finish of an interaction the step cannot carry the scopes, for a client an operator provisioned the step cannot carry the scopes, for a client no operator provisioned the step cannot carry the resources, at the finish of an interaction the step cannot carry the resources, for a client an operator provisioned the step cannot carry the resources, for a client no operator provisioned the step cannot carry the client's audience, at the finish of an interaction the step cannot carry the client's audience, for a client an operator provisioned the step cannot carry the client's audience, for a client no operator provisioned the step can show the request the interaction is stored fails: the record of the next interaction is over 32 KiB fails: the request is over 16 KiB, or the record over 32 KiB fails: the interaction store fails, at the finish of an interaction fails: the interaction store fails the ceremony page reads the step by the interaction id, for an agent client the ceremony page reads the step by the interaction id the ceremony page reads the personal data, with the flow cookie the person allows or denies, with the CSRF value of this view; the answer takes the turn the ceremony page reads the personal data, with the flow cookie the person allows or denies, with the CSRF value of this view; the answer takes the turn the ceremony page reads the step again the ceremony page reads the step again the person allows or denies, with the CSRF value of the step view; the answer takes the turn the answer does not pass the step grammar the answer passes the grammar the interaction is over fails: the expiry sweep claimed the interaction first fails: the session the consent was for has ended, such as by a sign-out in another tab fails: the interaction store fails; the turn is given back the client, or its redirect URI, no longer passes the client may no longer use the endpoint the request still passes, or only its dpop_jkt is refused the person denied; consent.denied { error: access_denied } is recorded the person allowed, and the request is refused for its dpop_jkt the person allowed the account was disabled since: the request needs a sign-in the session is live and its account may use it an agent scope is above the cap; consent.denied { error: invalid_scope, reason: agent_scope_mode_exceeded } is recorded no agent scope is above the cap a resource names a QAuth API audience the client does not list; consent.denied { error: invalid_target, reason: reserved_audience_not_listed } is recorded every QAuth API audience among the resources is one the client lists the step named an AI agent, and the client is no agent now; nothing is granted the step named no agent, and the client is an AI agent now; nothing is granted the client is what the step said the decision asks for consent: a new consent interaction continues the request the client may no longer have every shown scope; consent.denied { error: invalid_scope, reason: consent_scope_mismatch } is recorded the shown scopes are still grantable the shown resources are not the request's; consent.denied { error: invalid_target, reason: consent_resource_mismatch } is recorded the shown resources are the request's the sign-in is too old for what is asked; step_up.required { reason: fresh_authentication_required or native_profile_login_required, dangerousScopes?, prompt?, maxAge? } is recorded the sign-in is fresh enough the code is written, and the person asked to remember the decision the code is written, for this request only fails: the session ended under the request; nothing is written fails: the database fails, or three codes in a row collide; nothing is written the grant is written fails: the database fails; the transaction rolls back and no code is left the events are recorded and the transaction commits fails: the event store fails; the transaction rolls back and neither the code nor the grant is left the sign-in interaction is stored, for a step-up the sign-in interaction is stored, for a disabled account the sign-in interaction is stored, for a session that ended under the request fails: the record of the next interaction is over 32 KiB fails: the interaction store fails start idle Wait for a request that needs consent idle · server step clientDisplay(client, { redirectUri }) (#593, ADR-019 Decision 6), computed once, as the interaction starts, and kept in the interaction record (its optional member client; the record version stays): kind (cimd, admin_registered or unverified), the verified domain, where the code goes (a host, this_device for a loopback address, or an app's scheme), the declared name, sanitised and marked declared, and the newly registered badge (isDynamicClientWithinBadgeWindow). Never a logo, never a URL the client supplied. A redirect URI the display cannot describe refuses the start (InvalidRequestError), as the page does, and is never redirected to. Describe the client from the server's own records describingClient · server step consentStep: the requested scopes the client's allowlist holds (filterRequestedScopes), in request order, each { scope, text?, sensitive: false, dangerous } (text is describeScope's for the scopes QAuth defines, dangerous is isDangerousScope); the request's resource indicators; when the request named none, the token's aud from the audience an operator configured for the client, without QAuth API audiences (userGrantDefaultAudience); agent ({} or { max_lifespan } from the realm's sso_max_lifespan) for an AI agent client only; remember_default true. A request the step cannot carry is refused with authorization.failed (consent_step_unshowable) recorded, to the redirect_uri unless the client registered itself and the person has seen no QAuth page yet. Build the consent step from the request buildingStep · server step An interaction record (#592) holding the validated request, the consent step and the client display, at most 32 KiB, with a transaction of the browser session's user, sid and sign-in time, and that session recorded as carried in (#964); interaction.created is recorded. At /oauth/authorize a new secret in the __Host-qauth_flow cookie binds it to the browser; at a finish, continueInteraction keeps the finished interaction's absolute expiry and adds the new secret to the flow secrets the finish left. The exact scope set shown is the record's step params. Start the consent interaction startingConsent · server step The redirect carries the interaction id and nothing else, as core.authorization's atConsentCeremony. Send the browser to the ceremony page of the consent interaction atConsentCeremony · server step The step read needs the interaction id only and holds no personal data. Its data is the record's: the scope set the answer will grant is the one listed here, and nothing in it is HTML or a URL the client supplied. Return the consent step showingStep · server step As showingStep, with params.agent (ADR-014 §11, ADR-019 Decision 6): the screen says "This app is an AI agent." and how long it may keep working, in QAuth's own words, from this typed data only, never from the name the client declared. Return the consent step of an AI agent client showingStepForAgent · server step Only to the browser whose flow cookie binds the interaction, and never from another origin. Return the consent step's personal data showingPersonal · server step { type: urn:qauth:ia:consent:v1-rc.0, csrf, decision: allow | deny, remember?: boolean, default false }, nothing else: a member the grammar does not declare is refused, so no answer field can add a scope or a resource. A refusal gives the turn back; the step and its CSRF value stay. Check the answer against the consent step's grammar checkingAnswer · server step As every finish (#616, #963): the browser session the consent was for must still be live, the record is deleted unless the expiry sweep claimed it first, the interaction's secret leaves the flow cookie, and interaction.answered (outcome finished) is recorded. No user.signed_in and no last-login update: nobody signed in. Only now is the answer decided (finishConsent), so no code is minted while the interaction can still be answered. End the interaction before anything is decided takingFinish · server step checkAuthorizationRequest on the request the record held: the client may have been deleted, disabled or changed since the step was shown. A refusal is thrown, so nothing goes to a redirect URI the client no longer holds. Check the request again, as the endpoint checks it recheckingRequest · server step Deny is answered before anything else of the request is looked at. Read the decision readingDecision · server step liveBrowserSession, by the sid the interaction holds. Find the session again, with its account findingSession · server step ADR-007 §2. findExceedingAgentScopesForClient on the request's scope and the client as it is now. Refuse an agent scope above the client's cap, where the code is minted checkingAgentCap · server step unlistedReservedAudiences (#509), on the client as it is now. Refuse a QAuth API audience the client no longer lists checkingAudience · server step params.agent present against isAgentClient(client) now (#845): the person must not allow an agent that the step did not name as one, nor see an agent line for a client that is no agent any more. Check that the client is still what the step said checkingAgentKind · server step decideAuthorization, as core.authorization runs it at a finish, logged at warn and with no event of its own. It normally asks for consent again, and a new consent interaction starts, whose step names the client as it is now; its other outcomes are core.authorization's. Decide the request again for the client as it is now decidingAgain · server step The shown scopes, filtered by the client's allowlist as it is now, must be the shown set. The grant is the shown set, from the record: never the request's scope parameter, never a field of the answer. Grant only the scopes the step showed, while the client may still have them checkingScopes · server step RFC 8707. The shown resources must be the request's. Grant only the resources the step showed checkingResources · server step ADR-007 §2. evaluateStepUp with the shown scopes, no prior consent scopes (every scope about to be granted counts as new, so a stored grant cannot skip the gate), the request's prompt and max_age, the session's sign-in time, the two-minute window, the client's policy (agentStepUpEnforced) and forcesLoginPrompt(client) for a native_backend client. Ask for a fresh sign-in when the grant needs one checkingStepUp · server step One READ COMMITTED transaction (MINT_TRANSACTION) begins with mintAuthorizationCode: the shown scopes, the shown resources, the request's code_challenge and method, nonce, state, redirect_uri and dpop_jkt, the session's sign-in time, assurance level, expiry and sid. It locks the session's row and refuses a session that has ended; nothing is written then, and authorization.failed { error: login_required, reason: session_ended_during_authorization } is recorded. Mint the authorization code, in the mint transaction mintingCode · server step oauthConsents.upsertGrant(realm, user, client, the shown scopes, the shown resources): one active oauth_consents row, merged with a prior grant of the client. A later request that fits it skips the step through canSkipConsent. Save the grant, in the same transaction savingConsent · server step step_up.elevated { scopes, dangerousScopes, scopeMode?, authenticatedAt } when a dangerous scope is granted (scopeMode only for an agent client), then consent.granted { scopes, remembered, dangerousScopes? }. The code, the grant and both events commit together. Record the grant, in the same transaction, and commit recordingGrant · server step continueInteraction at the identifier step, carrying the session in (#964) so that "Not you?" can end it, within the finished interaction's absolute expiry. The person signs in afresh, the request is decided again and the consent step is shown again. No code. Continue the request in a new sign-in interaction continuingToSignIn · server step code_issued Send the authorization code to the client redirect with code + state + iss sign_in_continues.fresh_login Send the browser to sign in afresh before the grant no redirect, page shown sign_in_continues.account_disabled Send the browser to sign in, for an account disabled since the step was shown no redirect, page shown sign_in_continues.session_ended Send the browser to sign in, for a session that ended under the request no redirect, page shown access_denied.user_denied Tell the client the person denied the request redirect, error=access_denied invalid_request.dpop_jkt_unreadable Tell the client its dpop_jkt is malformed, now that DPoP is on redirect, error=invalid_request invalid_scope.agent_scope_mode_exceeded Tell the client an agent scope is above its cap redirect, error=invalid_scope invalid_target.unlisted_reserved_audience Tell the client it named a QAuth API audience it does not list redirect, error=invalid_target invalid_scope.consent_scope_mismatch Refuse a shown scope set the client may no longer have redirect, error=invalid_scope invalid_target.consent_resource_mismatch Refuse shown resources that are not the request's redirect, error=invalid_target invalid_scope.consent_not_shown Tell the client the consent step cannot show its scopes redirect, error=invalid_scope invalid_scope.consent_not_shown_shown Show, rather than redirect, that error for a client no operator provisioned no redirect, page shown invalid_scope.consent_not_shown_at_finish Tell the client the consent step cannot show its scopes, at the finish of an interaction redirect, error=invalid_scope invalid_target.consent_not_shown Tell the client the consent step cannot show its resources redirect, error=invalid_target invalid_target.consent_not_shown_shown Show, rather than redirect, that error for a client no operator provisioned no redirect, page shown invalid_target.consent_not_shown_at_finish Tell the client the consent step cannot show its resources, at the finish of an interaction redirect, error=invalid_target invalid_target.audience_not_shown Tell the client the consent step cannot show its audience redirect, error=invalid_target invalid_target.audience_not_shown_shown Show, rather than redirect, that error for a client no operator provisioned no redirect, page shown invalid_target.audience_not_shown_at_finish Tell the client the consent step cannot show its audience, at the finish of an interaction redirect, error=invalid_target invalid_request.redirect_uri_not_shown Refuse a redirect URI the consent step cannot describe, at /oauth/authorize no redirect, page shown invalid_request.redirect_uri_not_shown_at_finish Refuse a redirect URI the consent step cannot describe, at the finish of an interaction no redirect, page shown invalid_request.authorization_too_large Refuse a request too large to hold in an interaction, at /oauth/authorize no redirect, page shown invalid_request.authorization_too_large_at_finish Refuse a next interaction too large to hold no redirect, page shown server_error.interaction_store Fail the request when the interaction store fails, at /oauth/authorize no redirect, page shown The interaction stays at the step, with its CSRF value; nothing is finished or granted. Refuse an answer the consent step's grammar does not accept no redirect, page shown The sweep reports the interaction expired; nothing is issued. Refuse an answer the expiry sweep beat no redirect, page shown Nothing is written; the turn is given back and the step stays (#963). Refuse an answer whose browser session ended since the step was shown no redirect, page shown No redirect goes to a URI the client may no longer hold, whatever the decision was. Refuse an answer whose client or redirect URI no longer passes no redirect, page shown unauthorized_client.client_changed Refuse an answer whose client may no longer use the endpoint no redirect, page shown server_error.interaction_store_at_finish Fail the answer when the interaction store fails no redirect, page shown The transaction rolls back: no code, no grant and no event of the grant is left. Fail the answer when the mint transaction fails no redirect, page shown

blue line: the main storyred dashed line: a failure path (shown with “Show failure paths”)dotted line: decided by a rule, nobody actsdashed grey box: the server decides, nothing is showngreen exit: successred exit: no redirect

What each step answers

StepThe outside world sees
Send the browser to the ceremony page of the consent interaction
atConsentCeremony
302 from /oauth/authorize; 303 for a form post or 200 for a JSON answer at the finish of an interaction · headers: {"Location":"/ui/interaction/<the interaction id, 43 base64url characters>, from /oauth/authorize; the same path on the realm's issuer, at the finish of an interaction","Set-Cookie":"__Host-qauth_flow=<the browser's interaction secrets, this one's added>; Path=/; HttpOnly; SameSite=Lax","Cache-Control":"no-store"} · body: none from /oauth/authorize; at the finish of an interaction, a JSON answer gets { result: redirect, location }
Return the consent step
showingStep
200 · headers: {"Content-Type":"application/json; charset=utf-8","Cache-Control":"no-store"} · body: {"step":"urn:qauth:ia:consent:v1-rc.0","alternatives":"[]","csrf":"the CSRF value of this step view, for one answer","brand":"the realm's brand","expiresAt":"when the interaction expires","client":"the client display the start computed: kind, verifiedDomain, redirect (host, this_device with the loopback warning, or app), declaredName marked declared, newClient","params":{"scopes":"the shown scopes, in request order, each { scope, text?, sensitive: false, dangerous }","resources":"the request's resource indicators","audience":"the token's aud from the client's configured audience, only when the request named no resource and the tokens are not for the client itself","remember_default":"true"}}
Return the consent step of an AI agent client
showingStepForAgent
200 · headers: {"Content-Type":"application/json; charset=utf-8","Cache-Control":"no-store"} · body: {"step":"urn:qauth:ia:consent:v1-rc.0","params":{"agent":"{} when the realm sets no sso_max_lifespan; { max_lifespan: <the realm's sso_max_lifespan, in seconds> }","others":"as showingStep"},"others":"as showingStep"}
Return the consent step's personal data
showingPersonal
200 · headers: {"Content-Type":"application/json; charset=utf-8","Cache-Control":"no-store"} · body: { identifier }, the identifier the user signed in with, from the session's side record, when it holds one; {} otherwise
Send the authorization code to the client
code_issued
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, redirect_uri with code (43 base64url characters), state when the request had one, and iss","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Send the browser to sign in afresh before the grant
sign_in_continues.fresh_login
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, the ceremony page of the new interaction on the realm's issuer: <issuer>/ui/interaction/<the interaction id>","Set-Cookie":"__Host-qauth_flow with the new interaction's secret, and without the finished interaction's","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Send the browser to sign in, for an account disabled since the step was shown
sign_in_continues.account_disabled
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, the ceremony page of the new interaction on the realm's issuer: <issuer>/ui/interaction/<the interaction id>","Set-Cookie":"__Host-qauth_flow with the new interaction's secret, and without the finished interaction's","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Send the browser to sign in, for a session that ended under the request
sign_in_continues.session_ended
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, the ceremony page of the new interaction on the realm's issuer: <issuer>/ui/interaction/<the interaction id>","Set-Cookie":"__Host-qauth_flow with the new interaction's secret, and without the finished interaction's","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Tell the client the person denied the request
access_denied.user_denied
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, redirect_uri with error=access_denied, error_description=User denied the authorization request., state when the request had one, and iss","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Tell the client its dpop_jkt is malformed, now that DPoP is on
invalid_request.dpop_jkt_unreadable
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, redirect_uri with error=invalid_request, error_description=dpop_jkt must be the base64url SHA-256 JWK thumbprint of a DPoP key, state when the request had one, and iss","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Tell the client an agent scope is above its cap
invalid_scope.agent_scope_mode_exceeded
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, redirect_uri with error=invalid_scope, error_description=requested scope exceeds the agent scope mode for this client, state when the request had one, and iss","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Tell the client it named a QAuth API audience it does not list
invalid_target.unlisted_reserved_audience
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, redirect_uri with error=invalid_target, error_description=a QAuth API audience is issued only to a client that lists it, state when the request had one, and iss","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Refuse a shown scope set the client may no longer have
invalid_scope.consent_scope_mismatch
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, redirect_uri with error=invalid_scope, error_description=a scope the consent step showed is no longer allowed for the client, state when the request had one, and iss","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Refuse shown resources that are not the request's
invalid_target.consent_resource_mismatch
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, redirect_uri with error=invalid_target, error_description=the consent step showed other resources than the request names, state when the request had one, and iss","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Tell the client the consent step cannot show its scopes
invalid_scope.consent_not_shown
302 · headers: {"Location":"redirect_uri with error=invalid_scope, error_description=the requested scopes cannot be shown for consent, state when the request had one, and iss"}
Show, rather than redirect, that error for a client no operator provisioned
invalid_scope.consent_not_shown_shown
400 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: a page titled "Sign-in not completed" that names the error with its description and the host of redirect_uri, and, for an http(s) redirect URI, a link "Continue to <host>" carrying the whole error response
Tell the client the consent step cannot show its scopes, at the finish of an interaction
invalid_scope.consent_not_shown_at_finish
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, redirect_uri with error=invalid_scope, error_description=the requested scopes cannot be shown for consent, state when the request had one, and iss","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Tell the client the consent step cannot show its resources
invalid_target.consent_not_shown
302 · headers: {"Location":"redirect_uri with error=invalid_target, error_description=the requested resources cannot be shown for consent, state when the request had one, and iss"}
Show, rather than redirect, that error for a client no operator provisioned
invalid_target.consent_not_shown_shown
400 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: a page titled "Sign-in not completed" that names the error with its description and the host of redirect_uri, and, for an http(s) redirect URI, a link "Continue to <host>" carrying the whole error response
Tell the client the consent step cannot show its resources, at the finish of an interaction
invalid_target.consent_not_shown_at_finish
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, redirect_uri with error=invalid_target, error_description=the requested resources cannot be shown for consent, state when the request had one, and iss","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Tell the client the consent step cannot show its audience
invalid_target.audience_not_shown
302 · headers: {"Location":"redirect_uri with error=invalid_target, error_description=the client's audience cannot be shown for consent, state when the request had one, and iss"}
Show, rather than redirect, that error for a client no operator provisioned
invalid_target.audience_not_shown_shown
400 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: a page titled "Sign-in not completed" that names the error with its description and the host of redirect_uri, and, for an http(s) redirect URI, a link "Continue to <host>" carrying the whole error response
Tell the client the consent step cannot show its audience, at the finish of an interaction
invalid_target.audience_not_shown_at_finish
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, redirect_uri with error=invalid_target, error_description=the client's audience cannot be shown for consent, state when the request had one, and iss","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Refuse a redirect URI the consent step cannot describe, at /oauth/authorize
invalid_request.redirect_uri_not_shown
400 · headers: {"Content-Type":"application/json; charset=utf-8"} · body: {"error":"invalid_request","error_description":"redirect_uri cannot be shown"}
Refuse a redirect URI the consent step cannot describe, at the finish of an interaction
invalid_request.redirect_uri_not_shown_at_finish
400 · headers: {"Content-Type":"application/problem+json","Cache-Control":"no-store"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-request","status":400,"code":"invalid_request","trace_id":"the request id"}
Refuse a request too large to hold in an interaction, at /oauth/authorize
invalid_request.authorization_too_large
400 · headers: {"Content-Type":"application/json; charset=utf-8"} · body: {"error":"invalid_request","error_description":"authorization request is too large"}
Refuse a next interaction too large to hold
invalid_request.authorization_too_large_at_finish
400 · headers: {"Content-Type":"application/problem+json","Cache-Control":"no-store"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-request","status":400,"code":"invalid_request","trace_id":"the request id"}
Fail the request when the interaction store fails, at /oauth/authorize
server_error.interaction_store
500 · headers: {"Content-Type":"application/json; charset=utf-8"} · body: {"error":"server_error"}
Refuse an answer the consent step's grammar does not accept
invalid_answer.grammar
400 · headers: {"Content-Type":"application/problem+json","Cache-Control":"no-store"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-answer","status":400,"code":"invalid_answer","trace_id":"the request id"}
Refuse an answer the expiry sweep beat
interaction_expired.finished_late
410 · headers: {"Content-Type":"application/problem+json","Cache-Control":"no-store"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#interaction-expired","status":410,"code":"interaction_expired","trace_id":"the request id"}
Refuse an answer whose browser session ended since the step was shown
interaction_expired.session_ended
410 · headers: {"Content-Type":"application/problem+json","Cache-Control":"no-store"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#interaction-expired","status":410,"code":"interaction_expired","trace_id":"the request id"}
Refuse an answer whose client or redirect URI no longer passes
invalid_request.client_changed
400 · headers: {"Content-Type":"application/problem+json","Cache-Control":"no-store"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-request","status":400,"code":"invalid_request","trace_id":"the request id"}
Refuse an answer whose client may no longer use the endpoint
unauthorized_client.client_changed
400 · headers: {"Content-Type":"application/problem+json","Cache-Control":"no-store"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#unauthorized-client","status":400,"code":"unauthorized_client","trace_id":"the request id"}
Fail the answer when the interaction store fails
server_error.interaction_store_at_finish
500 · headers: {"Content-Type":"application/problem+json"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","status":500,"code":"internal_error","trace_id":"the request id"}
Fail the answer when the mint transaction fails
server_error.store_at_finish
500 · headers: {"Content-Type":"application/problem+json"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","status":500,"code":"internal_error","trace_id":"the request id"}

How it decides what happens next

Describe the client from the server's own records

#IfThen
1the display cannot describe where the redirect URI goes, at the finish of an interactionRefuse a redirect URI the consent step cannot describe, at the finish of an interaction
2the display cannot describe where the redirect URI goes, at /oauth/authorizeRefuse a redirect URI the consent step cannot describe, at /oauth/authorize
3the client is describedBuild the consent step from the request

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1redirectUndescribable and atFinishinvalid_request.redirect_uri_not_shown_at_finish352 of 5280
2redirectUndescribableinvalid_request.redirect_uri_not_shown704 of 5280
3otherwisebuildingStep4224 of 5280
atFinish
entry == 'finish'
redirectUndescribable
startGate == 'undescribableRedirect'

Build the consent step from the request

#IfThen
1the step cannot carry the scopes, at the finish of an interactionTell the client the consent step cannot show its scopes, at the finish of an interaction
2the step cannot carry the scopes, for a client an operator provisionedTell the client the consent step cannot show its scopes
3the step cannot carry the scopes, for a client no operator provisionedShow, rather than redirect, that error for a client no operator provisioned
4the step cannot carry the resources, at the finish of an interactionTell the client the consent step cannot show its resources, at the finish of an interaction
5the step cannot carry the resources, for a client an operator provisionedTell the client the consent step cannot show its resources
6the step cannot carry the resources, for a client no operator provisionedShow, rather than redirect, that error for a client no operator provisioned
7the step cannot carry the client's audience, at the finish of an interactionTell the client the consent step cannot show its audience, at the finish of an interaction
8the step cannot carry the client's audience, for a client an operator provisionedTell the client the consent step cannot show its audience
9the step cannot carry the client's audience, for a client no operator provisionedShow, rather than redirect, that error for a client no operator provisioned
10the step can show the requestStart the consent interaction

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1scopesUnshowable and atFinishinvalid_scope.consent_not_shown_at_finish352 of 4224
2scopesUnshowable and operatorClientinvalid_scope.consent_not_shown352 of 4224
3scopesUnshowableinvalid_scope.consent_not_shown_shown352 of 4224
4resourcesUnshowable and atFinishinvalid_target.consent_not_shown_at_finish352 of 4224
5resourcesUnshowable and operatorClientinvalid_target.consent_not_shown352 of 4224
6resourcesUnshowableinvalid_target.consent_not_shown_shown352 of 4224
7audienceUnshowable and atFinishinvalid_target.audience_not_shown_at_finish352 of 4224
8audienceUnshowable and operatorClientinvalid_target.audience_not_shown352 of 4224
9audienceUnshowableinvalid_target.audience_not_shown_shown352 of 4224
10otherwisestartingConsent1056 of 4224
atFinish
entry == 'finish'
operatorClient
entry == 'authorizeOperator'
scopesUnshowable
startGate == 'unshowableScopes'
resourcesUnshowable
startGate == 'unshowableResources'
audienceUnshowable
startGate == 'unshowableAudience'

Check the answer against the consent step's grammar

#IfThen
1the answer does not pass the step grammarRefuse an answer the consent step's grammar does not accept
2the answer passes the grammarEnd the interaction before anything is decided

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1malformedAnswerinvalid_answer.grammar264 of 1056
2otherwisetakingFinish792 of 1056
malformedAnswer
answer == 'malformed'

Check the request again, as the endpoint checks it

#IfThen
1the client, or its redirect URI, no longer passesRefuse an answer whose client or redirect URI no longer passes
2the client may no longer use the endpointRefuse an answer whose client may no longer use the endpoint
3the request still passes, or only its dpop_jkt is refusedRead the decision

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1clientGoneinvalid_request.client_changed198 of 792
2clientUnusableunauthorized_client.client_changed198 of 792
3otherwisereadingDecision396 of 792
clientGone
recheck == 'clientGone'
clientUnusable
recheck == 'clientUnusable'

Read the decision

#IfThen
1the person denied; consent.denied { error: access_denied } is recordedTell the client the person denied the request
2the person allowed, and the request is refused for its dpop_jktTell the client its dpop_jkt is malformed, now that DPoP is on
3the person allowedFind the session again, with its account

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1deniedaccess_denied.user_denied132 of 396
2dpopRefusedinvalid_request.dpop_jkt_unreadable132 of 396
3otherwisefindingSession132 of 396
denied
answer == 'deny'
dpopRefused
recheck == 'dpopRefused'

Find the session again, with its account

#IfThen
1the account was disabled since: the request needs a sign-inContinue the request in a new sign-in interaction
2the session is live and its account may use itRefuse an agent scope above the client's cap, where the code is minted

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1accountDisabledcontinuingToSignIn12 of 132
2otherwisecheckingAgentCap120 of 132
accountDisabled
allowGate == 'accountDisabled'

Refuse an agent scope above the client's cap, where the code is minted

#IfThen
1an agent scope is above the cap; consent.denied { error: invalid_scope, reason: agent_scope_mode_exceeded } is recordedTell the client an agent scope is above its cap
2no agent scope is above the capRefuse a QAuth API audience the client no longer lists

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1overAgentCapinvalid_scope.agent_scope_mode_exceeded12 of 120
2otherwisecheckingAudience108 of 120
overAgentCap
allowGate == 'overAgentCap'

Refuse a QAuth API audience the client no longer lists

#IfThen
1a resource names a QAuth API audience the client does not list; consent.denied { error: invalid_target, reason: reserved_audience_not_listed } is recordedTell the client it named a QAuth API audience it does not list
2every QAuth API audience among the resources is one the client listsCheck that the client is still what the step said

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1audienceUnlistedinvalid_target.unlisted_reserved_audience12 of 108
2otherwisecheckingAgentKind96 of 108
audienceUnlisted
allowGate == 'unlistedAudience'

Check that the client is still what the step said

#IfThen
1the step named an AI agent, and the client is no agent now; nothing is grantedDecide the request again for the client as it is now
2the step named no agent, and the client is an AI agent now; nothing is grantedDecide the request again for the client as it is now
3the client is what the step saidGrant only the scopes the step showed, while the client may still have them

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1agentChanged and agentClientdecidingAgain6 of 96
2agentChangeddecidingAgain6 of 96
3otherwisecheckingScopes84 of 96
agentClient
clientIsAgent
agentChanged
allowGate == 'agentChanged'

Decide the request again for the client as it is now

#IfThen
1the decision asks for consent: a new consent interaction continues the requestDescribe the client from the server's own records

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1otherwisedescribingClient12 of 12

Grant only the scopes the step showed, while the client may still have them

#IfThen
1the client may no longer have every shown scope; consent.denied { error: invalid_scope, reason: consent_scope_mismatch } is recordedRefuse a shown scope set the client may no longer have
2the shown scopes are still grantableGrant only the resources the step showed

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1scopesNarrowedinvalid_scope.consent_scope_mismatch12 of 84
2otherwisecheckingResources72 of 84
scopesNarrowed
allowGate == 'scopesNarrowed'

Grant only the resources the step showed

#IfThen
1the shown resources are not the request's; consent.denied { error: invalid_target, reason: consent_resource_mismatch } is recordedRefuse shown resources that are not the request's
2the shown resources are the request'sAsk for a fresh sign-in when the grant needs one

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1resourcesDifferinvalid_target.consent_resource_mismatch12 of 72
2otherwisecheckingStepUp60 of 72
resourcesDiffer
allowGate == 'resourcesDiffer'

Ask for a fresh sign-in when the grant needs one

#IfThen
1the sign-in is too old for what is asked; step_up.required { reason: fresh_authentication_required or native_profile_login_required, dangerousScopes?, prompt?, maxAge? } is recordedContinue the request in a new sign-in interaction
2the sign-in is fresh enoughMint the authorization code, in the mint transaction

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1needsFreshLogincontinuingToSignIn36 of 60
2otherwisemintingCode24 of 60
needsFreshLogin
allowGate in ['promptLogin', 'maxAgeExceeded', 'dangerousScope']

Every way it can end

It ends withWhenWhat happens
Send the authorization code to the client
redirect with code + state + iss
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person allowed
  11. Find the session again, with its account: the session is live and its account may use it
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Refuse a QAuth API audience the client no longer lists: every QAuth API audience among the resources is one the client lists
  14. Check that the client is still what the step said: the client is what the step said
  15. Grant only the scopes the step showed, while the client may still have them: the shown scopes are still grantable
  16. Grant only the resources the step showed: the shown resources are the request's
  17. Ask for a fresh sign-in when the grant needs one: the sign-in is fresh enough
  18. Mint the authorization code, in the mint transaction: the code is written, for this request only
  19. Record the grant, in the same transaction, and commit: the events are recorded and the transaction commits
Send the browser to sign in afresh before the grant
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, prompt=login, or the client's first-party profile is native_backend (forcesLoginPrompt), and the sign-in is more than two minutes old (STEP_UP_FRESH_AUTH_WINDOW_MS)
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person allowed
  11. Find the session again, with its account: the session is live and its account may use it
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Refuse a QAuth API audience the client no longer lists: every QAuth API audience among the resources is one the client lists
  14. Check that the client is still what the step said: the client is what the step said
  15. Grant only the scopes the step showed, while the client may still have them: the shown scopes are still grantable
  16. Grant only the resources the step showed: the shown resources are the request's
  17. Ask for a fresh sign-in when the grant needs one: the sign-in is too old for what is asked; step_up.required { reason: fresh_authentication_required or native_profile_login_required, dangerousScopes?, prompt?, maxAge? } is recorded
  18. Continue the request in a new sign-in interaction: the sign-in interaction is stored, for a step-up
Send the browser to sign in, for an account disabled since the step was shown
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, the account was disabled since the step was shown: liveBrowserSession finds no usable session for the user
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person allowed
  11. Find the session again, with its account: the account was disabled since: the request needs a sign-in
  12. Continue the request in a new sign-in interaction: the sign-in interaction is stored, for a disabled account
Send the browser to sign in, for a session that ended under the request
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person allowed
  11. Find the session again, with its account: the session is live and its account may use it
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Refuse a QAuth API audience the client no longer lists: every QAuth API audience among the resources is one the client lists
  14. Check that the client is still what the step said: the client is what the step said
  15. Grant only the scopes the step showed, while the client may still have them: the shown scopes are still grantable
  16. Grant only the resources the step showed: the shown resources are the request's
  17. Ask for a fresh sign-in when the grant needs one: the sign-in is fresh enough
  18. Mint the authorization code, in the mint transaction: it fails, because the session ended under the request; nothing is written
  19. Continue the request in a new sign-in interaction: the sign-in interaction is stored, for a session that ended under the request
Tell the client the person denied the request
redirect, error=access_denied
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision deny, whatever remember says, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person denied; consent.denied { error: access_denied } is recorded
Tell the client its dpop_jkt is malformed, now that DPoP is on
redirect, error=invalid_request
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, DPoP was switched on since, and the dpop_jkt the request named is not one thumbprint, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person allowed, and the request is refused for its dpop_jkt
Tell the client an agent scope is above its cap
redirect, error=invalid_scope
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, the request's scope names an agent:* scope above the client's agent mode cap (any agent:* scope for a client that is no agent or has no cap), as findExceedingAgentScopesForClient reads it now
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person allowed
  11. Find the session again, with its account: the session is live and its account may use it
  12. Refuse an agent scope above the client's cap, where the code is minted: an agent scope is above the cap; consent.denied { error: invalid_scope, reason: agent_scope_mode_exceeded } is recorded
Tell the client it named a QAuth API audience it does not list
redirect, error=invalid_target
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, a resource names a QAuth API audience (urn:qauth:aud:) the client no longer lists (unlistedReservedAudiences)
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person allowed
  11. Find the session again, with its account: the session is live and its account may use it
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Refuse a QAuth API audience the client no longer lists: a resource names a QAuth API audience the client does not list; consent.denied { error: invalid_target, reason: reserved_audience_not_listed } is recorded
Refuse a shown scope set the client may no longer have
redirect, error=invalid_scope
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, the shown scopes, filtered by the client's allowlist as it is now, are not the shown set
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person allowed
  11. Find the session again, with its account: the session is live and its account may use it
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Refuse a QAuth API audience the client no longer lists: every QAuth API audience among the resources is one the client lists
  14. Check that the client is still what the step said: the client is what the step said
  15. Grant only the scopes the step showed, while the client may still have them: the client may no longer have every shown scope; consent.denied { error: invalid_scope, reason: consent_scope_mismatch } is recorded
Refuse shown resources that are not the request's
redirect, error=invalid_target
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, the shown resources are not the request's (defensive: they cannot differ by construction)
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person allowed
  11. Find the session again, with its account: the session is live and its account may use it
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Refuse a QAuth API audience the client no longer lists: every QAuth API audience among the resources is one the client lists
  14. Check that the client is still what the step said: the client is what the step said
  15. Grant only the scopes the step showed, while the client may still have them: the shown scopes are still grantable
  16. Grant only the resources the step showed: the shown resources are not the request's; consent.denied { error: invalid_target, reason: consent_resource_mismatch } is recorded
Tell the client the consent step cannot show its scopes
redirect, error=invalid_scope
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the step cannot carry the scopes: more than 64, or one outside its alphabet (RFC 6749 §3.3 without < and >) or over 128 characters, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step cannot carry the scopes, for a client an operator provisioned
Show, rather than redirect, that error for a client no operator provisioned
no redirect, page shown
At /oauth/authorize, for a client that registered itself: open registration, CIMD or a developer account, the step cannot carry the scopes: more than 64, or one outside its alphabet (RFC 6749 §3.3 without < and >) or over 128 characters, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step cannot carry the scopes, for a client no operator provisioned
Tell the client the consent step cannot show its scopes, at the finish of an interaction
redirect, error=invalid_scope
At the finish of another interaction of the request: a sign-in, or a consent whose client changed whether it is an agent, the step cannot carry the scopes: more than 64, or one outside its alphabet (RFC 6749 §3.3 without < and >) or over 128 characters, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step cannot carry the scopes, at the finish of an interaction
Tell the client the consent step cannot show its resources
redirect, error=invalid_target
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the step cannot carry the resources: more than 16, or one over 2048 characters, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step cannot carry the resources, for a client an operator provisioned
Show, rather than redirect, that error for a client no operator provisioned
no redirect, page shown
At /oauth/authorize, for a client that registered itself: open registration, CIMD or a developer account, the step cannot carry the resources: more than 16, or one over 2048 characters, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step cannot carry the resources, for a client no operator provisioned
Tell the client the consent step cannot show its resources, at the finish of an interaction
redirect, error=invalid_target
At the finish of another interaction of the request: a sign-in, or a consent whose client changed whether it is an agent, the step cannot carry the resources: more than 16, or one over 2048 characters, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step cannot carry the resources, at the finish of an interaction
Tell the client the consent step cannot show its audience
redirect, error=invalid_target
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the request names no resource, and the step cannot carry the audience the token gets from the client's configuration: a value holding markup or a control character (the token endpoint reads at most 20 values of at most 256 characters, which the step carries), the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step cannot carry the client's audience, for a client an operator provisioned
Show, rather than redirect, that error for a client no operator provisioned
no redirect, page shown
At /oauth/authorize, for a client that registered itself: open registration, CIMD or a developer account, the request names no resource, and the step cannot carry the audience the token gets from the client's configuration: a value holding markup or a control character (the token endpoint reads at most 20 values of at most 256 characters, which the step carries), the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step cannot carry the client's audience, for a client no operator provisioned
Tell the client the consent step cannot show its audience, at the finish of an interaction
redirect, error=invalid_target
At the finish of another interaction of the request: a sign-in, or a consent whose client changed whether it is an agent, the request names no resource, and the step cannot carry the audience the token gets from the client's configuration: a value holding markup or a control character (the token endpoint reads at most 20 values of at most 256 characters, which the step carries), the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step cannot carry the client's audience, at the finish of an interaction
Refuse a redirect URI the consent step cannot describe, at /oauth/authorize
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, clientDisplay cannot describe where redirect_uri goes: an IPv6 address that is not loopback, a host that is not a DNS name of valid labels, or a scheme over 64 characters, which only a client registered before #593 holds, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the display cannot describe where the redirect URI goes, at /oauth/authorize
Refuse a redirect URI the consent step cannot describe, at the finish of an interaction
no redirect, page shown
At the finish of another interaction of the request: a sign-in, or a consent whose client changed whether it is an agent, clientDisplay cannot describe where redirect_uri goes: an IPv6 address that is not loopback, a host that is not a DNS name of valid labels, or a scheme over 64 characters, which only a client registered before #593 holds, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the display cannot describe where the redirect URI goes, at the finish of an interaction
Refuse a request too large to hold in an interaction, at /oauth/authorize
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: it fails, because the request is over 16 KiB, or the record over 32 KiB
Refuse a next interaction too large to hold
no redirect, page shown
At the finish of another interaction of the request: a sign-in, or a consent whose client changed whether it is an agent, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: it fails, because the record of the next interaction is over 32 KiB
Fail the request when the interaction store fails, at /oauth/authorize
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: it fails, because the interaction store fails
Refuse an answer the consent step's grammar does not accept
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, the answer does not pass the step grammar: no decision, another decision, a remember that is no boolean, or a member beyond type, csrf, decision and remember, such as a scope or a resource, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer does not pass the step grammar
Refuse an answer the expiry sweep beat
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: it fails, because the expiry sweep claimed the interaction first
Refuse an answer whose browser session ended since the step was shown
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: it fails, because the session the consent was for has ended, such as by a sign-out in another tab
Refuse an answer whose client or redirect URI no longer passes
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, client_id names no client any more, or the redirect URI was removed from it or is no longer permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the client, or its redirect URI, no longer passes
Refuse an answer whose client may no longer use the endpoint
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client was disabled or suspended, or lost the authorization_code grant or the code response type, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the client may no longer use the endpoint
Fail the answer when the interaction store fails
no redirect, page shown
At the finish of another interaction of the request: a sign-in, or a consent whose client changed whether it is an agent, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: it fails, because the interaction store fails, at the finish of an interaction
Fail the answer when the mint transaction fails
no redirect, page shown
At /oauth/authorize, for a client an operator provisioned: the system client or the seed manifest, the client display describes the redirect URI, and the step can carry the scopes, resources and audience, the client is not an agent when the step is built, decision allow, with remember false or absent, the client still resolves and may use the endpoint, its redirect URI is still registered and permitted, every check passes, and nothing asks for a fresh sign-in or the sign-in is fresh enough for what asks
  1. Wait for a request that needs consent: decideAuthorization answers consent for a signed-in browser session (core.authorization, consentNeeded): at /oauth/authorize, or at the finish of a sign-in interaction of the request
  2. Describe the client from the server's own records: the client is described
  3. Build the consent step from the request: the step can show the request
  4. Start the consent interaction: the interaction is stored
  5. Send the browser to the ceremony page of the consent interaction: the ceremony page reads the step by the interaction id
  6. Return the consent step: the person allows or denies, with the CSRF value of this view; the answer takes the turn
  7. Check the answer against the consent step's grammar: the answer passes the grammar
  8. End the interaction before anything is decided: the interaction is over
  9. Check the request again, as the endpoint checks it: the request still passes, or only its dpop_jkt is refused
  10. Read the decision: the person allowed
  11. Find the session again, with its account: the session is live and its account may use it
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Refuse a QAuth API audience the client no longer lists: every QAuth API audience among the resources is one the client lists
  14. Check that the client is still what the step said: the client is what the step said
  15. Grant only the scopes the step showed, while the client may still have them: the shown scopes are still grantable
  16. Grant only the resources the step showed: the shown resources are the request's
  17. Ask for a fresh sign-in when the grant needs one: the sign-in is fresh enough
  18. Mint the authorization code, in the mint transaction: it fails, because the database fails, or three codes in a row collide; nothing is written

Each row is also a generated test: set the starting situation, send the events, expect the ending.

What is guaranteed

These hold in all 32808 situations the model can reach, from 5280 different starting situations.

Exact rules
IdNever
step-only-for-a-request-it-can-showinteractionOpen and startGate != 'shown'
an-agent-is-named-as-one(state == 'showingStep' and agentClient) or (state == 'showingStepForAgent' and not agentClient)
no-code-without-the-finish(codeWritten or grantWritten or state == 'code_issued') and (interactionOpen or not finished)
no-answer-field-reaches-the-grant(codeWritten or grantWritten or state == 'code_issued') and malformedAnswer
only-the-shown-set-is-granted(codeWritten or grantWritten or state == 'code_issued') and (scopesNarrowed or resourcesDiffer)
no-code-on-a-stale-sign-in(codeWritten or grantWritten or state == 'code_issued') and needsFreshLogin
dangerous-scope-signs-in-afresh-firstallowGate == 'dangerousScope' and state in ['mintingCode', 'savingConsent', 'recordingGrant', 'code_issued']
no-code-above-the-agent-cap(codeWritten or grantWritten or state == 'code_issued') and overAgentCap
no-code-for-a-changed-client(codeWritten or grantWritten or state == 'code_issued') and (recheck != 'unchanged' or agentChanged)
deny-grants-nothingdenied and (codeWritten or grantWritten or state in ['code_issued', 'sign_in_continues.fresh_login', 'sign_in_continues.account_disabled', 'sign_in_continues.session_ended', 'decidingAgain'])
access-denied-only-for-a-denialstate == 'access_denied.user_denied' and not denied
remembered-only-when-askedgrantWritten and not remember
grant-only-in-the-code-transactiongrantWritten and not codeWritten
code-issued-means-committed-and-remembered-as-askedstate == 'code_issued' and (not committed or not codeWritten or (remember and not grantWritten))
nothing-left-after-a-failed-mintstate in ['server_error.store_at_finish', 'sign_in_continues.session_ended'] and (codeWritten or grantWritten)

Reference

All transitions (69)
FromEventGuardToEffectsTaken
idleSTARTdescribingClientyes
describingClientrule 1redirectUndescribable and atFinishinvalid_request.redirect_uri_not_shown_at_finishyes
describingClientrule 2redirectUndescribableinvalid_request.redirect_uri_not_shownyes
describingClientrule 3buildingStepyes
buildingSteprule 1scopesUnshowable and atFinishinvalid_scope.consent_not_shown_at_finishyes
buildingSteprule 2scopesUnshowable and operatorClientinvalid_scope.consent_not_shownyes
buildingSteprule 3scopesUnshowableinvalid_scope.consent_not_shown_shownyes
buildingSteprule 4resourcesUnshowable and atFinishinvalid_target.consent_not_shown_at_finishyes
buildingSteprule 5resourcesUnshowable and operatorClientinvalid_target.consent_not_shownyes
buildingSteprule 6resourcesUnshowableinvalid_target.consent_not_shown_shownyes
buildingSteprule 7audienceUnshowable and atFinishinvalid_target.audience_not_shown_at_finishyes
buildingSteprule 8audienceUnshowable and operatorClientinvalid_target.audience_not_shownyes
buildingSteprule 9audienceUnshowableinvalid_target.audience_not_shown_shownyes
buildingSteprule 10startingConsentyes
startingConsentDONEatConsentCeremonyopenInteractionyes
startingConsentFAIL.tooLargeatFinishinvalid_request.authorization_too_large_at_finishyes
startingConsentFAIL.tooLargeinvalid_request.authorization_too_largeyes
startingConsentFAIL.serveratFinishserver_error.interaction_store_at_finishyes
startingConsentFAIL.serverserver_error.interaction_storeyes
atConsentCeremonyREADagentClientshowingStepForAgentyes
atConsentCeremonyREADshowingStepyes
showingStepREAD_PERSONALshowingPersonalyes
showingStepANSWERcheckingAnsweryes
showingStepForAgentREAD_PERSONALshowingPersonalyes
showingStepForAgentANSWERcheckingAnsweryes
showingPersonalREADagentClientshowingStepForAgentyes
showingPersonalREADshowingStepyes
showingPersonalANSWERcheckingAnsweryes
checkingAnswerrule 1malformedAnswerinvalid_answer.grammaryes
checkingAnswerrule 2takingFinishyes
takingFinishDONErecheckingRequestfinishInteractionyes
takingFinishFAIL.expiredinteraction_expired.finished_lateyes
takingFinishFAIL.sessionEndedinteraction_expired.session_endedyes
takingFinishFAIL.serverserver_error.interaction_store_at_finishyes
recheckingRequestrule 1clientGoneinvalid_request.client_changedyes
recheckingRequestrule 2clientUnusableunauthorized_client.client_changedyes
recheckingRequestrule 3readingDecisionyes
readingDecisionrule 1deniedaccess_denied.user_deniedyes
readingDecisionrule 2dpopRefusedinvalid_request.dpop_jkt_unreadableyes
readingDecisionrule 3findingSessionyes
findingSessionrule 1accountDisabledcontinuingToSignInforDisabledAccountyes
findingSessionrule 2checkingAgentCapyes
checkingAgentCaprule 1overAgentCapinvalid_scope.agent_scope_mode_exceededyes
checkingAgentCaprule 2checkingAudienceyes
checkingAudiencerule 1audienceUnlistedinvalid_target.unlisted_reserved_audienceyes
checkingAudiencerule 2checkingAgentKindyes
checkingAgentKindrule 1agentChanged and agentClientdecidingAgainceasedAgentyes
checkingAgentKindrule 2agentChangeddecidingAgainbecameAgentyes
checkingAgentKindrule 3checkingScopesyes
decidingAgainrule 1describingClientnextInteractionyes
checkingScopesrule 1scopesNarrowedinvalid_scope.consent_scope_mismatchyes
checkingScopesrule 2checkingResourcesyes
checkingResourcesrule 1resourcesDifferinvalid_target.consent_resource_mismatchyes
checkingResourcesrule 2checkingStepUpyes
checkingStepUprule 1needsFreshLogincontinuingToSignInforFreshLoginyes
checkingStepUprule 2mintingCodeyes
mintingCodeDONEremembersavingConsentwriteCodeyes
mintingCodeDONErecordingGrantwriteCodeyes
mintingCodeFAIL.sessionEndedcontinuingToSignInforEndedSessionyes
mintingCodeFAIL.serverserver_error.store_at_finishyes
savingConsentDONErecordingGrantwriteGrantyes
savingConsentFAIL.serverserver_error.store_at_finishrollBackyes
recordingGrantDONEcode_issuedcommityes
recordingGrantFAIL.serverserver_error.store_at_finishrollBackyes
continuingToSignInDONEsignInCause == 'freshLogin'sign_in_continues.fresh_loginyes
continuingToSignInDONEsignInCause == 'accountDisabled'sign_in_continues.account_disabledyes
continuingToSignInDONEsign_in_continues.session_endedyes
continuingToSignInFAIL.tooLargeinvalid_request.authorization_too_large_at_finishyes
continuingToSignInFAIL.serverserver_error.interaction_store_at_finishyes