captured core.consent-page

Ask the user to approve a client's access

/oauth/authorize sends a signed-in user here when no stored consent covers the request, when the client is an AI agent, which never skips the screen, or when prompt=consent asks for the screen. An operator-flagged first-party client (ADR-017 Decision 9) needs no stored consent there: its administrative consent covers its scopes, so it is sent here only for a dangerous scope or prompt=consent. The GET checks the client and its redirect URI again, refuses a QAuth API audience the client does not list, binds the scopes and resource indicators it is about to show to the session, and renders a page with a CSRF value; for an agent client the page also names it as an AI agent and says how long it may keep working. The POST checks that value, burns it, checks the client again and acts on the decision: deny sends access_denied back to the client; allow refuses a scope above the agent cap, a scope or resource set other than the one the page showed, and a sign-in too old for what is asked (for a native_backend client, any sign-in older than two minutes), then saves the consent when the user ticked "Remember this decision", mints an authorization code and sends it back.

What this view shows

GET and POST /ui/consent from the request to the response, in the order the server runs its checks.

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: The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is 1, as the ticked "Remember this decision" box sends it.

  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client is enabled and not suspended
  7. Check the redirect URI against the client and its environment: the redirect URI is registered and permitted
  8. Read the DPoP key the code is to be bound to: dpop_jkt is absent, unread or well formed
  9. Refuse an agent scope above the client's cap: no agent scope is above the cap
  10. Describe the client from the server's own records: the client is described; the scopes to show are the requested ones the client's allowlist holds
  11. Refuse a QAuth API audience the client does not list: every QAuth API audience among the resources is one the client lists
  12. Bind the shown scopes and resources, and the CSRF value, to the session: the binding is written
  13. Show the consent page: the user presses Allow or Deny, and the browser posts the form
  14. Find the realm of the host: the request has its realm
  15. Count the request against the authorize limit: the address is under its limit
  16. Check the form against the consent form schema: the form matches the schema
  17. Find the browser session: the session is live
  18. Check the CSRF value against the session's: the values are equal
  19. Burn the CSRF value: the value is burned
  20. Find the client again: the client is enabled and not suspended
  21. Check the redirect URI against the client and its environment again: the redirect URI is registered and permitted
  22. Read the DPoP key from the form: dpop_jkt is absent, unread or well formed
  23. Read the decision: the user pressed Allow
  24. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  25. Grant only the scopes the page showed: the posted scopes are the shown ones
  26. Grant only the resources the page named: the posted resources are the named ones
  27. Ask for a fresh sign-in when the grant needs one: the sign-in is fresh enough, and the user ticked "Remember this decision"
  28. Save the consent: the consent is saved
  29. Mint the authorization code: the code is stored
  30. Record the grant: the events are recorded

Result: Send the authorization code to the client.

The flow

GET /ui/consent arrives, with the parameters of /oauth/authorize POST /ui/consent arrives for a page this walk did not render the host names no enabled realm with an issuer the host is one of the realm's endpoint hosts the request has its realm the address has used up its requests the address is under its limit the query does not match the schema the query matches the schema the session cookie cannot be decoded the request carries no session; the authorize URL is rebuilt from the query the cookie does not verify, or names a session that has ended, and is cleared the session's user is gone or disabled, or was disabled after the session began; the session is ended and its cookie cleared the session is live the client_id names no client of the realm the client is disabled or suspended the client is enabled and not suspended redirect_uri is not registered for the client no environment permits that redirect_uri the redirect URI is registered and permitted dpop_jkt is not one thumbprint dpop_jkt is absent, unread or well formed an agent scope is above the cap, for a client no operator provisioned an agent scope is above the cap, for a client an operator provisioned no agent scope is above the cap the client display data cannot show where the redirect URI goes the client is described; the scopes to show are the requested ones the client's allowlist holds a resource names a QAuth API audience the client does not list, for a client no operator provisioned a resource names a QAuth API audience the client does not list, for a client an operator provisioned every QAuth API audience among the resources is one the client lists the binding is written and the client is an agent; a development client's page is marked for the relaxed style policy as well the binding is written, and the page is marked for the relaxed style policy of a development client the binding is written the user presses Allow or Deny, and the browser posts the form the user presses Allow or Deny, and the browser posts the form the host names no enabled realm with an issuer the host is one of the realm's endpoint hosts the request has its realm the address has used up its authorize requests the address is under its limit the form does not match the schema the form matches the schema the session cookie cannot be decoded the request carries no session; the authorize URL is rebuilt from the form the cookie does not verify, or names a session that has ended, and is cleared the session's user is gone or disabled, or was disabled after the session began; the session is ended and its cookie cleared the session is live the posted value is not the session's live one the values are equal the value is burned the client_id names no client of the realm the client is disabled or suspended the client is enabled and not suspended redirect_uri is not registered for the client no environment permits that redirect_uri the redirect URI is registered and permitted dpop_jkt is not one thumbprint dpop_jkt is absent, unread or well formed the user pressed Deny; consent.denied is recorded the user pressed Allow an agent scope is above the cap; consent.denied is recorded no agent scope is above the cap the session holds no scope set for this client; consent.denied is recorded the posted scopes are not the ones the page showed; consent.denied is recorded the posted scopes are the shown ones the posted resources are not the ones the page named; consent.denied is recorded the posted resources are the named ones the sign-in is too old for what is asked; step_up.required is recorded the sign-in is fresh enough, and the user ticked "Remember this decision" the sign-in is fresh enough, and the approval is for this request only the consent is saved the code is stored the events are recorded the rebuilt request is over 16 KiB the request is parked, for a request with no session the request is parked, for a stale cookie the request is parked, for a disabled user's session the request is parked, for a step-up start idle Wait for a consent request idle · server step The realm resolver's onRequest hook, before any route code. /ui/consent is a browser route, so it answers on the realm's primary host only. A request refused here is never counted by the rate limiter. Find the realm of the host showResolvingRealm · server step The GET has no limit of its own: RATE_LIMIT_MAX requests per RATE_LIMIT_WINDOW seconds, for each client address and realm, shared with every route that sets none. Count the request against the global limit showRateLimiting · server step The same schema as /oauth/authorize (authorizeQuerySchema in apps/auth-server/src/app/schemas/oauth.ts). Check the query against the authorize schema showValidatingQuery · server step Without a session the authorize URL is rebuilt from the query and parked server-side, and the browser is sent to sign in with a short handle that /ui/resume swaps back for it. Find the browser session showFindingSession · server step A pre-registered client first; otherwise a URL client_id is resolved as a CIMD document, and a materialised one is resolved again. A client_id that names no client and a client that cannot be used are refused apart. Find the client showCheckingClient · server step An exact match against the registered URIs (a loopback URI may differ only in its port), then the environment gate, so the page is never shown for a redirect the POST would refuse. Check the redirect URI against the client and its environment showCheckingRedirectUri · server step RFC 9449 §10. Read only while DPOP_ENABLED is on; /oauth/authorize refused a malformed value already. Read the DPoP key the code is to be bound to showReadingDpopKey · server step ADR-007 §2. On the raw scope parameter, before the allowlist filter. The user has seen no page yet, so the error of a client no operator provisioned is shown rather than redirected (RFC 9700 §4.11.2). Refuse an agent scope above the client's cap showCheckingAgentCap · server step ADR-019 Decision 6 (#593). What the page shows first is what the server verified: the host of a CIMD client's client id URL, or the domain an admin set on a client the operator provisioned; any other client is unverified. Then where the code goes, a loopback address as "this device", and only then the name the client declared, stripped and capped. A redirect the display data cannot show refuses the screen before anything is bound. Describe the client from the server's own records showDescribingClient · server step #509. A urn:qauth:aud: audience among the request's resource indicators must be one the client's own audience lists, and a client that registered itself through open registration or CIMD lists none. /oauth/authorize refuses such a request before it redirects here; this refuses one that comes straight to the screen, so no consent is shown, bound or stored for an audience the token endpoint would refuse. The user has seen no page yet, so the error of a client no operator provisioned is shown rather than redirected (RFC 9700 §4.11.2). Refuse a QAuth API audience the client does not list showCheckingAudience · server step The scopes are the request's, filtered by the client's allowlist; the resources are the request's resource indicators. Both are written to the session's side record under the client_id, with the session's CSRF value, which is reused when it exists (a second tab or a reload keeps the first tab's form valid) and minted otherwise. The side record expires with the session; rendering never extends it. Bind the shown scopes and resources, and the CSRF value, to the session bindingScopes · server step The page of a client that is not an agent. It leads with the client's verified domain (its registrable domain in bold, a shared-hosting suffix flagged) or says the app is unverified, then names where the code goes ("this device", with a warning, for a loopback address), the name the client declared, marked as declared, a badge for a client registered dynamically in the last DYNAMIC_CLIENT_BADGE_DAYS days, the signed-in identifier, the scopes and their descriptions and the audience (the resources, else the client's audience). No logo or link the client supplied appears. Its form posts the scopes the page lists, never the raw request, so an allowlist widened after this render cannot add a scope the user did not see. Show the consent page rendered · server step ADR-014 §11 (#845). The same page as rendered, with two lines after the signed-in identifier, in QAuth's words and never the client's: the client named as an AI agent ("This app", never the name it declared), and how long it may keep working once allowed. That is the end of its refresh-token family (#552): with no sso_max_lifespan on the realm, until the user revokes the consent; with one, no later than the family's start plus the limit and the end of the browser session behind the code, which ends the limit after the sign-in. The realm's limit is read from the request's realm row as the page renders. An access token already issued is not cut short by either end, and the family needs a stored consent (out of view: /oauth/token). /oauth/authorize sends an agent here even when a stored consent covers the request (ADR-015 Decision 11). Show the consent page for an AI agent renderedForAgent · server step The realm resolver's onRequest hook, as for the GET. Find the realm of the host submitResolvingRealm · server step AUTHORIZE_RATE_LIMIT requests per AUTHORIZE_RATE_WINDOW seconds, for each client address and realm. Count the request against the authorize limit submitRateLimiting · server step consentFormSchema in apps/auth-server/src/app/schemas/consent.ts: the authorize schema's parameters with the same bounds, code_challenge's RFC 7636 character set among them, plus decision, allow_forever and csrf_token. Check the form against the consent form schema submitValidatingForm · server step Without a session the authorize URL is rebuilt from the form, not from a /ui/consent URL, so that /oauth/authorize checks the client again after the sign-in. That rebuild leaves out the form's resource fields, so the resumed request names no resource indicator; the step-up redirect below keeps them. Find the browser session submitFindingSession · server step A timing-safe comparison with the value in the session's side record. It runs before the client_id is read, so the refusal records request.csrf_rejected with no client. Check the CSRF value against the session's checkingCsrf · server step Deleted from the side record before anything else is checked, so the same form cannot be posted twice, whatever this POST ends in. The next GET mints a new value. The scope bindings stay. Burn the CSRF value burningCsrf · server step submitCheckingClient Find the client again submitCheckingClient · server step submitCheckingRedirectUri Check the redirect URI against the client and its environment again submitCheckingRedirectUri · server step A malformed value was never rendered, so it is refused. Read the DPoP key from the form submitReadingDpopKey · server step Deny is answered before any scope is looked at, so a denial with a tampered scope field is a denial. Read the decision readingDecision · server step ADR-007 §2. The posted scope field is the user's browser's to change, so the cap the GET applied is applied again. The user has seen the destination host on the page, so the error is redirected for every client. Refuse an agent scope above the client's cap, where the code is minted submitCheckingAgentCap · server step The posted scope, filtered by the client's allowlist as it is now, must equal the set the session bound for this client, in any order. The bound set, never the posted one, is what is granted. Grant only the scopes the page showed checkingScopeBinding · server step RFC 8707. The posted resource fields must name the bound resources, in any order and ignoring repeats. A session rendered before resources were bound reads as having named none. Grant only the resources the page named checkingResourceBinding · server step ADR-007 §2. Every scope about to be granted is treated as new, so a prior consent cannot skip the gate. prompt=login and a dangerous scope ask for a sign-in within the last two minutes (STEP_UP_FRESH_AUTH_WINDOW_MS); max_age asks for one within that many whole seconds. A development client is spared only the dangerous-scope rule. A native_backend client's request asks for a fresh sign-in as prompt=login does, whatever the form says (ADR-017 Decision 9), so a page reached without /oauth/authorize cannot mint a code for its browser leg on a sign-in older than two minutes. The authorize URL is rebuilt from the form with the bound resources and parked, and the browser is sent to sign in. Ask for a fresh sign-in when the grant needs one checkingStepUp · server step Merged into the user's active consent for the client, or created: the shown scopes, and those scopes for each named resource. Only a saved consent that covers a code's scopes at each of its resources lets /oauth/token mint a refresh token for it. A saved consent also lets /oauth/authorize skip this screen next time. A first-party client's administrative consent (ADR-017 Decision 9) skips it without one, for a request with no dangerous scope and no prompt=consent; such a code gets a refresh token only when a saved consent covers it all the same. Save the consent savingConsent · server step Through mintAuthorizationCode (apps/auth-server/src/app/helpers/authorization-code.ts), the one place every code is minted: 32 random bytes, bound to the client, the user, the redirect URI, the PKCE challenge, the nonce, the shown scopes and resources, the DPoP key, the session's sign-in time, assurance level and end, for AUTHORIZATION_CODE_TTL_MS (five minutes). It refuses, before it writes anything, a challenge that is not S256 and 43 to 128 unreserved characters; the form schema holds code_challenge to the same rule, so the POST never reaches that refusal. A code that collides with a stored one is drawn again, three attempts in all. Mint the authorization code mintingCode · server step step_up.elevated when a dangerous scope is granted, with the highest agent:* mode among the granted scopes for an agent client, then consent.granted with the scopes and whether they were remembered. A failed write fails the request after the code is minted. Record the grant recordingGrant · server step Every sign-in redirect of this flow runs here: the rebuilt authorize URL is checked against 16 KiB, stored under a single-use handle for ten minutes, and the browser is sent to /ui/login with /ui/resume/<handle> as its return_to. A stale cookie was already cleared, and a disabled user's session already ended, before this step. Park the authorization request for the sign-in parkingRequest · server step code_issued Send the authorization code to the client redirect with code + state + iss access_denied.user_denied Tell the client the user denied the request redirect, error=access_denied Reached from the GET for a client an operator provisioned (helpers/authorization-error.ts), and from the POST for every client, where consent.denied is recorded first. The two answers are the same. Tell the client an agent scope is above its cap redirect, error=invalid_scope invalid_scope.agent_scope_mode_exceeded_shown Show, rather than redirect, the agent-cap error of a client no operator provisioned no redirect, page shown invalid_scope.consent_scope_mismatch Refuse a scope set the page did not show redirect, error=invalid_scope invalid_target.consent_resource_mismatch Refuse a resource set the page did not name redirect, error=invalid_target Reached from the GET only, for a client an operator provisioned (helpers/authorization-error.ts). Tell the client it named a QAuth API audience it does not list redirect, error=invalid_target invalid_target.unlisted_reserved_audience_shown Show, rather than redirect, the unlisted-audience error of a client no operator provisioned no redirect, page shown sign_in_required.no_session Send the browser to sign in, then back to the authorization request no redirect, page shown sign_in_required.stale_session Clear the stale session cookie and send the browser to sign in no redirect, page shown The session is revoked and its side record deleted before the cookie is cleared. End the session of a disabled or deleted user and send the browser to sign in no redirect, page shown /ui/login mints a new session, so the resumed request is evaluated with a fresh sign-in time. Send the browser to sign in again before the grant no redirect, page shown A stale cookie is cleared in the same response. Refuse a request too large to park for the sign-in no redirect, page shown invalid_csrf_token.mismatch Refuse a form whose CSRF value is not the session's no redirect, page shown Shown, not redirected, because no redirect URI is trusted yet. Refuse a client_id that names no client no redirect, page shown Shown, not redirected, because no redirect URI is trusted yet. A suspended client reads as disabled. Refuse a client that is disabled or suspended no redirect, page shown invalid_request.redirect_uri_not_registered Refuse a redirect URI the client did not register no redirect, page shown invalid_request.redirect_uri_not_permitted Refuse a registered redirect URI that no environment permits no redirect, page shown invalid_request.malformed_dpop_jkt Refuse a dpop_jkt that is not a key thumbprint no redirect, page shown invalid_request.redirect_uri_not_shown Refuse a redirect URI the screen cannot show no redirect, page shown validation_error.query Refuse a query that does not match the authorize schema no redirect, page shown validation_error.form Refuse a form that does not match the consent form schema no redirect, page shown misdirected_request.host Refuse a host that serves no browser route no redirect, page shown too_many_requests.global_limit Refuse a GET over the global limit no redirect, page shown too_many_requests.authorize_limit Refuse a POST over the authorize limit no redirect, page shown The URIError reaches the error handler as an unknown error, logged at error level. The body says nothing of the failure, in any NODE_ENV; its trace_id finds the log line. Fail on a session cookie that cannot be decoded no redirect, page shown
GET /ui/consent arrives, with the parameters of /oauth/authorize POST /ui/consent arrives for a page this walk did not render the host names no enabled realm with an issuer the host is one of the realm's endpoint hosts the request has its realm the address has used up its requests the address is under its limit the query does not match the schema the query matches the schema the session cookie cannot be decoded the request carries no session; the authorize URL is rebuilt from the query the cookie does not verify, or names a session that has ended, and is cleared the session's user is gone or disabled, or was disabled after the session began; the session is ended and its cookie cleared the session is live the client_id names no client of the realm the client is disabled or suspended the client is enabled and not suspended redirect_uri is not registered for the client no environment permits that redirect_uri the redirect URI is registered and permitted dpop_jkt is not one thumbprint dpop_jkt is absent, unread or well formed an agent scope is above the cap, for a client no operator provisioned an agent scope is above the cap, for a client an operator provisioned no agent scope is above the cap the client display data cannot show where the redirect URI goes the client is described; the scopes to show are the requested ones the client's allowlist holds a resource names a QAuth API audience the client does not list, for a client no operator provisioned a resource names a QAuth API audience the client does not list, for a client an operator provisioned every QAuth API audience among the resources is one the client lists the binding is written and the client is an agent; a development client's page is marked for the relaxed style policy as well the binding is written, and the page is marked for the relaxed style policy of a development client the binding is written fails: the cache fails the user presses Allow or Deny, and the browser posts the form the user presses Allow or Deny, and the browser posts the form the host names no enabled realm with an issuer the host is one of the realm's endpoint hosts the request has its realm the address has used up its authorize requests the address is under its limit the form does not match the schema the form matches the schema the session cookie cannot be decoded the request carries no session; the authorize URL is rebuilt from the form the cookie does not verify, or names a session that has ended, and is cleared the session's user is gone or disabled, or was disabled after the session began; the session is ended and its cookie cleared the session is live the posted value is not the session's live one the values are equal the value is burned fails: the cache fails the client_id names no client of the realm the client is disabled or suspended the client is enabled and not suspended redirect_uri is not registered for the client no environment permits that redirect_uri the redirect URI is registered and permitted dpop_jkt is not one thumbprint dpop_jkt is absent, unread or well formed the user pressed Deny; consent.denied is recorded the user pressed Allow an agent scope is above the cap; consent.denied is recorded no agent scope is above the cap the session holds no scope set for this client; consent.denied is recorded the posted scopes are not the ones the page showed; consent.denied is recorded the posted scopes are the shown ones the posted resources are not the ones the page named; consent.denied is recorded the posted resources are the named ones the sign-in is too old for what is asked; step_up.required is recorded the sign-in is fresh enough, and the user ticked "Remember this decision" the sign-in is fresh enough, and the approval is for this request only the consent is saved fails: the database fails the code is stored fails: the database fails, or three codes in a row collide the events are recorded fails: the event store fails the rebuilt request is over 16 KiB the request is parked, for a request with no session the request is parked, for a stale cookie the request is parked, for a disabled user's session the request is parked, for a step-up start idle Wait for a consent request idle · server step The realm resolver's onRequest hook, before any route code. /ui/consent is a browser route, so it answers on the realm's primary host only. A request refused here is never counted by the rate limiter. Find the realm of the host showResolvingRealm · server step The GET has no limit of its own: RATE_LIMIT_MAX requests per RATE_LIMIT_WINDOW seconds, for each client address and realm, shared with every route that sets none. Count the request against the global limit showRateLimiting · server step The same schema as /oauth/authorize (authorizeQuerySchema in apps/auth-server/src/app/schemas/oauth.ts). Check the query against the authorize schema showValidatingQuery · server step Without a session the authorize URL is rebuilt from the query and parked server-side, and the browser is sent to sign in with a short handle that /ui/resume swaps back for it. Find the browser session showFindingSession · server step A pre-registered client first; otherwise a URL client_id is resolved as a CIMD document, and a materialised one is resolved again. A client_id that names no client and a client that cannot be used are refused apart. Find the client showCheckingClient · server step An exact match against the registered URIs (a loopback URI may differ only in its port), then the environment gate, so the page is never shown for a redirect the POST would refuse. Check the redirect URI against the client and its environment showCheckingRedirectUri · server step RFC 9449 §10. Read only while DPOP_ENABLED is on; /oauth/authorize refused a malformed value already. Read the DPoP key the code is to be bound to showReadingDpopKey · server step ADR-007 §2. On the raw scope parameter, before the allowlist filter. The user has seen no page yet, so the error of a client no operator provisioned is shown rather than redirected (RFC 9700 §4.11.2). Refuse an agent scope above the client's cap showCheckingAgentCap · server step ADR-019 Decision 6 (#593). What the page shows first is what the server verified: the host of a CIMD client's client id URL, or the domain an admin set on a client the operator provisioned; any other client is unverified. Then where the code goes, a loopback address as "this device", and only then the name the client declared, stripped and capped. A redirect the display data cannot show refuses the screen before anything is bound. Describe the client from the server's own records showDescribingClient · server step #509. A urn:qauth:aud: audience among the request's resource indicators must be one the client's own audience lists, and a client that registered itself through open registration or CIMD lists none. /oauth/authorize refuses such a request before it redirects here; this refuses one that comes straight to the screen, so no consent is shown, bound or stored for an audience the token endpoint would refuse. The user has seen no page yet, so the error of a client no operator provisioned is shown rather than redirected (RFC 9700 §4.11.2). Refuse a QAuth API audience the client does not list showCheckingAudience · server step The scopes are the request's, filtered by the client's allowlist; the resources are the request's resource indicators. Both are written to the session's side record under the client_id, with the session's CSRF value, which is reused when it exists (a second tab or a reload keeps the first tab's form valid) and minted otherwise. The side record expires with the session; rendering never extends it. Bind the shown scopes and resources, and the CSRF value, to the session bindingScopes · server step The page of a client that is not an agent. It leads with the client's verified domain (its registrable domain in bold, a shared-hosting suffix flagged) or says the app is unverified, then names where the code goes ("this device", with a warning, for a loopback address), the name the client declared, marked as declared, a badge for a client registered dynamically in the last DYNAMIC_CLIENT_BADGE_DAYS days, the signed-in identifier, the scopes and their descriptions and the audience (the resources, else the client's audience). No logo or link the client supplied appears. Its form posts the scopes the page lists, never the raw request, so an allowlist widened after this render cannot add a scope the user did not see. Show the consent page rendered · server step ADR-014 §11 (#845). The same page as rendered, with two lines after the signed-in identifier, in QAuth's words and never the client's: the client named as an AI agent ("This app", never the name it declared), and how long it may keep working once allowed. That is the end of its refresh-token family (#552): with no sso_max_lifespan on the realm, until the user revokes the consent; with one, no later than the family's start plus the limit and the end of the browser session behind the code, which ends the limit after the sign-in. The realm's limit is read from the request's realm row as the page renders. An access token already issued is not cut short by either end, and the family needs a stored consent (out of view: /oauth/token). /oauth/authorize sends an agent here even when a stored consent covers the request (ADR-015 Decision 11). Show the consent page for an AI agent renderedForAgent · server step The realm resolver's onRequest hook, as for the GET. Find the realm of the host submitResolvingRealm · server step AUTHORIZE_RATE_LIMIT requests per AUTHORIZE_RATE_WINDOW seconds, for each client address and realm. Count the request against the authorize limit submitRateLimiting · server step consentFormSchema in apps/auth-server/src/app/schemas/consent.ts: the authorize schema's parameters with the same bounds, code_challenge's RFC 7636 character set among them, plus decision, allow_forever and csrf_token. Check the form against the consent form schema submitValidatingForm · server step Without a session the authorize URL is rebuilt from the form, not from a /ui/consent URL, so that /oauth/authorize checks the client again after the sign-in. That rebuild leaves out the form's resource fields, so the resumed request names no resource indicator; the step-up redirect below keeps them. Find the browser session submitFindingSession · server step A timing-safe comparison with the value in the session's side record. It runs before the client_id is read, so the refusal records request.csrf_rejected with no client. Check the CSRF value against the session's checkingCsrf · server step Deleted from the side record before anything else is checked, so the same form cannot be posted twice, whatever this POST ends in. The next GET mints a new value. The scope bindings stay. Burn the CSRF value burningCsrf · server step submitCheckingClient Find the client again submitCheckingClient · server step submitCheckingRedirectUri Check the redirect URI against the client and its environment again submitCheckingRedirectUri · server step A malformed value was never rendered, so it is refused. Read the DPoP key from the form submitReadingDpopKey · server step Deny is answered before any scope is looked at, so a denial with a tampered scope field is a denial. Read the decision readingDecision · server step ADR-007 §2. The posted scope field is the user's browser's to change, so the cap the GET applied is applied again. The user has seen the destination host on the page, so the error is redirected for every client. Refuse an agent scope above the client's cap, where the code is minted submitCheckingAgentCap · server step The posted scope, filtered by the client's allowlist as it is now, must equal the set the session bound for this client, in any order. The bound set, never the posted one, is what is granted. Grant only the scopes the page showed checkingScopeBinding · server step RFC 8707. The posted resource fields must name the bound resources, in any order and ignoring repeats. A session rendered before resources were bound reads as having named none. Grant only the resources the page named checkingResourceBinding · server step ADR-007 §2. Every scope about to be granted is treated as new, so a prior consent cannot skip the gate. prompt=login and a dangerous scope ask for a sign-in within the last two minutes (STEP_UP_FRESH_AUTH_WINDOW_MS); max_age asks for one within that many whole seconds. A development client is spared only the dangerous-scope rule. A native_backend client's request asks for a fresh sign-in as prompt=login does, whatever the form says (ADR-017 Decision 9), so a page reached without /oauth/authorize cannot mint a code for its browser leg on a sign-in older than two minutes. The authorize URL is rebuilt from the form with the bound resources and parked, and the browser is sent to sign in. Ask for a fresh sign-in when the grant needs one checkingStepUp · server step Merged into the user's active consent for the client, or created: the shown scopes, and those scopes for each named resource. Only a saved consent that covers a code's scopes at each of its resources lets /oauth/token mint a refresh token for it. A saved consent also lets /oauth/authorize skip this screen next time. A first-party client's administrative consent (ADR-017 Decision 9) skips it without one, for a request with no dangerous scope and no prompt=consent; such a code gets a refresh token only when a saved consent covers it all the same. Save the consent savingConsent · server step Through mintAuthorizationCode (apps/auth-server/src/app/helpers/authorization-code.ts), the one place every code is minted: 32 random bytes, bound to the client, the user, the redirect URI, the PKCE challenge, the nonce, the shown scopes and resources, the DPoP key, the session's sign-in time, assurance level and end, for AUTHORIZATION_CODE_TTL_MS (five minutes). It refuses, before it writes anything, a challenge that is not S256 and 43 to 128 unreserved characters; the form schema holds code_challenge to the same rule, so the POST never reaches that refusal. A code that collides with a stored one is drawn again, three attempts in all. Mint the authorization code mintingCode · server step step_up.elevated when a dangerous scope is granted, with the highest agent:* mode among the granted scopes for an agent client, then consent.granted with the scopes and whether they were remembered. A failed write fails the request after the code is minted. Record the grant recordingGrant · server step Every sign-in redirect of this flow runs here: the rebuilt authorize URL is checked against 16 KiB, stored under a single-use handle for ten minutes, and the browser is sent to /ui/login with /ui/resume/<handle> as its return_to. A stale cookie was already cleared, and a disabled user's session already ended, before this step. Park the authorization request for the sign-in parkingRequest · server step code_issued Send the authorization code to the client redirect with code + state + iss access_denied.user_denied Tell the client the user denied the request redirect, error=access_denied Reached from the GET for a client an operator provisioned (helpers/authorization-error.ts), and from the POST for every client, where consent.denied is recorded first. The two answers are the same. Tell the client an agent scope is above its cap redirect, error=invalid_scope invalid_scope.agent_scope_mode_exceeded_shown Show, rather than redirect, the agent-cap error of a client no operator provisioned no redirect, page shown invalid_scope.consent_scope_mismatch Refuse a scope set the page did not show redirect, error=invalid_scope invalid_target.consent_resource_mismatch Refuse a resource set the page did not name redirect, error=invalid_target Reached from the GET only, for a client an operator provisioned (helpers/authorization-error.ts). Tell the client it named a QAuth API audience it does not list redirect, error=invalid_target invalid_target.unlisted_reserved_audience_shown Show, rather than redirect, the unlisted-audience error of a client no operator provisioned no redirect, page shown sign_in_required.no_session Send the browser to sign in, then back to the authorization request no redirect, page shown sign_in_required.stale_session Clear the stale session cookie and send the browser to sign in no redirect, page shown The session is revoked and its side record deleted before the cookie is cleared. End the session of a disabled or deleted user and send the browser to sign in no redirect, page shown /ui/login mints a new session, so the resumed request is evaluated with a fresh sign-in time. Send the browser to sign in again before the grant no redirect, page shown A stale cookie is cleared in the same response. Refuse a request too large to park for the sign-in no redirect, page shown invalid_csrf_token.mismatch Refuse a form whose CSRF value is not the session's no redirect, page shown Shown, not redirected, because no redirect URI is trusted yet. Refuse a client_id that names no client no redirect, page shown Shown, not redirected, because no redirect URI is trusted yet. A suspended client reads as disabled. Refuse a client that is disabled or suspended no redirect, page shown invalid_request.redirect_uri_not_registered Refuse a redirect URI the client did not register no redirect, page shown invalid_request.redirect_uri_not_permitted Refuse a registered redirect URI that no environment permits no redirect, page shown invalid_request.malformed_dpop_jkt Refuse a dpop_jkt that is not a key thumbprint no redirect, page shown invalid_request.redirect_uri_not_shown Refuse a redirect URI the screen cannot show no redirect, page shown validation_error.query Refuse a query that does not match the authorize schema no redirect, page shown validation_error.form Refuse a form that does not match the consent form schema no redirect, page shown misdirected_request.host Refuse a host that serves no browser route no redirect, page shown too_many_requests.global_limit Refuse a GET over the global limit no redirect, page shown too_many_requests.authorize_limit Refuse a POST over the authorize limit no redirect, page shown The URIError reaches the error handler as an unknown error, logged at error level. The body says nothing of the failure, in any NODE_ENV; its trace_id finds the log line. Fail on a session cookie that cannot be decoded no redirect, page shown The steps before the failing one stay done. The body says nothing of the failure, in any NODE_ENV; its trace_id finds the log line. Fail on a store error 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
Show the consent page
rendered
200 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store","Content-Security-Policy":"the global policy, whose style-src carries this response's nonce; for a development client \"default-src 'self'; base-uri 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'\""} · body: {"form":{"method":"post","action":"/ui/consent","fields":{"csrf_token":"the session's CSRF value (hidden)","allow_forever":"1, ticked by default","decision":"deny or allow, the two submit buttons","client_id":"the request's (hidden)","redirect_uri":"the request's (hidden)","scope":"the scopes the page lists, joined by spaces (hidden)","resource":"one hidden field per resource indicator","state":"the request's, when it has one (hidden)","nonce":"the request's, when it has one (hidden)","code_challenge":"the request's (hidden)","code_challenge_method":"S256 (hidden)","response_type":"code (hidden)","prompt":"the request's, when it has one (hidden)","max_age":"the request's, when it has one (hidden)","dpop_jkt":"the bound key, while DPoP is on and the request named one (hidden)"}}}
Show the consent page for an AI agent
renderedForAgent
200 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store","Content-Security-Policy":"as for rendered"} · body: {"agentLine":"\"This app is an AI agent.\"","persistenceLine":"\"It may keep working until you revoke it.\" when the realm sets no sso_max_lifespan; when it sets one, \"It may keep working until you revoke it, or until <the limit, an exact duration such as 1 hour or 1 day and 12 hours> after you signed in, whichever comes first.\"","form":"the same form as rendered"}
Send the authorization code to the client
code_issued
302 · headers: {"Location":"redirect_uri with code (43 base64url characters), state when the form had one, and iss"}
Tell the client the user denied the request
access_denied.user_denied
302 · headers: {"Location":"redirect_uri with error=access_denied, error_description=User denied the authorization request., state when the form had one, and iss"}
Tell the client an agent scope is above its cap
invalid_scope.agent_scope_mode_exceeded
302 · headers: {"Location":"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"}
Show, rather than redirect, the agent-cap error of a client no operator provisioned
invalid_scope.agent_scope_mode_exceeded_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 invalid_scope 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
Refuse a scope set the page did not show
invalid_scope.consent_scope_mismatch
302 · headers: {"Location":"redirect_uri with error=invalid_scope, error_description=consent scope does not match the rendered authorization request, state when the form had one, and iss"}
Refuse a resource set the page did not name
invalid_target.consent_resource_mismatch
302 · headers: {"Location":"redirect_uri with error=invalid_target, error_description=consent resource does not match the rendered authorization request, state when the form had one, and iss"}
Tell the client it named a QAuth API audience it does not list
invalid_target.unlisted_reserved_audience
302 · headers: {"Location":"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"}
Show, rather than redirect, the unlisted-audience error of a client no operator provisioned
invalid_target.unlisted_reserved_audience_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 invalid_target 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
Send the browser to sign in, then back to the authorization request
sign_in_required.no_session
302 · headers: {"Location":"/ui/login?return_to=%2Fui%2Fresume%2F<handle>, the handle 43 base64url characters"}
Clear the stale session cookie and send the browser to sign in
sign_in_required.stale_session
302 · headers: {"Set-Cookie":"__Host-qauth_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0, with Secure when SESSION_COOKIE_SECURE is on","Location":"/ui/login?return_to=%2Fui%2Fresume%2F<handle>"}
End the session of a disabled or deleted user and send the browser to sign in
sign_in_required.account_disabled
302 · headers: {"Set-Cookie":"__Host-qauth_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0, with Secure when SESSION_COOKIE_SECURE is on","Location":"/ui/login?return_to=%2Fui%2Fresume%2F<handle>"}
Send the browser to sign in again before the grant
sign_in_required.fresh_login
302 · headers: {"Location":"/ui/login?return_to=%2Fui%2Fresume%2F<handle>"}
Refuse a request too large to park for the sign-in
invalid_request.authorization_too_large
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-request","title":"Invalid request","status":400,"detail":"authorization request is too large","instance":"/ui/consent","code":"invalid_request","trace_id":"the request id"}
Refuse a form whose CSRF value is not the session's
invalid_csrf_token.mismatch
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-csrf-token","title":"Invalid CSRF token","status":400,"detail":"The CSRF token is missing or does not match the session","instance":"/ui/consent","code":"invalid_csrf_token","trace_id":"the request id"}
Refuse a client_id that names no client
invalid_request.unknown_client
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-request","title":"Invalid request","status":400,"detail":"client_id does not name a client of this realm","instance":"/ui/consent","code":"invalid_request","trace_id":"the request id"}
Refuse a client that is disabled or suspended
unauthorized_client.disabled
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#unauthorized-client","title":"Unauthorized client","status":400,"detail":"client is disabled","instance":"/ui/consent","code":"unauthorized_client","trace_id":"the request id"}
Refuse a redirect URI the client did not register
invalid_request.redirect_uri_not_registered
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-request","title":"Invalid request","status":400,"detail":"redirect_uri not registered","instance":"/ui/consent","code":"invalid_request","trace_id":"the request id"}
Refuse a registered redirect URI that no environment permits
invalid_request.redirect_uri_not_permitted
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-request","title":"Invalid request","status":400,"detail":"redirect_uri not permitted for this environment","instance":"/ui/consent","code":"invalid_request","trace_id":"the request id"}
Refuse a dpop_jkt that is not a key thumbprint
invalid_request.malformed_dpop_jkt
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-request","title":"Invalid request","status":400,"detail":"dpop_jkt must be the base64url SHA-256 JWK thumbprint of a DPoP key","instance":"/ui/consent","code":"invalid_request","trace_id":"the request id"}
Refuse a redirect URI the screen cannot show
invalid_request.redirect_uri_not_shown
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-request","title":"Invalid request","status":400,"detail":"redirect_uri cannot be shown","instance":"/ui/consent","code":"invalid_request","trace_id":"the request id"}
Refuse a query that does not match the authorize schema
validation_error.query
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#validation-error","title":"Validation error","status":400,"detail":"The request is not valid.","instance":"/ui/consent","code":"validation_error","trace_id":"the request id","errors":[{"pointer":"/<the parameter>, such as /code_challenge","message":"the schema's message"}]}
Refuse a form that does not match the consent form schema
validation_error.form
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#validation-error","title":"Validation error","status":400,"detail":"The request is not valid.","instance":"/ui/consent","code":"validation_error","trace_id":"the request id","errors":[{"pointer":"/<the field>, such as /decision","message":"the schema's message"}]}
Refuse a host that serves no browser route
misdirected_request.host
421 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#misdirected-request","title":"Misdirected request","status":421,"detail":"This server does not answer for the requested host","instance":"/ui/consent","code":"misdirected_request","trace_id":"the request id"}
Refuse a GET over the global limit
too_many_requests.global_limit
429 · headers: {"x-ratelimit-limit":"RATE_LIMIT_MAX","x-ratelimit-remaining":"0","x-ratelimit-reset":"the seconds until the window resets","retry-after":"the same seconds"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#too-many-requests","title":"Too many requests","status":429,"detail":"Rate limit exceeded, retry in {the rest of the window, such as 1 minute}","instance":"/ui/consent","code":"too_many_requests","trace_id":"the request id"}
Refuse a POST over the authorize limit
too_many_requests.authorize_limit
429 · headers: {"x-ratelimit-limit":"AUTHORIZE_RATE_LIMIT","x-ratelimit-remaining":"0","x-ratelimit-reset":"the seconds until the window resets","retry-after":"the same seconds"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#too-many-requests","title":"Too many requests","status":429,"detail":"Rate limit exceeded, retry in {the rest of the window, such as 1 minute}","instance":"/ui/consent","code":"too_many_requests","trace_id":"the request id"}
Fail on a session cookie that cannot be decoded
server_error.malformed_cookie
500 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","title":"Internal error","status":500,"instance":"/ui/consent","code":"internal_error","trace_id":"the request id"}
Fail on a store error
server_error.store
500 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","title":"Internal error","status":500,"instance":"/ui/consent","code":"internal_error","trace_id":"the request id"}

How it decides what happens next

Find the realm of the host

#IfThen
1the host names no enabled realm with an issuerRefuse a host that serves no browser route
2the host is one of the realm's endpoint hostsRefuse a host that serves no browser route
3the request has its realmCount the request against the global limit

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1hostUnknownmisdirected_request.host64512 of 580608
2endpointHostmisdirected_request.host64512 of 580608
3otherwiseshowRateLimiting451584 of 580608
hostUnknown
gate == 'unknownHost'
endpointHost
gate == 'endpointHost'

Count the request against the global limit

#IfThen
1the address has used up its requestsRefuse a GET over the global limit
2the address is under its limitCheck the query against the authorize schema

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1rateLimitedtoo_many_requests.global_limit64512 of 451584
2otherwiseshowValidatingQuery387072 of 451584
rateLimited
gate == 'rateLimited'

Check the query against the authorize schema

#IfThen
1the query does not match the schemaRefuse a query that does not match the authorize schema
2the query matches the schemaFind the browser session

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1inputInvalidvalidation_error.query64512 of 387072
2otherwiseshowFindingSession322560 of 387072
inputInvalid
gate == 'invalidInput'

Find the browser session

#IfThen
1the session cookie cannot be decodedFail on a session cookie that cannot be decoded
2the request carries no session; the authorize URL is rebuilt from the queryPark the authorization request for the sign-in
3the cookie does not verify, or names a session that has ended, and is clearedPark the authorization request for the sign-in
4the session's user is gone or disabled, or was disabled after the session began; the session is ended and its cookie clearedPark the authorization request for the sign-in
5the session is liveFind the client

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1cookieMalformedserver_error.malformed_cookie64512 of 322560
2noSessionparkingRequest64512 of 322560
3staleSessionparkingRequest64512 of 322560
4accountDisabledparkingRequest64512 of 322560
5otherwiseshowCheckingClient64512 of 322560
cookieMalformed
gate == 'malformedCookie'
noSession
gate == 'noSession'
staleSession
gate == 'staleSession'
accountDisabled
gate == 'accountDisabled'

Find the client

#IfThen
1the client_id names no client of the realmRefuse a client_id that names no client
2the client is disabled or suspendedRefuse a client that is disabled or suspended
3the client is enabled and not suspendedCheck the redirect URI against the client and its environment

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1clientUnknowninvalid_request.unknown_client9216 of 64512
2clientUnusableunauthorized_client.disabled9216 of 64512
3otherwiseshowCheckingRedirectUri46080 of 64512
clientUnknown
params == 'unknownClient'
clientUnusable
params == 'unusableClient'

Check the redirect URI against the client and its environment

#IfThen
1redirect_uri is not registered for the clientRefuse a redirect URI the client did not register
2no environment permits that redirect_uriRefuse a registered redirect URI that no environment permits
3the redirect URI is registered and permittedRead the DPoP key the code is to be bound to

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1redirectUnregisteredinvalid_request.redirect_uri_not_registered9216 of 46080
2redirectNotPermittedinvalid_request.redirect_uri_not_permitted9216 of 46080
3otherwiseshowReadingDpopKey27648 of 46080
redirectUnregistered
params == 'unregisteredRedirect'
redirectNotPermitted
params == 'unpermittedRedirect'

Read the DPoP key the code is to be bound to

#IfThen
1dpop_jkt is not one thumbprintRefuse a dpop_jkt that is not a key thumbprint
2dpop_jkt is absent, unread or well formedRefuse an agent scope above the client's cap

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1dpopJktMalformedinvalid_request.malformed_dpop_jkt9216 of 27648
2otherwiseshowCheckingAgentCap18432 of 27648
dpopJktMalformed
params == 'malformedDpopJkt'

Refuse an agent scope above the client's cap

#IfThen
1an agent scope is above the cap, for a client no operator provisionedShow, rather than redirect, the agent-cap error of a client no operator provisioned
2an agent scope is above the cap, for a client an operator provisionedTell the client an agent scope is above its cap
3no agent scope is above the capDescribe the client from the server's own records

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1overAgentCapShowninvalid_scope.agent_scope_mode_exceeded_shown6144 of 18432
2overAgentCapinvalid_scope.agent_scope_mode_exceeded6144 of 18432
3otherwiseshowDescribingClient6144 of 18432
overAgentCap
agentScope != 'withinCap'
overAgentCapShown
agentScope == 'overCapSelfService'

Describe the client from the server's own records

#IfThen
1the client display data cannot show where the redirect URI goesRefuse a redirect URI the screen cannot show
2the client is described; the scopes to show are the requested ones the client's allowlist holdsRefuse a QAuth API audience the client does not list

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1redirectNotShowninvalid_request.redirect_uri_not_shown3072 of 6144
2otherwiseshowCheckingAudience3072 of 6144
redirectNotShown
params == 'unshownRedirect'

Refuse a QAuth API audience the client does not list

#IfThen
1a resource names a QAuth API audience the client does not list, for a client no operator provisionedShow, rather than redirect, the unlisted-audience error of a client no operator provisioned
2a resource names a QAuth API audience the client does not list, for a client an operator provisionedTell the client it named a QAuth API audience it does not list
3every QAuth API audience among the resources is one the client listsBind the shown scopes and resources, and the CSRF value, to the session

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1audienceUnlistedShowninvalid_target.unlisted_reserved_audience_shown1024 of 3072
2audienceUnlistedinvalid_target.unlisted_reserved_audience1024 of 3072
3otherwisebindingScopes1024 of 3072
audienceUnlisted
audience != 'listed'
audienceUnlistedShown
audience == 'unlistedSelfService'

Find the realm of the host

#IfThen
1the host names no enabled realm with an issuerRefuse a host that serves no browser route
2the host is one of the realm's endpoint hostsRefuse a host that serves no browser route
3the request has its realmCount the request against the authorize limit

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1hostUnknownmisdirected_request.host64512 of 580608
2endpointHostmisdirected_request.host64512 of 580608
3otherwisesubmitRateLimiting451584 of 580608
hostUnknown
gate == 'unknownHost'
endpointHost
gate == 'endpointHost'

Count the request against the authorize limit

#IfThen
1the address has used up its authorize requestsRefuse a POST over the authorize limit
2the address is under its limitCheck the form against the consent form schema

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1rateLimitedtoo_many_requests.authorize_limit64512 of 451584
2otherwisesubmitValidatingForm387072 of 451584
rateLimited
gate == 'rateLimited'

Check the form against the consent form schema

#IfThen
1the form does not match the schemaRefuse a form that does not match the consent form schema
2the form matches the schemaFind the browser session

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1inputInvalidvalidation_error.form64512 of 387072
2otherwisesubmitFindingSession322560 of 387072
inputInvalid
gate == 'invalidInput'

Find the browser session

#IfThen
1the session cookie cannot be decodedFail on a session cookie that cannot be decoded
2the request carries no session; the authorize URL is rebuilt from the formPark the authorization request for the sign-in
3the cookie does not verify, or names a session that has ended, and is clearedPark the authorization request for the sign-in
4the session's user is gone or disabled, or was disabled after the session began; the session is ended and its cookie clearedPark the authorization request for the sign-in
5the session is liveCheck the CSRF value against the session's

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1cookieMalformedserver_error.malformed_cookie64512 of 322560
2noSessionparkingRequest64512 of 322560
3staleSessionparkingRequest64512 of 322560
4accountDisabledparkingRequest64512 of 322560
5otherwisecheckingCsrf64512 of 322560
cookieMalformed
gate == 'malformedCookie'
noSession
gate == 'noSession'
staleSession
gate == 'staleSession'
accountDisabled
gate == 'accountDisabled'

Check the CSRF value against the session's

#IfThen
1the posted value is not the session's live oneRefuse a form whose CSRF value is not the session's
2the values are equalBurn the CSRF value

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1csrfWronginvalid_csrf_token.mismatch32256 of 64512
2otherwiseburningCsrf32256 of 64512
csrfWrong
csrf == 'wrong'

Find the client again

#IfThen
1the client_id names no client of the realmRefuse a client_id that names no client
2the client is disabled or suspendedRefuse a client that is disabled or suspended
3the client is enabled and not suspendedCheck the redirect URI against the client and its environment again

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1clientUnknowninvalid_request.unknown_client4608 of 32256
2clientUnusableunauthorized_client.disabled4608 of 32256
3otherwisesubmitCheckingRedirectUri23040 of 32256
clientUnknown
params == 'unknownClient'
clientUnusable
params == 'unusableClient'

Check the redirect URI against the client and its environment again

#IfThen
1redirect_uri is not registered for the clientRefuse a redirect URI the client did not register
2no environment permits that redirect_uriRefuse a registered redirect URI that no environment permits
3the redirect URI is registered and permittedRead the DPoP key from the form

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1redirectUnregisteredinvalid_request.redirect_uri_not_registered4608 of 23040
2redirectNotPermittedinvalid_request.redirect_uri_not_permitted4608 of 23040
3otherwisesubmitReadingDpopKey13824 of 23040
redirectUnregistered
params == 'unregisteredRedirect'
redirectNotPermitted
params == 'unpermittedRedirect'

Read the DPoP key from the form

#IfThen
1dpop_jkt is not one thumbprintRefuse a dpop_jkt that is not a key thumbprint
2dpop_jkt is absent, unread or well formedRead the decision

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1dpopJktMalformedinvalid_request.malformed_dpop_jkt4608 of 13824
2otherwisereadingDecision9216 of 13824
dpopJktMalformed
params == 'malformedDpopJkt'

Read the decision

#IfThen
1the user pressed Deny; consent.denied is recordedTell the client the user denied the request
2the user pressed AllowRefuse an agent scope above the client's cap, where the code is minted

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1deniedaccess_denied.user_denied4608 of 9216
2otherwisesubmitCheckingAgentCap4608 of 9216
denied
decision == 'deny'

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

#IfThen
1an agent scope is above the cap; consent.denied is recordedTell the client an agent scope is above its cap
2no agent scope is above the capGrant only the scopes the page showed

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1overAgentCapinvalid_scope.agent_scope_mode_exceeded3072 of 4608
2otherwisecheckingScopeBinding1536 of 4608
overAgentCap
agentScope != 'withinCap'

Grant only the scopes the page showed

#IfThen
1the session holds no scope set for this client; consent.denied is recordedRefuse a scope set the page did not show
2the posted scopes are not the ones the page showed; consent.denied is recordedRefuse a scope set the page did not show
3the posted scopes are the shown onesGrant only the resources the page named

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1scopesUnboundinvalid_scope.consent_scope_mismatch384 of 1536
2scopesDifferinvalid_scope.consent_scope_mismatch384 of 1536
3otherwisecheckingResourceBinding768 of 1536
scopesUnbound
posted == 'unboundScopes'
scopesDiffer
posted == 'otherScopes'

Grant only the resources the page named

#IfThen
1the posted resources are not the ones the page named; consent.denied is recordedRefuse a resource set the page did not name
2the posted resources are the named onesAsk for a fresh sign-in when the grant needs one

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1resourcesDifferinvalid_target.consent_resource_mismatch384 of 768
2otherwisecheckingStepUp384 of 768
resourcesDiffer
posted == 'otherResources'

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 is recordedPark the authorization request for the sign-in
2the sign-in is fresh enough, and the user ticked "Remember this decision"Save the consent
3the sign-in is fresh enough, and the approval is for this request onlyMint the authorization code

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1needsFreshLoginparkingRequest240 of 384
2remembersavingConsent72 of 384
3otherwisemintingCode72 of 384
needsFreshLogin
stepUp in ['promptLogin', 'maxAgeExceeded'] or (stepUp == 'dangerousScope' and not developmentClient)
remember
allowForever

Park the authorization request for the sign-in

#IfThen
1the rebuilt request is over 16 KiBRefuse a request too large to park for the sign-in
2the request is parked, for a request with no sessionSend the browser to sign in, then back to the authorization request
3the request is parked, for a stale cookieClear the stale session cookie and send the browser to sign in
4the request is parked, for a disabled user's sessionEnd the session of a disabled or deleted user and send the browser to sign in
5the request is parked, for a step-upSend the browser to sign in again before the grant

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1requestTooLargeinvalid_request.authorization_too_large96888 of 193776
2noSessionsign_in_required.no_session32256 of 193776
3staleSessionsign_in_required.stale_session32256 of 193776
4accountDisabledsign_in_required.account_disabled32256 of 193776
5otherwisesign_in_required.fresh_login120 of 193776
noSession
gate == 'noSession'
staleSession
gate == 'staleSession'
accountDisabled
gate == 'accountDisabled'
requestTooLarge
returnUrl == 'tooLarge'

Every way it can end

It ends withWhenWhat happens
Send the authorization code to the client
redirect with code + state + iss
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: POST /ui/consent arrives for a page this walk did not render
  2. Find the realm of the host: the request has its realm
  3. Count the request against the authorize limit: the address is under its limit
  4. Check the form against the consent form schema: the form matches the schema
  5. Find the browser session: the session is live
  6. Check the CSRF value against the session's: the values are equal
  7. Burn the CSRF value: the value is burned
  8. Find the client again: the client is enabled and not suspended
  9. Check the redirect URI against the client and its environment again: the redirect URI is registered and permitted
  10. Read the DPoP key from the form: dpop_jkt is absent, unread or well formed
  11. Read the decision: the user pressed Allow
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Grant only the scopes the page showed: the posted scopes are the shown ones
  14. Grant only the resources the page named: the posted resources are the named ones
  15. Ask for a fresh sign-in when the grant needs one: the sign-in is fresh enough, and the approval is for this request only
  16. Mint the authorization code: the code is stored
  17. Record the grant: the events are recorded
Tell the client the user denied the request
redirect, error=access_denied
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Deny, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: POST /ui/consent arrives for a page this walk did not render
  2. Find the realm of the host: the request has its realm
  3. Count the request against the authorize limit: the address is under its limit
  4. Check the form against the consent form schema: the form matches the schema
  5. Find the browser session: the session is live
  6. Check the CSRF value against the session's: the values are equal
  7. Burn the CSRF value: the value is burned
  8. Find the client again: the client is enabled and not suspended
  9. Check the redirect URI against the client and its environment again: the redirect URI is registered and permitted
  10. Read the DPoP key from the form: dpop_jkt is absent, unread or well formed
  11. Read the decision: the user pressed Deny; consent.denied is recorded
Tell the client an agent scope is above its cap
redirect, error=invalid_scope
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, it names one (any agent:* scope for a client that is no agent or has no cap, or a mode above its cap), and an operator provisioned the client: the system client or the seed manifest, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client is enabled and not suspended
  7. Check the redirect URI against the client and its environment: the redirect URI is registered and permitted
  8. Read the DPoP key the code is to be bound to: dpop_jkt is absent, unread or well formed
  9. Refuse an agent scope above the client's cap: an agent scope is above the cap, for a client an operator provisioned
Show, rather than redirect, the agent-cap error of a client no operator provisioned
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, it names one, and the client registered itself: open registration, CIMD or a developer account, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client is enabled and not suspended
  7. Check the redirect URI against the client and its environment: the redirect URI is registered and permitted
  8. Read the DPoP key the code is to be bound to: dpop_jkt is absent, unread or well formed
  9. Refuse an agent scope above the client's cap: an agent scope is above the cap, for a client no operator provisioned
Refuse a scope set the page did not show
redirect, error=invalid_scope
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scopes are another set, such as a tampered hidden field, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: POST /ui/consent arrives for a page this walk did not render
  2. Find the realm of the host: the request has its realm
  3. Count the request against the authorize limit: the address is under its limit
  4. Check the form against the consent form schema: the form matches the schema
  5. Find the browser session: the session is live
  6. Check the CSRF value against the session's: the values are equal
  7. Burn the CSRF value: the value is burned
  8. Find the client again: the client is enabled and not suspended
  9. Check the redirect URI against the client and its environment again: the redirect URI is registered and permitted
  10. Read the DPoP key from the form: dpop_jkt is absent, unread or well formed
  11. Read the decision: the user pressed Allow
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Grant only the scopes the page showed: the posted scopes are not the ones the page showed; consent.denied is recorded
Refuse a resource set the page did not name
redirect, error=invalid_target
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the scopes match, but the resource fields name other resources, or none where the page named some, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: POST /ui/consent arrives for a page this walk did not render
  2. Find the realm of the host: the request has its realm
  3. Count the request against the authorize limit: the address is under its limit
  4. Check the form against the consent form schema: the form matches the schema
  5. Find the browser session: the session is live
  6. Check the CSRF value against the session's: the values are equal
  7. Burn the CSRF value: the value is burned
  8. Find the client again: the client is enabled and not suspended
  9. Check the redirect URI against the client and its environment again: the redirect URI is registered and permitted
  10. Read the DPoP key from the form: dpop_jkt is absent, unread or well formed
  11. Read the decision: the user pressed Allow
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Grant only the scopes the page showed: the posted scopes are the shown ones
  14. Grant only the resources the page named: the posted resources are not the ones the page named; consent.denied is recorded
Tell the client it named a QAuth API audience it does not list
redirect, error=invalid_target
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, they name one, and an operator provisioned the client: the system client or the seed manifest, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client is enabled and not suspended
  7. Check the redirect URI against the client and its environment: the redirect URI is registered and permitted
  8. Read the DPoP key the code is to be bound to: dpop_jkt is absent, unread or well formed
  9. Refuse an agent scope above the client's cap: no agent scope is above the cap
  10. Describe the client from the server's own records: the client is described; the scopes to show are the requested ones the client's allowlist holds
  11. Refuse a QAuth API audience the client does not list: a resource names a QAuth API audience the client does not list, for a client an operator provisioned
Show, rather than redirect, the unlisted-audience error of a client no operator provisioned
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, they name one, and the client registered itself: open registration, CIMD or a developer account; an open registration or CIMD client lists none, whatever its row holds, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client is enabled and not suspended
  7. Check the redirect URI against the client and its environment: the redirect URI is registered and permitted
  8. Read the DPoP key the code is to be bound to: dpop_jkt is absent, unread or well formed
  9. Refuse an agent scope above the client's cap: no agent scope is above the cap
  10. Describe the client from the server's own records: the client is described; the scopes to show are the requested ones the client's allowlist holds
  11. Refuse a QAuth API audience the client does not list: a resource names a QAuth API audience the client does not list, for a client no operator provisioned
Send the browser to sign in, then back to the authorization request
no redirect, page shown
The request carries no session cookie, or one that names a cookie key this process has not read yet and may not read again yet (#493), which is left for the next request, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the request carries no session; the authorize URL is rebuilt from the query
  6. Park the authorization request for the sign-in: the request is parked, for a request with no session
Clear the stale session cookie and send the browser to sign in
no redirect, page shown
The cookie does not verify with the realm's cookie keys (#493), or it does but its session has ended or expired, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the cookie does not verify, or names a session that has ended, and is cleared
  6. Park the authorization request for the sign-in: the request is parked, for a stale cookie
End the session of a disabled or deleted user and send the browser to sign in
no redirect, page shown
The session is live, but its user has been deleted or disabled, or was disabled after the session began and enabled again since, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session's user is gone or disabled, or was disabled after the session began; the session is ended and its cookie cleared
  6. Park the authorization request for the sign-in: the request is parked, for a disabled user's session
Send the browser to sign in again before the grant
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, the form carries prompt=login, or the client's first-party profile is native_backend, whose request asks for a fresh sign-in as prompt=login does whatever the form says (ADR-017 Decision 9), and the sign-in is more than two minutes old, allow_forever is absent or any other value
  1. Wait for a consent request: POST /ui/consent arrives for a page this walk did not render
  2. Find the realm of the host: the request has its realm
  3. Count the request against the authorize limit: the address is under its limit
  4. Check the form against the consent form schema: the form matches the schema
  5. Find the browser session: the session is live
  6. Check the CSRF value against the session's: the values are equal
  7. Burn the CSRF value: the value is burned
  8. Find the client again: the client is enabled and not suspended
  9. Check the redirect URI against the client and its environment again: the redirect URI is registered and permitted
  10. Read the DPoP key from the form: dpop_jkt is absent, unread or well formed
  11. Read the decision: the user pressed Allow
  12. Refuse an agent scope above the client's cap, where the code is minted: no agent scope is above the cap
  13. Grant only the scopes the page showed: the posted scopes are the shown ones
  14. Grant only the resources the page named: the posted resources are the named ones
  15. Ask for a fresh sign-in when the grant needs one: the sign-in is too old for what is asked; step_up.required is recorded
  16. Park the authorization request for the sign-in: the request is parked, for a step-up
Refuse a request too large to park for the sign-in
no redirect, page shown
The request carries no session cookie, or one that names a cookie key this process has not read yet and may not read again yet (#493), which is left for the next request, it is longer: the rebuild encodes each ~ ! ' ( ) of the parameters as three bytes, so a request line of some 8 KiB can give 24 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the request carries no session; the authorize URL is rebuilt from the query
  6. Park the authorization request for the sign-in: the rebuilt request is over 16 KiB
Refuse a form whose CSRF value is not the session's
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, it is another value, one an earlier POST of the session burned, or the session holds none; a form with no csrf_token fails the schema instead, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: POST /ui/consent arrives for a page this walk did not render
  2. Find the realm of the host: the request has its realm
  3. Count the request against the authorize limit: the address is under its limit
  4. Check the form against the consent form schema: the form matches the schema
  5. Find the browser session: the session is live
  6. Check the CSRF value against the session's: the posted value is not the session's live one
Refuse a client_id that names no client
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names no client of the realm, or a CIMD document that does not resolve, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client_id names no client of the realm
Refuse a client that is disabled or suspended
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a client that is disabled or suspended, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client is disabled or suspended
Refuse a redirect URI the client did not register
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, redirect_uri is none of the client's, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client is enabled and not suspended
  7. Check the redirect URI against the client and its environment: redirect_uri is not registered for the client
Refuse a registered redirect URI that no environment permits
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, redirect_uri is the client's, but no environment permits it: a scheme outside the allowed ones, or plain http to a host that is not loopback, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client is enabled and not suspended
  7. Check the redirect URI against the client and its environment: no environment permits that redirect_uri
Refuse a dpop_jkt that is not a key thumbprint
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, DPoP is on and dpop_jkt is not one thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client is enabled and not suspended
  7. Check the redirect URI against the client and its environment: the redirect URI is registered and permitted
  8. Read the DPoP key the code is to be bound to: dpop_jkt is not one thumbprint
Refuse a redirect URI the screen cannot show
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, every other parameter is valid, but the client display data (#593) cannot show 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. Every registration path refuses such a URI since #593, so only an older client holds one; read by the GET only, after the agent cap, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session is live
  6. Find the client: the client is enabled and not suspended
  7. Check the redirect URI against the client and its environment: the redirect URI is registered and permitted
  8. Read the DPoP key the code is to be bound to: dpop_jkt is absent, unread or well formed
  9. Refuse an agent scope above the client's cap: no agent scope is above the cap
  10. Describe the client from the server's own records: the client display data cannot show where the redirect URI goes
Refuse a query that does not match the authorize schema
no redirect, page shown
The query (GET) or the form (POST) does not pass the route schema, such as a missing code_challenge or decision, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query does not match the schema
Refuse a form that does not match the consent form schema
no redirect, page shown
The query (GET) or the form (POST) does not pass the route schema, such as a missing code_challenge or decision, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: POST /ui/consent arrives for a page this walk did not render
  2. Find the realm of the host: the request has its realm
  3. Count the request against the authorize limit: the address is under its limit
  4. Check the form against the consent form schema: the form does not match the schema
Refuse a host that serves no browser route
no redirect, page shown
The host names no realm, a disabled realm or a realm with no issuer, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the host names no enabled realm with an issuer
Refuse a GET over the global limit
no redirect, page shown
The address has used up its requests for the window: the global limit for the GET, the authorize limit for the POST, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address has used up its requests
Refuse a POST over the authorize limit
no redirect, page shown
The address has used up its requests for the window: the global limit for the GET, the authorize limit for the POST, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: POST /ui/consent arrives for a page this walk did not render
  2. Find the realm of the host: the request has its realm
  3. Count the request against the authorize limit: the address has used up its authorize requests
Fail on a session cookie that cannot be decoded
no redirect, page shown
The request carries a session cookie whose value is not valid percent-encoding, such as %zz, which the cookie reader decodes without a catch, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: GET /ui/consent arrives, with the parameters of /oauth/authorize
  2. Find the realm of the host: the request has its realm
  3. Count the request against the global limit: the address is under its limit
  4. Check the query against the authorize schema: the query matches the schema
  5. Find the browser session: the session cookie cannot be decoded
Fail on a store error
no redirect, page shown
The host is the primary host of an enabled realm, the address is under its limit, the query or form passes the route schema, and the request carries a live browser session of an enabled user, the authorization request, rebuilt for a sign-in redirect, is at most 16 KiB, client_id names a usable client, redirect_uri is one of its registered URIs that the environment permits, and dpop_jkt is absent, unread (DPoP off) or one base64url SHA-256 thumbprint, the scope parameter names no agent:* scope above the agent mode cap the operator set for the client, the resource indicators name no QAuth API audience (urn:qauth:aud:) that the client's own audience does not list, the client is not an agent: its is_agent is false, the client resolves to the production or staging profile, the posted csrf_token is the session's live value, the user pressed Allow, the posted scope, filtered by the client's allowlist, is the set the session bound for this client, and the posted resource fields name the bound resources, nothing asks for a fresh sign-in, or the sign-in is fresh enough for what asks, allow_forever is absent or any other value
  1. Wait for a consent request: POST /ui/consent arrives for a page this walk did not render
  2. Find the realm of the host: the request has its realm
  3. Count the request against the authorize limit: the address is under its limit
  4. Check the form against the consent form schema: the form matches the schema
  5. Find the browser session: the session is live
  6. Check the CSRF value against the session's: the values are equal
  7. Burn the CSRF value: it fails, because the cache fails

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

What is guaranteed

These hold in all 5414976 situations the model can reach, from 580608 different starting situations.

Exact rules
IdNever
page-only-for-a-request-the-post-acceptsstate in ['rendered', 'renderedForAgent'] and (gate != 'open' or params != 'valid' or overAgentCap)
no-page-for-an-unlisted-reserved-audiencestate in ['bindingScopes', 'rendered', 'renderedForAgent'] and audienceUnlisted
no-code-without-the-live-csrf-valuestate in ['savingConsent', 'mintingCode', 'recordingGrant', 'code_issued'] and csrfWrong
csrf-value-spent-before-the-client-is-readstate in ['submitCheckingClient', 'submitCheckingRedirectUri', 'submitReadingDpopKey', 'readingDecision', 'submitCheckingAgentCap', 'checkingScopeBinding', 'checkingResourceBinding', 'checkingStepUp', 'savingConsent', 'mintingCode', 'recordingGrant', 'code_issued'] and not csrfBurned
only-the-shown-scopes-and-resources-are-grantedstate in ['savingConsent', 'mintingCode', 'recordingGrant', 'code_issued'] and posted != 'shown'
deny-mints-nothing(state in ['savingConsent', 'mintingCode', 'recordingGrant', 'code_issued'] or consentSaved or codeMinted) and denied
no-code-above-the-agent-capstate in ['savingConsent', 'mintingCode', 'recordingGrant', 'code_issued'] and overAgentCap
no-code-on-a-stale-sign-instate in ['savingConsent', 'mintingCode', 'recordingGrant', 'code_issued'] and needsFreshLogin
remembered-only-when-askedconsentSaved and not allowForever
code-issued-means-minted-and-remembered-as-askedstate == 'code_issued' and (not codeMinted or (allowForever and not consentSaved))
an-agent-is-named-as-one(state == 'rendered' and agentClient) or (state == 'renderedForAgent' and not agentClient)

Reference

All transitions (83)
FromEventGuardToEffectsTaken
idleSHOWshowResolvingRealmyes
idleSUBMITsubmitResolvingRealmyes
showResolvingRealmrule 1hostUnknownmisdirected_request.hostyes
showResolvingRealmrule 2endpointHostmisdirected_request.hostyes
showResolvingRealmrule 3showRateLimitingyes
showRateLimitingrule 1rateLimitedtoo_many_requests.global_limityes
showRateLimitingrule 2showValidatingQueryyes
showValidatingQueryrule 1inputInvalidvalidation_error.queryyes
showValidatingQueryrule 2showFindingSessionyes
showFindingSessionrule 1cookieMalformedserver_error.malformed_cookieyes
showFindingSessionrule 2noSessionparkingRequestyes
showFindingSessionrule 3staleSessionparkingRequestyes
showFindingSessionrule 4accountDisabledparkingRequestyes
showFindingSessionrule 5showCheckingClientyes
showCheckingClientrule 1clientUnknowninvalid_request.unknown_clientyes
showCheckingClientrule 2clientUnusableunauthorized_client.disabledyes
showCheckingClientrule 3showCheckingRedirectUriyes
showCheckingRedirectUrirule 1redirectUnregisteredinvalid_request.redirect_uri_not_registeredyes
showCheckingRedirectUrirule 2redirectNotPermittedinvalid_request.redirect_uri_not_permittedyes
showCheckingRedirectUrirule 3showReadingDpopKeyyes
showReadingDpopKeyrule 1dpopJktMalformedinvalid_request.malformed_dpop_jktyes
showReadingDpopKeyrule 2showCheckingAgentCapyes
showCheckingAgentCaprule 1overAgentCapShowninvalid_scope.agent_scope_mode_exceeded_shownyes
showCheckingAgentCaprule 2overAgentCapinvalid_scope.agent_scope_mode_exceededyes
showCheckingAgentCaprule 3showDescribingClientyes
showDescribingClientrule 1redirectNotShowninvalid_request.redirect_uri_not_shownyes
showDescribingClientrule 2showCheckingAudienceyes
showCheckingAudiencerule 1audienceUnlistedShowninvalid_target.unlisted_reserved_audience_shownyes
showCheckingAudiencerule 2audienceUnlistedinvalid_target.unlisted_reserved_audienceyes
showCheckingAudiencerule 3bindingScopesyes
bindingScopesDONEagentClientrenderedForAgentyes
bindingScopesDONEdevelopmentClientrenderedyes
bindingScopesDONErenderedyes
bindingScopesFAIL.serverserver_error.storeyes
renderedSUBMITsubmitResolvingRealmyes
renderedForAgentSUBMITsubmitResolvingRealmyes
submitResolvingRealmrule 1hostUnknownmisdirected_request.hostyes
submitResolvingRealmrule 2endpointHostmisdirected_request.hostyes
submitResolvingRealmrule 3submitRateLimitingyes
submitRateLimitingrule 1rateLimitedtoo_many_requests.authorize_limityes
submitRateLimitingrule 2submitValidatingFormyes
submitValidatingFormrule 1inputInvalidvalidation_error.formyes
submitValidatingFormrule 2submitFindingSessionyes
submitFindingSessionrule 1cookieMalformedserver_error.malformed_cookieyes
submitFindingSessionrule 2noSessionparkingRequestyes
submitFindingSessionrule 3staleSessionparkingRequestyes
submitFindingSessionrule 4accountDisabledparkingRequestyes
submitFindingSessionrule 5checkingCsrfyes
checkingCsrfrule 1csrfWronginvalid_csrf_token.mismatchyes
checkingCsrfrule 2burningCsrfyes
burningCsrfDONEsubmitCheckingClientburnCsrfyes
burningCsrfFAIL.serverserver_error.storeyes
submitCheckingClientrule 1clientUnknowninvalid_request.unknown_clientyes
submitCheckingClientrule 2clientUnusableunauthorized_client.disabledyes
submitCheckingClientrule 3submitCheckingRedirectUriyes
submitCheckingRedirectUrirule 1redirectUnregisteredinvalid_request.redirect_uri_not_registeredyes
submitCheckingRedirectUrirule 2redirectNotPermittedinvalid_request.redirect_uri_not_permittedyes
submitCheckingRedirectUrirule 3submitReadingDpopKeyyes
submitReadingDpopKeyrule 1dpopJktMalformedinvalid_request.malformed_dpop_jktyes
submitReadingDpopKeyrule 2readingDecisionyes
readingDecisionrule 1deniedaccess_denied.user_deniedyes
readingDecisionrule 2submitCheckingAgentCapyes
submitCheckingAgentCaprule 1overAgentCapinvalid_scope.agent_scope_mode_exceededyes
submitCheckingAgentCaprule 2checkingScopeBindingyes
checkingScopeBindingrule 1scopesUnboundinvalid_scope.consent_scope_mismatchyes
checkingScopeBindingrule 2scopesDifferinvalid_scope.consent_scope_mismatchyes
checkingScopeBindingrule 3checkingResourceBindingyes
checkingResourceBindingrule 1resourcesDifferinvalid_target.consent_resource_mismatchyes
checkingResourceBindingrule 2checkingStepUpyes
checkingStepUprule 1needsFreshLoginparkingRequestyes
checkingStepUprule 2remembersavingConsentyes
checkingStepUprule 3mintingCodeyes
savingConsentDONEmintingCodesaveConsentyes
savingConsentFAIL.serverserver_error.storeyes
mintingCodeDONErecordingGrantmintCodeyes
mintingCodeFAIL.serverserver_error.storeyes
recordingGrantDONEcode_issuedyes
recordingGrantFAIL.serverserver_error.storeyes
parkingRequestrule 1requestTooLargeinvalid_request.authorization_too_largeyes
parkingRequestrule 2noSessionsign_in_required.no_sessionyes
parkingRequestrule 3staleSessionsign_in_required.stale_sessionyes
parkingRequestrule 4accountDisabledsign_in_required.account_disabledyes
parkingRequestrule 5sign_in_required.fresh_loginyes