proposed core.logout

Sign the browser out at an app's request, and send it back to the app

An app sends the browser to /oauth/end-session with an id_token_hint, its client_id, a post_logout_redirect_uri and state, all optional. QAuth checks the hint's signature and issuer and accepts it expired. A return URI must be one the app registered, matched exactly, for the client the hint or client_id names; otherwise QAuth shows an info page, ends nothing and never redirects. When the hint names the account of the browser's session and, by its sid, that session, the session ends at once: its rows, its cookie and its registry rows go, and the session's end reaches the back-channel logout of its apps. In every other case a browser that is signed in gets the logout_confirm step, and nothing ends until the person answers it. The person signs out of this account or of every account on the browser, or stays signed in; either way the browser goes back to the app's registered URI with state, or to the sign-out status page. An app the operator did not set up is never redirected to before the browser has seen a QAuth page: the signed-out page links to its return URI instead.

What this view shows

GET and POST /oauth/end-session from the request to the response; the logout_confirm interaction from its start through the step reads, the answer and the cancel; and GET /oauth/end-session/status.

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 a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, an ID token this realm issued: its signature verifies with a key of the realm's token keys (EdDSA or RS256), its iss is the realm's issuer, its typ is an ID token's, and it has not expired, client_id is absent, or it is the valid hint's aud, post_logout_redirect_uri is, exactly, one of the post_logout_redirect_uris of the client the valid hint's aud or client_id names, that client may be used, and the operator set it up (the seed manifest), the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered.

  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: the valid hint names the session's account: no confirmation is asked
  9. End the browser's session: the session is ended
  10. Record the sign-out: the request carries a registered return URI of a client the operator set up

Result: Send the browser back to the app, signed out.

The flow

GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives GET /oauth/end-session/status arrives the host names no enabled realm with an issuer the request has its realm the address has used up its end-session requests the address is under its limit a parameter is repeated or over its bound a POST came without the session cookie: it is sent again as a GET, which carries the cookie the parameters pass the schema the hint is read, or counted as absent client_id is not the hint's audience; user.sign_out_failed (client_id_mismatch) is recorded client_id is absent or matches no client is named; user.sign_out_failed (post_logout_redirect_uri_without_client) is recorded the client does not register the URI; user.sign_out_failed (post_logout_redirect_uri_not_registered) the client is disabled or suspended; user.sign_out_failed (client_unusable) is recorded there is no return URI, or it is registered the browser holds no live session: nothing to end the valid hint names the session's account: no confirmation is asked no valid hint names the account of the session: the person is asked the request carries a registered return URI of a client the operator set up the request carries a registered return URI of a client the operator did not set up there is nowhere to return the session is ended the request carries a registered return URI of a client the operator set up the request carries a registered return URI of a client the operator did not set up there is nowhere to return the interaction is stored, and the ceremony app draws the step the interaction is stored, and the route draws the step itself the ceremony page reads the step by the interaction id the person presses a button: the form posts the answer the ceremony page reads the personal data, with the flow cookie the person answers, with the CSRF value of this view; the answer takes the turn the person cancels, with the CSRF value of this view the ceremony page reads the step again the person answers, with the CSRF value of the step view the person cancels, with the CSRF value of the step view the answer does not pass the step grammar the answer passes the grammar the interaction is over the person stays signed in the person signs out, and the session has already ended the person signs out (logout, or logout_all, which ends the same one session today) the session is ended the client still registers the return URI there is nowhere to return, or no longer the client still registers the return URI there is nowhere to return, or no longer the interaction is cancelled the client still registers the return URI: the app has signed the person out of itself there is nowhere to return, or no longer the browser holds no live session the browser holds a live session start idle Wait for an end-session request idle · server step The realm resolver's onRequest hook, before any route code. /oauth/end-session is a browser route, served on the realm's own host. A request refused here is never counted by the rate limiter. Find the realm of the host resolvingRealm · server step The authorization endpoint's budget, for each client address and realm: AUTHORIZE_RATE_LIMIT requests per AUTHORIZE_RATE_WINDOW seconds, or AUTHORIZE_RATE_LIMIT_LENIENT where the realm's environment ceiling allows it. Count the request against the end-session limit rateLimiting · server step id_token_hint, client_id, post_logout_redirect_uri, state, ui_locales and logout_hint, each optional and each a single bounded string, in the query of a GET or the form body of a POST (§2: the OP MUST support both). Check the parameters against the schema validatingParams · server step With the realm's token keys and issuer, as the realm's ID tokens are signed. A hint that does not verify is logged and counts as no hint: it names no client and no account, and the request goes on without it. Verify the id_token_hint, and accept it expired verifyingHint · server step checkingClient Check client_id against the hint checkingClient · server step Only when the request carries post_logout_redirect_uri. The client is the one the valid hint's aud names, else the one client_id names; its post_logout_redirect_uris are matched exactly, as strings (§3.1). A refusal records user.sign_out_failed with its reason and shows the info page; nothing ends and the browser is never redirected. Check the return URI against the client's registration checkingReturn · server step The session cookie, read as every page reads it (resolveBrowserSession): a cookie of this realm that names a live session of a usable account. Find the browser's session findingSession · server step user.sign_out_failed with reason no_session, for the client the request named. Record that there was nothing to end recordingNoSession · server step The session service revokes the one session, reason sign_out, by its user: its row is marked revoked in Postgres, a tombstone covers its cache entry, its registry rows and unredeemed codes go with it, and its side record is dropped. One back-channel logout notice per client it served is queued in the same transaction (#624). Every other session of the user stays, and no refresh token is revoked. The response clears the session cookie. End the browser's session endingSession · server step user.signed_out with the ended session's sid, for the user and the client the request named. Record the sign-out recordingSignOut · server step An interaction record (#592) of purpose logout, holding the checked request (the client, the return URI and state, never the hint), the logout_confirm step, the browser session it is for (carried in, as #964 records one) and, when the request named a client, the client's display data; interaction.created is recorded with purpose logout. A new secret in the __Host-qauth_flow cookie binds it to this browser. Nothing has ended. Start the logout_confirm interaction startingConfirm · server step The redirect carries the interaction id and nothing else. Send the browser to the ceremony page of the confirm step atConfirmCeremony · server step While INTERACTION_UI_ENABLED is off there is no ceremony app, so the route draws the step itself: the account it signs out, the app that asked when the request named one, and three buttons that post the answer to the Interaction API with the step's CSRF value. It needs no JavaScript. Show the confirm step as a plain form showingConfirmPage · server step The step read needs the interaction id only and holds no personal data. Return the confirm step showingStep · server step Only to the browser whose flow cookie binds the interaction, and never from another origin. Return the confirm step's personal data showingPersonal · server step { type: urn:qauth:ia:logout_confirm:v1-rc.0, csrf, decision: logout | logout_all | stay }, nothing else. A refusal gives the turn back; the step and its CSRF value stay. Check the answer against the step's grammar checkingAnswer · server step As every finish (#616): the record is deleted unless the expiry sweep claimed it first, the interaction's secret leaves the flow cookie, and interaction.answered (outcome finished) is recorded. Only now is the decision read. End the interaction before anything is decided takingFinish · server step The return URI is checked again first: the client is resolved and its registration matched as at the start, so a client deleted, disabled or suspended since, or one that dropped the URI, gets no redirect. Read the decision readingDecision · server step As endingSession, by the session's user. The response clears the session cookie when it still names this session. End the session the step was started for endingConfirmedSession · server step user.signed_out with the ended session's sid. Record the sign-out recordingConfirmedSignOut · server step user.sign_out_failed with reason no_session. Record that there was nothing left to end recordingNoSessionAtFinish · server step POST /interaction/v1-rc.0/{id}/cancel: the record is deleted unless the expiry sweep claimed it first, its secret leaves the flow cookie, and interaction.cancelled (reason user) is recorded. A cancel is a decision to stay. Cancel the interaction takingCancel · server step user.sign_out_failed with reason declined. Nothing ends. Record that the person stayed signed in recordingDecline · server step The page the confirm step ends on when there is nowhere to return. It takes no parameter, so nobody can make it say anything but the truth about this browser. Read whether the browser is still signed in readingStatus · server step signed_out.returned Send the browser back to the app, signed out redirect with state nothing_to_end.returned Send the browser back to the app; it was not signed in redirect with state signed_out.page Show that the browser is signed out no redirect, page shown nothing_to_end.page Show that the browser is signed out; it was not signed in no redirect, page shown signed_out.offered Show that the browser is signed out, with a link back to an app the operator did not set up no redirect, page shown nothing_to_end.offered Show that the browser is signed out, with a link back to an app the operator did not set up; it was not signed in no redirect, page shown signed_out.confirmed_returned Send the browser back to the app after the person signed out redirect with state signed_out.confirmed_page Send the browser to the status page after the person signed out no redirect, page shown nothing_to_end.finish_returned Send the browser back to the app; its session had already ended redirect with state nothing_to_end.finish_page Send the browser to the status page; its session had already ended no redirect, page shown The app asked to sign the person out and has signed them out of itself; the person chose to stay signed in to QAuth, so the app gets its browser back with state and nothing ended here. Send the browser back to the app, still signed in to QAuth redirect with state stayed.page Send the browser to the status page, still signed in no redirect, page shown status.signed_out Say that the browser is signed out no redirect, page shown status.signed_in Say that the browser is still signed in no redirect, page shown A top-level GET carries the SameSite=Lax session cookie that a cross-site POST does not. Nothing is checked, recorded or ended on this leg; the GET is checked as any other. Send a POST without the session cookie again as a GET no redirect, page shown invalid_request.client_mismatch Refuse a client_id that is not the hint's audience no redirect, page shown invalid_request.return_without_client Refuse a return URI when no client is named no redirect, page shown invalid_request.return_not_registered Refuse a return URI the client did not register no redirect, page shown invalid_request.return_client_unusable Refuse a return URI of a disabled or suspended client no redirect, page shown misdirected_request.host Refuse a host QAuth does not answer for no redirect, page shown too_many_requests.end_session_limit Refuse a request over the end-session limit no redirect, page shown validation_error.params Refuse parameters the schema does not accept no redirect, page shown The turn is given back; the step and its CSRF value stay. Refuse an answer the step grammar does not accept no redirect, page shown
GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives GET /oauth/end-session/status arrives the host names no enabled realm with an issuer the request has its realm the address has used up its end-session requests the address is under its limit a parameter is repeated or over its bound a POST came without the session cookie: it is sent again as a GET, which carries the cookie the parameters pass the schema the hint is read, or counted as absent client_id is not the hint's audience; user.sign_out_failed (client_id_mismatch) is recorded client_id is absent or matches no client is named; user.sign_out_failed (post_logout_redirect_uri_without_client) is recorded the client does not register the URI; user.sign_out_failed (post_logout_redirect_uri_not_registered) the client is disabled or suspended; user.sign_out_failed (client_unusable) is recorded there is no return URI, or it is registered the browser holds no live session: nothing to end the valid hint names the session's account: no confirmation is asked no valid hint names the account of the session: the person is asked the request carries a registered return URI of a client the operator set up the request carries a registered return URI of a client the operator did not set up there is nowhere to return fails: the event store fails the session is ended fails: the session store fails the request carries a registered return URI of a client the operator set up the request carries a registered return URI of a client the operator did not set up there is nowhere to return fails: the event store fails, after the session ended the interaction is stored, and the ceremony app draws the step the interaction is stored, and the route draws the step itself fails: the interaction store fails the ceremony page reads the step by the interaction id the person presses a button: the form posts the answer the ceremony page reads the personal data, with the flow cookie the person answers, with the CSRF value of this view; the answer takes the turn the person cancels, with the CSRF value of this view the ceremony page reads the step again the person answers, with the CSRF value of the step view the person cancels, with the CSRF value of the step view the answer does not pass the step grammar the answer passes the grammar the interaction is over fails: the expiry sweep claimed the interaction first fails: the interaction store fails; the turn is given back the person stays signed in the person signs out, and the session has already ended the person signs out (logout, or logout_all, which ends the same one session today) the session is ended fails: the session store fails the client still registers the return URI there is nowhere to return, or no longer fails: the event store fails, after the session ended the client still registers the return URI there is nowhere to return, or no longer fails: the event store fails the interaction is cancelled fails: the expiry sweep claimed the interaction first fails: the interaction store fails; the turn is given back the client still registers the return URI: the app has signed the person out of itself there is nowhere to return, or no longer fails: the event store fails the browser holds no live session the browser holds a live session start idle Wait for an end-session request idle · server step The realm resolver's onRequest hook, before any route code. /oauth/end-session is a browser route, served on the realm's own host. A request refused here is never counted by the rate limiter. Find the realm of the host resolvingRealm · server step The authorization endpoint's budget, for each client address and realm: AUTHORIZE_RATE_LIMIT requests per AUTHORIZE_RATE_WINDOW seconds, or AUTHORIZE_RATE_LIMIT_LENIENT where the realm's environment ceiling allows it. Count the request against the end-session limit rateLimiting · server step id_token_hint, client_id, post_logout_redirect_uri, state, ui_locales and logout_hint, each optional and each a single bounded string, in the query of a GET or the form body of a POST (§2: the OP MUST support both). Check the parameters against the schema validatingParams · server step With the realm's token keys and issuer, as the realm's ID tokens are signed. A hint that does not verify is logged and counts as no hint: it names no client and no account, and the request goes on without it. Verify the id_token_hint, and accept it expired verifyingHint · server step checkingClient Check client_id against the hint checkingClient · server step Only when the request carries post_logout_redirect_uri. The client is the one the valid hint's aud names, else the one client_id names; its post_logout_redirect_uris are matched exactly, as strings (§3.1). A refusal records user.sign_out_failed with its reason and shows the info page; nothing ends and the browser is never redirected. Check the return URI against the client's registration checkingReturn · server step The session cookie, read as every page reads it (resolveBrowserSession): a cookie of this realm that names a live session of a usable account. Find the browser's session findingSession · server step user.sign_out_failed with reason no_session, for the client the request named. Record that there was nothing to end recordingNoSession · server step The session service revokes the one session, reason sign_out, by its user: its row is marked revoked in Postgres, a tombstone covers its cache entry, its registry rows and unredeemed codes go with it, and its side record is dropped. One back-channel logout notice per client it served is queued in the same transaction (#624). Every other session of the user stays, and no refresh token is revoked. The response clears the session cookie. End the browser's session endingSession · server step user.signed_out with the ended session's sid, for the user and the client the request named. Record the sign-out recordingSignOut · server step An interaction record (#592) of purpose logout, holding the checked request (the client, the return URI and state, never the hint), the logout_confirm step, the browser session it is for (carried in, as #964 records one) and, when the request named a client, the client's display data; interaction.created is recorded with purpose logout. A new secret in the __Host-qauth_flow cookie binds it to this browser. Nothing has ended. Start the logout_confirm interaction startingConfirm · server step The redirect carries the interaction id and nothing else. Send the browser to the ceremony page of the confirm step atConfirmCeremony · server step While INTERACTION_UI_ENABLED is off there is no ceremony app, so the route draws the step itself: the account it signs out, the app that asked when the request named one, and three buttons that post the answer to the Interaction API with the step's CSRF value. It needs no JavaScript. Show the confirm step as a plain form showingConfirmPage · server step The step read needs the interaction id only and holds no personal data. Return the confirm step showingStep · server step Only to the browser whose flow cookie binds the interaction, and never from another origin. Return the confirm step's personal data showingPersonal · server step { type: urn:qauth:ia:logout_confirm:v1-rc.0, csrf, decision: logout | logout_all | stay }, nothing else. A refusal gives the turn back; the step and its CSRF value stay. Check the answer against the step's grammar checkingAnswer · server step As every finish (#616): the record is deleted unless the expiry sweep claimed it first, the interaction's secret leaves the flow cookie, and interaction.answered (outcome finished) is recorded. Only now is the decision read. End the interaction before anything is decided takingFinish · server step The return URI is checked again first: the client is resolved and its registration matched as at the start, so a client deleted, disabled or suspended since, or one that dropped the URI, gets no redirect. Read the decision readingDecision · server step As endingSession, by the session's user. The response clears the session cookie when it still names this session. End the session the step was started for endingConfirmedSession · server step user.signed_out with the ended session's sid. Record the sign-out recordingConfirmedSignOut · server step user.sign_out_failed with reason no_session. Record that there was nothing left to end recordingNoSessionAtFinish · server step POST /interaction/v1-rc.0/{id}/cancel: the record is deleted unless the expiry sweep claimed it first, its secret leaves the flow cookie, and interaction.cancelled (reason user) is recorded. A cancel is a decision to stay. Cancel the interaction takingCancel · server step user.sign_out_failed with reason declined. Nothing ends. Record that the person stayed signed in recordingDecline · server step The page the confirm step ends on when there is nowhere to return. It takes no parameter, so nobody can make it say anything but the truth about this browser. Read whether the browser is still signed in readingStatus · server step signed_out.returned Send the browser back to the app, signed out redirect with state nothing_to_end.returned Send the browser back to the app; it was not signed in redirect with state signed_out.page Show that the browser is signed out no redirect, page shown nothing_to_end.page Show that the browser is signed out; it was not signed in no redirect, page shown signed_out.offered Show that the browser is signed out, with a link back to an app the operator did not set up no redirect, page shown nothing_to_end.offered Show that the browser is signed out, with a link back to an app the operator did not set up; it was not signed in no redirect, page shown signed_out.confirmed_returned Send the browser back to the app after the person signed out redirect with state signed_out.confirmed_page Send the browser to the status page after the person signed out no redirect, page shown nothing_to_end.finish_returned Send the browser back to the app; its session had already ended redirect with state nothing_to_end.finish_page Send the browser to the status page; its session had already ended no redirect, page shown The app asked to sign the person out and has signed them out of itself; the person chose to stay signed in to QAuth, so the app gets its browser back with state and nothing ended here. Send the browser back to the app, still signed in to QAuth redirect with state stayed.page Send the browser to the status page, still signed in no redirect, page shown status.signed_out Say that the browser is signed out no redirect, page shown status.signed_in Say that the browser is still signed in no redirect, page shown A top-level GET carries the SameSite=Lax session cookie that a cross-site POST does not. Nothing is checked, recorded or ended on this leg; the GET is checked as any other. Send a POST without the session cookie again as a GET no redirect, page shown invalid_request.client_mismatch Refuse a client_id that is not the hint's audience no redirect, page shown invalid_request.return_without_client Refuse a return URI when no client is named no redirect, page shown invalid_request.return_not_registered Refuse a return URI the client did not register no redirect, page shown invalid_request.return_client_unusable Refuse a return URI of a disabled or suspended client no redirect, page shown misdirected_request.host Refuse a host QAuth does not answer for no redirect, page shown too_many_requests.end_session_limit Refuse a request over the end-session limit no redirect, page shown validation_error.params Refuse parameters the schema does not accept no redirect, page shown The turn is given back; the step and its CSRF value stay. Refuse an answer the step grammar does not accept no redirect, page shown The sweep reports the interaction expired; nothing ends. Refuse an answer the expiry sweep beat no redirect, page shown interaction_expired.cancelled_late Refuse a cancel the expiry sweep beat no redirect, page shown A session ended before the failure stays ended. Fail on an event store error no redirect, page shown server_error.session_store Fail on a session store error no redirect, page shown server_error.interaction_store Fail on an interaction store error at the start no redirect, page shown The turn is given back; the step stays and can be answered again. Fail on an interaction store error at the answer or the cancel no redirect, page shown server_error.session_store_at_finish Fail on a session store error after the interaction finished no redirect, page shown server_error.event_store_at_finish Fail on an event store error after the interaction finished no redirect, page shown

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

What each step answers

StepThe outside world sees
Send the browser to the ceremony page of the confirm step
atConfirmCeremony
303 · headers: {"Location":"<issuer>/ui/interaction/<the interaction id, 43 base64url characters>","Set-Cookie":"__Host-qauth_flow=<the browser's interaction secrets, this one's added>; Path=/; HttpOnly; SameSite=Lax","Cache-Control":"no-store"}
Show the confirm step as a plain form
showingConfirmPage
200 · headers: {"Content-Type":"text/html; charset=utf-8","Set-Cookie":"__Host-qauth_flow=<the browser's interaction secrets, this one's added>; Path=/; HttpOnly; SameSite=Lax","Cache-Control":"no-store","Content-Security-Policy":"the server-rendered pages' nonce-based policy"} · body: "Sign out?", the account's identifier, the app's display name marked as declared when the request named one, and a form to POST /interaction/v1-rc.0/<id>/answer with csrf and the buttons Sign out (decision logout), Sign out of all accounts (logout_all) and Stay signed in (stay)
Return the confirm step
showingStep
200 · headers: {"Content-Type":"application/json; charset=utf-8","Cache-Control":"no-store"} · body: {"step":"urn:qauth:ia:logout_confirm:v1-rc.0","params":"{}","alternatives":"[]","csrf":"the CSRF value of this step view, for one answer","client":"the client display, when the request named a client","brand":"the realm's brand","expiresAt":"when the interaction expires"}
Return the confirm step's personal data
showingPersonal
200 · headers: {"Content-Type":"application/json; charset=utf-8","Cache-Control":"no-store"} · body: { accounts: [{ identifier, current: true }] }: the account the session is of, by the identifier it signed in with
Send the browser back to the app, signed out
signed_out.returned
303 · headers: {"Location":"post_logout_redirect_uri, with state added to its query when the request had one","Set-Cookie":"__Host-qauth_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0, with Secure when SESSION_COOKIE_SECURE is on","Cache-Control":"no-store"}
Send the browser back to the app; it was not signed in
nothing_to_end.returned
303 · headers: {"Location":"post_logout_redirect_uri, with state added to its query when the request had one","Cache-Control":"no-store"}
Show that the browser is signed out
signed_out.page
200 · headers: {"Content-Type":"text/html; charset=utf-8","Set-Cookie":"__Host-qauth_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0, with Secure when SESSION_COOKIE_SECURE is on","Cache-Control":"no-store"} · body: "You are signed out."
Show that the browser is signed out; it was not signed in
nothing_to_end.page
200 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: "You are signed out."
Show that the browser is signed out, with a link back to an app the operator did not set up
signed_out.offered
200 · headers: {"Content-Type":"text/html; charset=utf-8","Set-Cookie":"__Host-qauth_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0, with Secure when SESSION_COOKIE_SECURE is on","Cache-Control":"no-store"} · body: "You are signed out.", the host of post_logout_redirect_uri, and, for an http(s) URI, a link to it with state added when the request had one
Show that the browser is signed out, with a link back to an app the operator did not set up; it was not signed in
nothing_to_end.offered
200 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: "You are signed out.", the host of post_logout_redirect_uri, and, for an http(s) URI, a link to it with state added when the request had one
Send the browser back to the app after the person signed out
signed_out.confirmed_returned
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, post_logout_redirect_uri with state added when the request had one","Set-Cookie":"__Host-qauth_session cleared when it names the ended session; __Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Send the browser to the status page after the person signed out
signed_out.confirmed_page
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, <issuer>/oauth/end-session/status","Set-Cookie":"__Host-qauth_session cleared when it names the ended session; __Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Send the browser back to the app; its session had already ended
nothing_to_end.finish_returned
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, post_logout_redirect_uri with state added when the request had one","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Send the browser to the status page; its session had already ended
nothing_to_end.finish_page
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, <issuer>/oauth/end-session/status","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Send the browser back to the app, still signed in to QAuth
stayed.returned
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, post_logout_redirect_uri with state added when the request had one","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Send the browser to the status page, still signed in
stayed.page
303 for a form post; 200 for a JSON answer · headers: {"Location":"for a form post, <issuer>/oauth/end-session/status","Set-Cookie":"__Host-qauth_flow without the finished interaction's secret","Cache-Control":"no-store"} · body: for a JSON answer, { result: redirect, location: the same URL }
Say that the browser is signed out
status.signed_out
200 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: "You are signed out."
Say that the browser is still signed in
status.signed_in
200 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: "You are still signed in."
Send a POST without the session cookie again as a GET
resent.as_get
303 · headers: {"Location":"<issuer>/oauth/end-session?<the validated parameters, as the POST carried them>","Cache-Control":"no-store"}
Refuse a client_id that is not the hint's audience
invalid_request.client_mismatch
400 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: the info page: "This sign-out request is not valid. You are still signed in; nothing was changed.", with the request id
Refuse a return URI when no client is named
invalid_request.return_without_client
400 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: the info page: "This sign-out request is not valid. You are still signed in; nothing was changed.", with the request id
Refuse a return URI the client did not register
invalid_request.return_not_registered
400 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: the info page: "This sign-out request is not valid. You are still signed in; nothing was changed.", with the request id
Refuse a return URI of a disabled or suspended client
invalid_request.return_client_unusable
400 · headers: {"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"} · body: the info page: "This sign-out request is not valid. You are still signed in; nothing was changed.", with the request id
Refuse a host QAuth does not answer for
misdirected_request.host
421 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#misdirected-request","status":421,"code":"misdirected_request","trace_id":"the request id"}
Refuse a request over the end-session limit
too_many_requests.end_session_limit
429 · headers: {"retry-after":"the seconds until the window resets"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#too-many-requests","status":429,"code":"too_many_requests","trace_id":"the request id"}
Refuse parameters the schema does not accept
validation_error.params
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#validation-error","status":400,"code":"validation_error","trace_id":"the request id"}
Refuse an answer the step grammar does not accept
invalid_answer.grammar
400 · headers: {"Content-Type":"application/problem+json","Cache-Control":"no-store"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-answer","status":400,"code":"invalid_answer","trace_id":"the request id"}
Refuse an answer the expiry sweep beat
interaction_expired.finished_late
410 · headers: {"Content-Type":"application/problem+json","Cache-Control":"no-store"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#interaction-expired","status":410,"code":"interaction_expired","trace_id":"the request id"}
Refuse a cancel the expiry sweep beat
interaction_expired.cancelled_late
410 · headers: {"Content-Type":"application/problem+json","Cache-Control":"no-store"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#interaction-expired","status":410,"code":"interaction_expired","trace_id":"the request id"}
Fail on an event store error
server_error.event_store
500 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","status":500,"code":"internal_error","trace_id":"the request id"}
Fail on a session store error
server_error.session_store
500 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","status":500,"code":"internal_error","trace_id":"the request id"}
Fail on an interaction store error at the start
server_error.interaction_store
500 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","status":500,"code":"internal_error","trace_id":"the request id"}
Fail on an interaction store error at the answer or the cancel
server_error.interaction_store_at_finish
500 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","status":500,"code":"internal_error","trace_id":"the request id"}
Fail on a session store error after the interaction finished
server_error.session_store_at_finish
500 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","status":500,"code":"internal_error","trace_id":"the request id"}
Fail on an event store error after the interaction finished
server_error.event_store_at_finish
500 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","status":500,"code":"internal_error","trace_id":"the request id"}

How it decides what happens next

Find the realm of the host

#IfThen
1the host names no enabled realm with an issuerRefuse a host QAuth does not answer for
2the request has its realmCount the request against the end-session limit

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1host == 'unknown'misdirected_request.host55296 of 110592
2otherwiserateLimiting55296 of 110592

Count the request against the end-session limit

#IfThen
1the address has used up its end-session requestsRefuse a request over the end-session limit
2the address is under its limitCheck the parameters against the schema

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1overLimittoo_many_requests.end_session_limit27648 of 55296
2otherwisevalidatingParams27648 of 55296

Check the parameters against the schema

#IfThen
1a parameter is repeated or over its boundRefuse parameters the schema does not accept
2a POST came without the session cookie: it is sent again as a GET, which carries the cookieSend a POST without the session cookie again as a GET
3the parameters pass the schemaVerify the id_token_hint, and accept it expired

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1params == 'malformed'validation_error.params13824 of 27648
2method == 'postWithoutCookie'resent.as_get4608 of 27648
3otherwiseverifyingHint9216 of 27648

Verify the id_token_hint, and accept it expired

#IfThen
1the hint is read, or counted as absentCheck client_id against the hint

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1otherwisecheckingClient9216 of 9216

Check client_id against the hint

#IfThen
1client_id is not the hint's audience; user.sign_out_failed (client_id_mismatch) is recordedRefuse a client_id that is not the hint's audience
2client_id is absent or matchesCheck the return URI against the client's registration

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1clientGate == 'mismatch' and hintValidinvalid_request.client_mismatch2304 of 9216
2otherwisecheckingReturn6912 of 9216
hintValid
hint in ['valid', 'expired']

Check the return URI against the client's registration

#IfThen
1no client is named; user.sign_out_failed (post_logout_redirect_uri_without_client) is recordedRefuse a return URI when no client is named
2the client does not register the URI; user.sign_out_failed (post_logout_redirect_uri_not_registered)Refuse a return URI the client did not register
3the client is disabled or suspended; user.sign_out_failed (client_unusable) is recordedRefuse a return URI of a disabled or suspended client
4there is no return URI, or it is registeredFind the browser's session

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1returnTo == 'noClient'invalid_request.return_without_client1152 of 6912
2returnTo == 'unregistered'invalid_request.return_not_registered1152 of 6912
3returnTo == 'clientUnusable'invalid_request.return_client_unusable1152 of 6912
4otherwisefindingSession3456 of 6912

Find the browser's session

#IfThen
1the browser holds no live session: nothing to endRecord that there was nothing to end
2the valid hint names the session's account: no confirmation is askedEnd the browser's session
3no valid hint names the account of the session: the person is askedStart the logout_confirm interaction

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1account == 'none'recordingNoSession1152 of 3456
2account == 'hinted' and hintValidendingSession384 of 3456
3otherwisestartingConfirm1920 of 3456
hintValid
hint in ['valid', 'expired']

Check the answer against the step's grammar

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

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1decision == 'malformed'invalid_answer.grammar480 of 1920
2otherwisetakingFinish1440 of 1920

Read the decision

#IfThen
1the person stays signed inRecord that the person stayed signed in
2the person signs out, and the session has already endedRecord that there was nothing left to end
3the person signs out (logout, or logout_all, which ends the same one session today)End the session the step was started for

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1declinedrecordingDecline480 of 1440
2sessionAtFinish == 'ended'recordingNoSessionAtFinish480 of 1440
3otherwiseendingConfirmedSession480 of 1440
declined
decision == 'stay'

Read whether the browser is still signed in

#IfThen
1the browser holds no live sessionSay that the browser is signed out
2the browser holds a live sessionSay that the browser is still signed in

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1account == 'none'status.signed_out36864 of 110592
2otherwisestatus.signed_in73728 of 110592

Every way it can end

It ends withWhenWhat happens
Send the browser back to the app, signed out
redirect with state
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, an ID token this realm issued: its signature verifies with a key of the realm's token keys (EdDSA or RS256), its iss is the realm's issuer, its typ is an ID token's, and it has not expired, client_id is absent, or it is the valid hint's aud, post_logout_redirect_uri is, exactly, one of the post_logout_redirect_uris of the client the valid hint's aud or client_id names, that client may be used, and the operator set it up (the seed manifest), the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: the valid hint names the session's account: no confirmation is asked
  9. End the browser's session: the session is ended
  10. Record the sign-out: the request carries a registered return URI of a client the operator set up
Send the browser back to the app; it was not signed in
redirect with state
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, post_logout_redirect_uri is, exactly, one of the post_logout_redirect_uris of the client the valid hint's aud or client_id names, that client may be used, and the operator set it up (the seed manifest), the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: the browser holds no live session: nothing to end
  9. Record that there was nothing to end: the request carries a registered return URI of a client the operator set up
Show that the browser is signed out
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, an ID token this realm issued: its signature verifies with a key of the realm's token keys (EdDSA or RS256), its iss is the realm's issuer, its typ is an ID token's, and it has not expired, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: the valid hint names the session's account: no confirmation is asked
  9. End the browser's session: the session is ended
  10. Record the sign-out: there is nowhere to return
Show that the browser is signed out; it was not signed in
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: the browser holds no live session: nothing to end
  9. Record that there was nothing to end: there is nowhere to return
Show that the browser is signed out, with a link back to an app the operator did not set up
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, an ID token this realm issued: its signature verifies with a key of the realm's token keys (EdDSA or RS256), its iss is the realm's issuer, its typ is an ID token's, and it has not expired, client_id is absent, or it is the valid hint's aud, as registered, but the client was not set up by the operator (open dynamic registration, a CIMD document, the developer API): whoever registered it chose the URI, so the browser is not sent there before it has seen a QAuth page (RFC 9700 §4.11.2), the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: the valid hint names the session's account: no confirmation is asked
  9. End the browser's session: the session is ended
  10. Record the sign-out: the request carries a registered return URI of a client the operator did not set up
Show that the browser is signed out, with a link back to an app the operator did not set up; it was not signed in
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, as registered, but the client was not set up by the operator (open dynamic registration, a CIMD document, the developer API): whoever registered it chose the URI, so the browser is not sent there before it has seen a QAuth page (RFC 9700 §4.11.2), the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: the browser holds no live session: nothing to end
  9. Record that there was nothing to end: the request carries a registered return URI of a client the operator did not set up
Send the browser back to the app after the person signed out
redirect with state
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, post_logout_redirect_uri is, exactly, one of the post_logout_redirect_uris of the client the valid hint's aud or client_id names, that client may be used, and the operator set it up (the seed manifest), the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the route draws the step itself
  10. Show the confirm step as a plain form: the person presses a button: the form posts the answer
  11. Check the answer against the step's grammar: the answer passes the grammar
  12. End the interaction before anything is decided: the interaction is over
  13. Read the decision: the person signs out (logout, or logout_all, which ends the same one session today)
  14. End the session the step was started for: the session is ended
  15. Record the sign-out: the client still registers the return URI
Send the browser to the status page after the person signed out
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the route draws the step itself
  10. Show the confirm step as a plain form: the person presses a button: the form posts the answer
  11. Check the answer against the step's grammar: the answer passes the grammar
  12. End the interaction before anything is decided: the interaction is over
  13. Read the decision: the person signs out (logout, or logout_all, which ends the same one session today)
  14. End the session the step was started for: the session is ended
  15. Record the sign-out: there is nowhere to return, or no longer
Send the browser back to the app; its session had already ended
redirect with state
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, post_logout_redirect_uri is, exactly, one of the post_logout_redirect_uris of the client the valid hint's aud or client_id names, that client may be used, and the operator set it up (the seed manifest), the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, it ended meanwhile: a sign-out in another tab, a revoke, or its own end
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the route draws the step itself
  10. Show the confirm step as a plain form: the person presses a button: the form posts the answer
  11. Check the answer against the step's grammar: the answer passes the grammar
  12. End the interaction before anything is decided: the interaction is over
  13. Read the decision: the person signs out, and the session has already ended
  14. Record that there was nothing left to end: the client still registers the return URI
Send the browser to the status page; its session had already ended
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, it ended meanwhile: a sign-out in another tab, a revoke, or its own end
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the route draws the step itself
  10. Show the confirm step as a plain form: the person presses a button: the form posts the answer
  11. Check the answer against the step's grammar: the answer passes the grammar
  12. End the interaction before anything is decided: the interaction is over
  13. Read the decision: the person signs out, and the session has already ended
  14. Record that there was nothing left to end: there is nowhere to return, or no longer
Send the browser back to the app, still signed in to QAuth
redirect with state
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, post_logout_redirect_uri is, exactly, one of the post_logout_redirect_uris of the client the valid hint's aud or client_id names, that client may be used, and the operator set it up (the seed manifest), the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is on: the ceremony app draws the step, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the ceremony app draws the step
  10. Send the browser to the ceremony page of the confirm step: the ceremony page reads the step by the interaction id
  11. Return the confirm step: the person cancels, with the CSRF value of this view
  12. Cancel the interaction: the interaction is cancelled
  13. Record that the person stayed signed in: the client still registers the return URI: the app has signed the person out of itself
Send the browser to the status page, still signed in
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is on: the ceremony app draws the step, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the ceremony app draws the step
  10. Send the browser to the ceremony page of the confirm step: the ceremony page reads the step by the interaction id
  11. Return the confirm step: the person cancels, with the CSRF value of this view
  12. Cancel the interaction: the interaction is cancelled
  13. Record that the person stayed signed in: there is nowhere to return, or no longer
Say that the browser is signed out
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session/status arrives
  2. Read whether the browser is still signed in: the browser holds no live session
Say that the browser is still signed in
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session/status arrives
  2. Read whether the browser is still signed in: the browser holds a live session
Send a POST without the session cookie again as a GET
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a POST without the session cookie: the browser withholds a SameSite=Lax cookie from a cross-site POST, so it cannot tell whether it is signed in, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: a POST came without the session cookie: it is sent again as a GET, which carries the cookie
Refuse a client_id that is not the hint's audience
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, an ID token this realm issued: its signature verifies with a key of the realm's token keys (EdDSA or RS256), its iss is the realm's issuer, its typ is an ID token's, and it has not expired, client_id and the aud of a valid hint name different clients (§2: the OP MUST verify they match), the request carries no post_logout_redirect_uri, the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is not the hint's audience; user.sign_out_failed (client_id_mismatch) is recorded
Refuse a return URI when no client is named
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, post_logout_redirect_uri is given, and neither client_id nor a valid hint names a client, the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: no client is named; user.sign_out_failed (post_logout_redirect_uri_without_client) is recorded
Refuse a return URI the client did not register
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, post_logout_redirect_uri is given, and the client named is unknown or does not register exactly that URI (a client that registered none never gets a redirect), the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: the client does not register the URI; user.sign_out_failed (post_logout_redirect_uri_not_registered)
Refuse a return URI of a disabled or suspended client
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, post_logout_redirect_uri is given, and the client named is disabled or suspended, the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: the client is disabled or suspended; user.sign_out_failed (client_unusable) is recorded
Refuse a host QAuth does not answer for
no redirect, page shown
The host names no realm, a disabled realm or a realm with no issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the host names no enabled realm with an issuer
Refuse a request over the end-session limit
no redirect, page shown
The host is a host of an enabled realm that has an issuer, this client address has used up its end-session requests for the window, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address has used up its end-session requests
Refuse parameters the schema does not accept
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, a parameter is repeated or over its bound, or the POST body is not a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: a parameter is repeated or over its bound
Refuse an answer the step grammar does not accept
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the answer does not pass the step grammar: no decision, another one, or a member beyond type, csrf and decision, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the route draws the step itself
  10. Show the confirm step as a plain form: the person presses a button: the form posts the answer
  11. Check the answer against the step's grammar: the answer does not pass the step grammar
Refuse an answer the expiry sweep beat
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the route draws the step itself
  10. Show the confirm step as a plain form: the person presses a button: the form posts the answer
  11. Check the answer against the step's grammar: the answer passes the grammar
  12. End the interaction before anything is decided: it fails, because the expiry sweep claimed the interaction first
Refuse a cancel the expiry sweep beat
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is on: the ceremony app draws the step, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the ceremony app draws the step
  10. Send the browser to the ceremony page of the confirm step: the ceremony page reads the step by the interaction id
  11. Return the confirm step: the person cancels, with the CSRF value of this view
  12. Cancel the interaction: it fails, because the expiry sweep claimed the interaction first
Fail on an event store error
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser holds no live session of this realm, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: the browser holds no live session: nothing to end
  9. Record that there was nothing to end: it fails, because the event store fails
Fail on a session store error
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, an ID token this realm issued: its signature verifies with a key of the realm's token keys (EdDSA or RS256), its iss is the realm's issuer, its typ is an ID token's, and it has not expired, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: the valid hint names the session's account: no confirmation is asked
  9. End the browser's session: it fails, because the session store fails
Fail on an interaction store error at the start
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: it fails, because the interaction store fails
Fail on an interaction store error at the answer or the cancel
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the route draws the step itself
  10. Show the confirm step as a plain form: the person presses a button: the form posts the answer
  11. Check the answer against the step's grammar: the answer passes the grammar
  12. End the interaction before anything is decided: it fails, because the interaction store fails; the turn is given back
Fail on a session store error after the interaction finished
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is off (the default): the end-session route draws the step as a plain form, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the route draws the step itself
  10. Show the confirm step as a plain form: the person presses a button: the form posts the answer
  11. Check the answer against the step's grammar: the answer passes the grammar
  12. End the interaction before anything is decided: the interaction is over
  13. Read the decision: the person signs out (logout, or logout_all, which ends the same one session today)
  14. End the session the step was started for: it fails, because the session store fails
Fail on an event store error after the interaction finished
no redirect, page shown
The host is a host of an enabled realm that has an issuer, the address is under its end-session limit, the request is a GET, every parameter is given at most once and within its bound; the POST body is a form, the request carries no id_token_hint, client_id is absent, or it is the valid hint's aud, the request carries no post_logout_redirect_uri, the browser's session is of the account the valid hint's sub names, the hint's sid is that session's, and the client the hint was issued to is known and may be used, INTERACTION_UI_ENABLED is on: the ceremony app draws the step, the person signs out of the account the request is for, when the step is answered, the client still registers the return URI and may be used, the session the step was started for is still live when the step is answered
  1. Wait for an end-session request: GET /oauth/end-session with a query, or POST /oauth/end-session with a form, arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the end-session limit: the address is under its limit
  4. Check the parameters against the schema: the parameters pass the schema
  5. Verify the id_token_hint, and accept it expired: the hint is read, or counted as absent
  6. Check client_id against the hint: client_id is absent or matches
  7. Check the return URI against the client's registration: there is no return URI, or it is registered
  8. Find the browser's session: no valid hint names the account of the session: the person is asked
  9. Start the logout_confirm interaction: the interaction is stored, and the ceremony app draws the step
  10. Send the browser to the ceremony page of the confirm step: the ceremony page reads the step by the interaction id
  11. Return the confirm step: the person cancels, with the CSRF value of this view
  12. Cancel the interaction: the interaction is cancelled
  13. Record that the person stayed signed in: it fails, because the event store 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 691824 situations the model can reach, from 110592 different starting situations.

Exact rules
IdNever
nothing-ends-unless-the-hint-or-the-person-says-sosessionEnded and not ((hintValid and account == 'hinted') or confirmed)
a-refused-request-ends-nothingstate in ['invalid_request.client_mismatch', 'invalid_request.return_without_client', 'invalid_request.return_not_registered', 'invalid_request.return_client_unusable'] and (sessionEnded or interactionOpen)
back-only-to-a-registered-uri(state in ['signed_out.returned', 'nothing_to_end.returned'] and returnTo != 'registered') or (state in ['signed_out.confirmed_returned', 'nothing_to_end.finish_returned', 'stayed.returned'] and not canReturnAtFinish)
staying-ends-nothingstate in ['stayed.returned', 'stayed.page'] and sessionEnded
no-unseen-redirect-for-an-unvetted-clientstate in ['signed_out.returned', 'nothing_to_end.returned'] and returnTo == 'registeredUnvetted'
no-step-without-a-sessioninteractionOpen and account == 'none'
a-post-without-the-cookie-ends-nothingstate == 'resent.as_get' and (sessionEnded or interactionOpen)
misdirected-is-not-countedstate == 'too_many_requests.end_session_limit' and host != 'realm'

Reference

All transitions (64)
FromEventGuardToEffectsTaken
idleEND_SESSIONresolvingRealmyes
idleSTATUSreadingStatusyes
resolvingRealmrule 1host == 'unknown'misdirected_request.hostyes
resolvingRealmrule 2rateLimitingyes
rateLimitingrule 1overLimittoo_many_requests.end_session_limityes
rateLimitingrule 2validatingParamsyes
validatingParamsrule 1params == 'malformed'validation_error.paramsyes
validatingParamsrule 2method == 'postWithoutCookie'resent.as_getyes
validatingParamsrule 3verifyingHintyes
verifyingHintrule 1checkingClientyes
checkingClientrule 1clientGate == 'mismatch' and hintValidinvalid_request.client_mismatchyes
checkingClientrule 2checkingReturnyes
checkingReturnrule 1returnTo == 'noClient'invalid_request.return_without_clientyes
checkingReturnrule 2returnTo == 'unregistered'invalid_request.return_not_registeredyes
checkingReturnrule 3returnTo == 'clientUnusable'invalid_request.return_client_unusableyes
checkingReturnrule 4findingSessionyes
findingSessionrule 1account == 'none'recordingNoSessionyes
findingSessionrule 2account == 'hinted' and hintValidendingSessionyes
findingSessionrule 3startingConfirmyes
recordingNoSessionDONEcanReturnnothing_to_end.returnedyes
recordingNoSessionDONEcanOfferReturnnothing_to_end.offeredyes
recordingNoSessionDONEnothing_to_end.pageyes
recordingNoSessionFAIL.serverserver_error.event_storeyes
endingSessionDONErecordingSignOutendSessionyes
endingSessionFAIL.serverserver_error.session_storeyes
recordingSignOutDONEcanReturnsigned_out.returnedyes
recordingSignOutDONEcanOfferReturnsigned_out.offeredyes
recordingSignOutDONEsigned_out.pageyes
recordingSignOutFAIL.serverserver_error.event_storeyes
startingConfirmDONEinteractionUi == 'on'atConfirmCeremonyopenInteractionyes
startingConfirmDONEshowingConfirmPageopenInteractionyes
startingConfirmFAIL.serverserver_error.interaction_storeyes
atConfirmCeremonyREADshowingStepyes
showingConfirmPageANSWERcheckingAnsweryes
showingStepREAD_PERSONALshowingPersonalyes
showingStepANSWERcheckingAnsweryes
showingStepCANCELtakingCancelyes
showingPersonalREADshowingStepyes
showingPersonalANSWERcheckingAnsweryes
showingPersonalCANCELtakingCancelyes
checkingAnswerrule 1decision == 'malformed'invalid_answer.grammaryes
checkingAnswerrule 2takingFinishyes
takingFinishDONEreadingDecisionfinishInteractionyes
takingFinishFAIL.expiredinteraction_expired.finished_lateyes
takingFinishFAIL.serverserver_error.interaction_store_at_finishyes
readingDecisionrule 1declinedrecordingDeclineyes
readingDecisionrule 2sessionAtFinish == 'ended'recordingNoSessionAtFinishyes
readingDecisionrule 3endingConfirmedSessionconfirmyes
endingConfirmedSessionDONErecordingConfirmedSignOutendSessionyes
endingConfirmedSessionFAIL.serverserver_error.session_store_at_finishyes
recordingConfirmedSignOutDONEcanReturnAtFinishsigned_out.confirmed_returnedyes
recordingConfirmedSignOutDONEsigned_out.confirmed_pageyes
recordingConfirmedSignOutFAIL.serverserver_error.event_store_at_finishyes
recordingNoSessionAtFinishDONEcanReturnAtFinishnothing_to_end.finish_returnedyes
recordingNoSessionAtFinishDONEnothing_to_end.finish_pageyes
recordingNoSessionAtFinishFAIL.serverserver_error.event_store_at_finishyes
takingCancelDONErecordingDeclinefinishInteractionyes
takingCancelFAIL.expiredinteraction_expired.cancelled_lateyes
takingCancelFAIL.serverserver_error.interaction_store_at_finishyes
recordingDeclineDONEcanReturnAtFinishstayed.returnedyes
recordingDeclineDONEstayed.pageyes
recordingDeclineFAIL.serverserver_error.event_store_at_finishyes
readingStatusrule 1account == 'none'status.signed_outyes
readingStatusrule 2status.signed_inyes