captured core.logout-token

Sign out with an access token

The developer portal's server, or any caller holding the token /auth/login minted, posts it as a Bearer token. QAuth accepts it even when it has expired, as long as QAuth signed it. It ends the user's browser sessions, invalidates their unredeemed authorization codes, revokes their refresh tokens at every client and on every device, denylists the token when it is still live, and records the sign-out. It refuses any other token with 401.

What this view shows

POST /auth/logout from the request to the response, in the order the server runs its checks.

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

Left out of this view:

The main story

When: The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's.

  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token verifies
  7. Accept only the portal's own token: the token is the portal's
  8. Refuse a DPoP-bound token: the token is bound to no key
  9. Find the token's user in this realm: the subject is a user of this realm
  10. End the user's browser sessions: the sessions are ended
  11. Invalidate the user's unredeemed authorization codes: the codes are spent
  12. Revoke every active refresh token of the user: the refresh tokens are revoked and the token is still live
  13. Denylist the token of this request: the token is denylisted
  14. Record the sign-out: the event is recorded

Result: The user is signed out.

The flow

POST /auth/logout arrives the host names no enabled realm with an issuer the request has its realm the address has used up its logout requests the address is under its limit the header is missing or does not match ^Bearer .+$ the header matches the schema the header holds no single token the header holds one token the token does not verify the token has expired, but QAuth signed it: its claims are read anyway the token verifies the token's client_id is not the system client's the token acts for an agent the token's audience is not exactly the developer API's (#524), nor, on a live token, the system client's id the token is the portal's the token is bound to a DPoP key the token is bound to no key the account was disabled after the token was minted and is enabled again: the token must not sign the owner out of the sessions started since (#748) the subject names no user of this realm: deleted since the token was minted, or a user of another realm the subject is a user of this realm the sessions are ended the codes are spent the token has expired, so it is already inert and is not denylisted the refresh tokens are revoked and the token is still live the token is denylisted the request's own session was one of those ended the event is recorded start idle Wait for a logout request idle · server step The realm resolver's onRequest hook, before any route code. /auth/logout is an api route, so it answers on a realm's endpoint hosts too. A request refused here is never counted by the rate limiter. Find the realm of the host resolvingRealm · server step LOGOUT_RATE_LIMIT requests per LOGOUT_RATE_WINDOW seconds, for each client address and realm. Count the request against the logout limit rateLimiting · server step validatingHeader Check the Authorization header against the schema validatingHeader · server step The handler splits the header on single spaces and takes the token only when there are exactly two parts and the first is Bearer. The schema lets through a header such as "Bearer a b", which this refuses. Read the token from the header readingToken · server step The EdDSA signature, then the issuer (the one this realm's tokens carry), then the expiry, in that order. A token that fails only on its expiry is accepted, however long ago it expired: its claims are decoded without the typ and claim-shape checks a live token goes through. Unlike requireJwt, this route does not read the revocation denylist, so a token revoked earlier still signs its user out. Verify the token, and accept it expired verifyingToken · server step assertManagementToken with requireActiveUser false, so a disabled user may still sign out. It checks the client, the act claim and the audience, in this order, and looks no user up: the route does that below, in its realm. For this release only, a token that has not expired may carry the system client's id as its whole audience, as a portal token minted before #524 does, so a sign-out during the rolling upgrade still ends access. Accept only the portal's own token checkingManagementToken · server step RFC 9449 §7.2. The route reads a Bearer token, and a DPoP-bound token is never one. Refuse a DPoP-bound token checkingBinding · server step Looked up in the realm of the request (#489), so a user of another realm is no user here, refused exactly as a deleted one is. Find the token's user in this realm findingUser · server step Every live hosted browser session of the user, found by the user in Postgres rather than through a cookie or a per-user index in Redis (#912) and revoked in one transaction by the session service, so that none of them can obtain a new authorization code once the codes are invalidated. The cookie of the request is cleared when it names one of them. End the user's browser sessions endingSessions · server step Before the refresh tokens, because a code redeemed after that sweep would mint a refresh token that survives the logout. It waits for a redemption in flight, which holds its code row until its refresh token is committed. Invalidate the user's unredeemed authorization codes invalidatingCodes · server step At every client and on every device, with the reason logout. Revoke every active refresh token of the user revokingRefreshTokens · server step Its jti, for the rest of its lifetime, as /oauth/revoke does for a token presented to it; otherwise it would keep working at the management API until it expires. Denylist the token of this request revokingAccessToken · server step The event urn:qauth:event:user.signed_out:v1-rc.0. A failed write fails the request, after everything above has ended. Record the sign-out recordingSignOut · server step signed_out The user is signed out ends here signed_out.cookie_cleared The user is signed out, and the request's session cookie is cleared ends here misdirected_request.host Refuse a host QAuth does not answer for ends here too_many_requests.logout_limit Refuse a request over the logout limit ends here validation_error.authorization Refuse a missing or malformed Authorization header ends here invalid_token.missing_token Refuse a header that holds no single token ends here A bad signature, a malformed token, another issuer, or a live token with the wrong typ or malformed claims. Refuse a token that does not verify ends here invalid_token.not_management_client Refuse a token issued to another client ends here invalid_token.delegated Refuse a token that acts for an agent ends here invalid_token.wrong_audience Refuse a token for another audience ends here invalid_token.dpop_bound Refuse a DPoP-bound token sent as a Bearer token ends here invalid_token.subject_not_a_user Refuse a token that names no user of this realm ends here Answered exactly as a token that names no user (#748); only the event's reason, issued_before_disablement, tells them apart. While the account is still disabled, its user signs out as any other. Refuse a token minted before the account was last disabled, once it is enabled again ends here
POST /auth/logout arrives the host names no enabled realm with an issuer the request has its realm the address has used up its logout requests the address is under its limit the header is missing or does not match ^Bearer .+$ the header matches the schema the header holds no single token the header holds one token the token does not verify the token has expired, but QAuth signed it: its claims are read anyway the token verifies the token's client_id is not the system client's the token acts for an agent the token's audience is not exactly the developer API's (#524), nor, on a live token, the system client's id the token is the portal's the token is bound to a DPoP key the token is bound to no key the account was disabled after the token was minted and is enabled again: the token must not sign the owner out of the sessions started since (#748) the subject names no user of this realm: deleted since the token was minted, or a user of another realm the subject is a user of this realm the sessions are ended fails: the session store fails the codes are spent fails: the database fails the token has expired, so it is already inert and is not denylisted the refresh tokens are revoked and the token is still live fails: the database fails the token is denylisted fails: the cache fails the request's own session was one of those ended the event is recorded fails: the event store fails start idle Wait for a logout request idle · server step The realm resolver's onRequest hook, before any route code. /auth/logout is an api route, so it answers on a realm's endpoint hosts too. A request refused here is never counted by the rate limiter. Find the realm of the host resolvingRealm · server step LOGOUT_RATE_LIMIT requests per LOGOUT_RATE_WINDOW seconds, for each client address and realm. Count the request against the logout limit rateLimiting · server step validatingHeader Check the Authorization header against the schema validatingHeader · server step The handler splits the header on single spaces and takes the token only when there are exactly two parts and the first is Bearer. The schema lets through a header such as "Bearer a b", which this refuses. Read the token from the header readingToken · server step The EdDSA signature, then the issuer (the one this realm's tokens carry), then the expiry, in that order. A token that fails only on its expiry is accepted, however long ago it expired: its claims are decoded without the typ and claim-shape checks a live token goes through. Unlike requireJwt, this route does not read the revocation denylist, so a token revoked earlier still signs its user out. Verify the token, and accept it expired verifyingToken · server step assertManagementToken with requireActiveUser false, so a disabled user may still sign out. It checks the client, the act claim and the audience, in this order, and looks no user up: the route does that below, in its realm. For this release only, a token that has not expired may carry the system client's id as its whole audience, as a portal token minted before #524 does, so a sign-out during the rolling upgrade still ends access. Accept only the portal's own token checkingManagementToken · server step RFC 9449 §7.2. The route reads a Bearer token, and a DPoP-bound token is never one. Refuse a DPoP-bound token checkingBinding · server step Looked up in the realm of the request (#489), so a user of another realm is no user here, refused exactly as a deleted one is. Find the token's user in this realm findingUser · server step Every live hosted browser session of the user, found by the user in Postgres rather than through a cookie or a per-user index in Redis (#912) and revoked in one transaction by the session service, so that none of them can obtain a new authorization code once the codes are invalidated. The cookie of the request is cleared when it names one of them. End the user's browser sessions endingSessions · server step Before the refresh tokens, because a code redeemed after that sweep would mint a refresh token that survives the logout. It waits for a redemption in flight, which holds its code row until its refresh token is committed. Invalidate the user's unredeemed authorization codes invalidatingCodes · server step At every client and on every device, with the reason logout. Revoke every active refresh token of the user revokingRefreshTokens · server step Its jti, for the rest of its lifetime, as /oauth/revoke does for a token presented to it; otherwise it would keep working at the management API until it expires. Denylist the token of this request revokingAccessToken · server step The event urn:qauth:event:user.signed_out:v1-rc.0. A failed write fails the request, after everything above has ended. Record the sign-out recordingSignOut · server step signed_out The user is signed out ends here signed_out.cookie_cleared The user is signed out, and the request's session cookie is cleared ends here misdirected_request.host Refuse a host QAuth does not answer for ends here too_many_requests.logout_limit Refuse a request over the logout limit ends here validation_error.authorization Refuse a missing or malformed Authorization header ends here invalid_token.missing_token Refuse a header that holds no single token ends here A bad signature, a malformed token, another issuer, or a live token with the wrong typ or malformed claims. Refuse a token that does not verify ends here invalid_token.not_management_client Refuse a token issued to another client ends here invalid_token.delegated Refuse a token that acts for an agent ends here invalid_token.wrong_audience Refuse a token for another audience ends here invalid_token.dpop_bound Refuse a DPoP-bound token sent as a Bearer token ends here invalid_token.subject_not_a_user Refuse a token that names no user of this realm ends here Answered exactly as a token that names no user (#748); only the event's reason, issued_before_disablement, tells them apart. While the account is still disabled, its user signs out as any other. Refuse a token minted before the account was last disabled, once it is enabled again ends here The steps before the failing one stay done. The body says nothing of the failure, in any NODE_ENV; its trace_id finds the log line. Fail on a store error ends here

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 shown

What each step answers

StepThe outside world sees
The user is signed out
signed_out
200 · body: {"success":true,"message":"Successfully logged out"}
The user is signed out, and the request's session cookie is cleared
signed_out.cookie_cleared
200 · headers: {"Set-Cookie":"__Host-qauth_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0, with Secure when SESSION_COOKIE_SECURE is on"} · body: {"success":true,"message":"Successfully logged out"}
Refuse a host QAuth does not answer for
misdirected_request.host
421 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#misdirected-request","title":"Misdirected request","status":421,"detail":"This server does not answer for the requested host","instance":"/auth/logout","code":"misdirected_request","trace_id":"the request id"}
Refuse a request over the logout limit
too_many_requests.logout_limit
429 · headers: {"x-ratelimit-limit":"LOGOUT_RATE_LIMIT","x-ratelimit-remaining":"0","x-ratelimit-reset":"the seconds until the window resets","retry-after":"the same seconds"} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#too-many-requests","title":"Too many requests","status":429,"detail":"Rate limit exceeded, retry in {the rest of the window, such as 1 minute}","instance":"/auth/logout","code":"too_many_requests","trace_id":"the request id"}
Refuse a missing or malformed Authorization header
validation_error.authorization
400 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#validation-error","title":"Validation error","status":400,"detail":"The request is not valid.","instance":"/auth/logout","code":"validation_error","trace_id":"the request id","errors":[{"pointer":"/authorization","message":"the schema's message, such as: Authorization header must be in format: Bearer <token>"}]}
Refuse a header that holds no single token
invalid_token.missing_token
401 · headers: {"WWW-Authenticate":"Bearer realm=\"OAuth\""} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#unauthorized","title":"Unauthorized","status":401,"detail":"Missing or invalid Authorization header. Expected format: \"Bearer <token>\"","instance":"/auth/logout","code":"unauthorized","trace_id":"the request id"}
Refuse a token that does not verify
invalid_token.unverified
401 · headers: {"WWW-Authenticate":"Bearer realm=\"OAuth\", error=\"invalid_token\", error_description=\"<the body's error>\""} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-access-token","title":"Invalid access token","status":401,"detail":"Invalid JWT token, or Invalid JWT token: <why>","instance":"/auth/logout","code":"invalid_access_token","trace_id":"the request id"}
Refuse a token issued to another client
invalid_token.not_management_client
401 · headers: {"WWW-Authenticate":"Bearer realm=\"OAuth\", error=\"invalid_token\", error_description=\"Access token was not issued for the management API\""} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-access-token","title":"Invalid access token","status":401,"detail":"Access token was not issued for the management API","instance":"/auth/logout","code":"invalid_access_token","trace_id":"the request id"}
Refuse a token that acts for an agent
invalid_token.delegated
401 · headers: {"WWW-Authenticate":"Bearer realm=\"OAuth\", error=\"invalid_token\", error_description=\"A delegated access token cannot use the management API\""} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-access-token","title":"Invalid access token","status":401,"detail":"A delegated access token cannot use the management API","instance":"/auth/logout","code":"invalid_access_token","trace_id":"the request id"}
Refuse a token for another audience
invalid_token.wrong_audience
401 · headers: {"WWW-Authenticate":"Bearer realm=\"OAuth\", error=\"invalid_token\", error_description=\"Access token audience does not match the management API\""} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-access-token","title":"Invalid access token","status":401,"detail":"Access token audience does not match the management API","instance":"/auth/logout","code":"invalid_access_token","trace_id":"the request id"}
Refuse a DPoP-bound token sent as a Bearer token
invalid_token.dpop_bound
401 · headers: {"WWW-Authenticate":"Bearer realm=\"OAuth\", error=\"invalid_token\", error_description=\"A DPoP-bound access token cannot be used as a Bearer token\""} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-access-token","title":"Invalid access token","status":401,"detail":"A DPoP-bound access token cannot be used as a Bearer token","instance":"/auth/logout","code":"invalid_access_token","trace_id":"the request id"}
Refuse a token that names no user of this realm
invalid_token.subject_not_a_user
401 · headers: {"WWW-Authenticate":"Bearer realm=\"OAuth\", error=\"invalid_token\", error_description=\"Access token subject is not an active user\""} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-access-token","title":"Invalid access token","status":401,"detail":"Access token subject is not an active user","instance":"/auth/logout","code":"invalid_access_token","trace_id":"the request id"}
Refuse a token minted before the account was last disabled, once it is enabled again
invalid_token.issued_before_disablement
401 · headers: {"WWW-Authenticate":"Bearer realm=\"OAuth\", error=\"invalid_token\", error_description=\"Access token subject is not an active user\""} · body: {"type":"https://docs.qauth.dev/reference/problem-types/#invalid-access-token","title":"Invalid access token","status":401,"detail":"Access token subject is not an active user","instance":"/auth/logout","code":"invalid_access_token","trace_id":"the request id"}
Fail on a store error
server_error.store
500 · body: {"type":"https://docs.qauth.dev/reference/problem-types/#internal-error","title":"Internal error","status":500,"instance":"/auth/logout","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 logout limit

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1host == 'unknown'misdirected_request.host1536 of 3072
2otherwiserateLimiting1536 of 3072

Count the request against the logout limit

#IfThen
1the address has used up its logout requestsRefuse a request over the logout limit
2the address is under its limitCheck the Authorization header against the schema

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1overLimittoo_many_requests.logout_limit768 of 1536
2otherwisevalidatingHeader768 of 1536

Check the Authorization header against the schema

#IfThen
1the header is missing or does not match ^Bearer .+$Refuse a missing or malformed Authorization header
2the header matches the schemaRead the token from the header

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1header == 'invalid'validation_error.authorization256 of 768
2otherwisereadingToken512 of 768

Read the token from the header

#IfThen
1the header holds no single tokenRefuse a header that holds no single token
2the header holds one tokenVerify the token, and accept it expired

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1header == 'notOneToken'invalid_token.missing_token256 of 512
2otherwiseverifyingToken256 of 512

Verify the token, and accept it expired

#IfThen
1the token does not verifyRefuse a token that does not verify
2the token has expired, but QAuth signed it: its claims are read anywayAccept only the portal's own token
3the token verifiesAccept only the portal's own token

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1signature == 'invalid'invalid_token.unverified128 of 256
2expiredcheckingManagementToken64 of 256
3otherwisecheckingManagementToken64 of 256

Accept only the portal's own token

#IfThen
1the token's client_id is not the system client'sRefuse a token issued to another client
2the token acts for an agentRefuse a token that acts for an agent
3the token's audience is not exactly the developer API's (#524), nor, on a live token, the system client's idRefuse a token for another audience
4the token is the portal'sRefuse a DPoP-bound token

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1issuedFor == 'otherClient'invalid_token.not_management_client32 of 128
2issuedFor == 'delegated'invalid_token.delegated32 of 128
3issuedFor == 'otherAudience'invalid_token.wrong_audience32 of 128
4otherwisecheckingBinding32 of 128

Refuse a DPoP-bound token

#IfThen
1the token is bound to a DPoP keyRefuse a DPoP-bound token sent as a Bearer token
2the token is bound to no keyFind the token's user in this realm

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1dpopBoundinvalid_token.dpop_bound16 of 32
2otherwisefindingUser16 of 32

Find the token's user in this realm

#IfThen
1the account was disabled after the token was minted and is enabled again: the token must not sign the owner out of the sessions started since (#748)Refuse a token minted before the account was last disabled, once it is enabled again
2the subject names no user of this realm: deleted since the token was minted, or a user of another realmRefuse a token that names no user of this realm
3the subject is a user of this realmEnd the user's browser sessions

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1subject == 'reenabled'invalid_token.issued_before_disablement4 of 16
2subject != 'user'invalid_token.subject_not_a_user8 of 16
3otherwiseendingSessions4 of 16

Every way it can end

It ends withWhenWhat happens
The user is signed out
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token has expired, but QAuth signed it: its claims are read anyway
  7. Accept only the portal's own token: the token is the portal's
  8. Refuse a DPoP-bound token: the token is bound to no key
  9. Find the token's user in this realm: the subject is a user of this realm
  10. End the user's browser sessions: the sessions are ended
  11. Invalidate the user's unredeemed authorization codes: the codes are spent
  12. Revoke every active refresh token of the user: the token has expired, so it is already inert and is not denylisted
  13. Record the sign-out: the event is recorded
The user is signed out, and the request's session cookie is cleared
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, the token is bound to no key, the request carries the session cookie of one of the user's browser sessions
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token has expired, but QAuth signed it: its claims are read anyway
  7. Accept only the portal's own token: the token is the portal's
  8. Refuse a DPoP-bound token: the token is bound to no key
  9. Find the token's user in this realm: the subject is a user of this realm
  10. End the user's browser sessions: the sessions are ended
  11. Invalidate the user's unredeemed authorization codes: the codes are spent
  12. Revoke every active refresh token of the user: the token has expired, so it is already inert and is not denylisted
  13. Record the sign-out: the request's own session was one of those ended
Refuse a host QAuth does not answer for
ends here
The host names no realm, a disabled realm or a realm with no issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the host names no enabled realm with an issuer
Refuse a request over the logout limit
ends here
The host is a host of an enabled realm that has an issuer, this client address has used up its logout requests for the window, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address has used up its logout requests
Refuse a missing or malformed Authorization header
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is missing, or does not match ^Bearer .+$, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header is missing or does not match ^Bearer .+$
Refuse a header that holds no single token
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the header matches ^Bearer .+$ but is not two parts split by one space, such as Bearer a b, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds no single token
Refuse a token that does not verify
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, the token is no JWT, its signature or issuer does not verify, or it is live and its typ or claims do not verify, the token has not expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token does not verify
Refuse a token issued to another client
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token was issued to another client, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token verifies
  7. Accept only the portal's own token: the token's client_id is not the system client's
Refuse a token that acts for an agent
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token carries an act claim: it acts for an agent, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token verifies
  7. Accept only the portal's own token: the token acts for an agent
Refuse a token for another audience
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token was issued to the system client for another audience, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token verifies
  7. Accept only the portal's own token: the token's audience is not exactly the developer API's (#524), nor, on a live token, the system client's id
Refuse a DPoP-bound token sent as a Bearer token
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, the token is bound to a DPoP key (cnf), the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token verifies
  7. Accept only the portal's own token: the token is the portal's
  8. Refuse a DPoP-bound token: the token is bound to a DPoP key
Refuse a token that names no user of this realm
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token is the one /auth/login mints for the developer portal, the token's subject names no user any more, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token verifies
  7. Accept only the portal's own token: the token is the portal's
  8. Refuse a DPoP-bound token: the token is bound to no key
  9. Find the token's user in this realm: the subject names no user of this realm: deleted since the token was minted, or a user of another realm
Refuse a token minted before the account was last disabled, once it is enabled again
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, disabled after the token was minted and enabled again since, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token verifies
  7. Accept only the portal's own token: the token is the portal's
  8. Refuse a DPoP-bound token: the token is bound to no key
  9. Find the token's user in this realm: the account was disabled after the token was minted and is enabled again: the token must not sign the owner out of the sessions started since (#748)
Fail on a store error
ends here
The host is a host of an enabled realm that has an issuer, the address is under its logout limit, the Authorization header is Bearer and one token, QAuth signed the token, for the issuer of this realm's tokens, the token has not expired, the token is the one /auth/login mints for the developer portal, the token's subject is a user of this realm, the token is bound to no key, the request carries no session cookie of the user's
  1. Wait for a logout request: POST /auth/logout arrives
  2. Find the realm of the host: the request has its realm
  3. Count the request against the logout limit: the address is under its limit
  4. Check the Authorization header against the schema: the header matches the schema
  5. Read the token from the header: the header holds one token
  6. Verify the token, and accept it expired: the token verifies
  7. Accept only the portal's own token: the token is the portal's
  8. Refuse a DPoP-bound token: the token is bound to no key
  9. Find the token's user in this realm: the subject is a user of this realm
  10. End the user's browser sessions: it fails, because the session 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 12498 situations the model can reach, from 3072 different starting situations.

Exact rules
IdNever
only-the-portals-token-ends-accessstate in ['endingSessions', 'invalidatingCodes', 'revokingRefreshTokens', 'revokingAccessToken', 'recordingSignOut', 'signed_out', 'signed_out.cookie_cleared'] and (host != 'realm' or overLimit or header != 'bearer' or signature != 'valid' or issuedFor != 'portal' or subject != 'user' or dpopBound)
signed-by-qauth-is-never-unverifiedstate == 'invalid_token.unverified' and signature == 'valid'
codes-before-refresh-tokensstate == 'revokingRefreshTokens' and not (sessionsEnded and codesInvalidated)
signed-out-means-endedstate in ['signed_out', 'signed_out.cookie_cleared'] and not (sessionsEnded and codesInvalidated and refreshTokensRevoked)
expired-token-not-denylistedstate == 'revokingAccessToken' and expired
misdirected-is-not-countedstate == 'too_many_requests.logout_limit' and host != 'realm'

Reference

All transitions (33)
FromEventGuardToEffectsTaken
idleLOGOUTresolvingRealmyes
resolvingRealmrule 1host == 'unknown'misdirected_request.hostyes
resolvingRealmrule 2rateLimitingyes
rateLimitingrule 1overLimittoo_many_requests.logout_limityes
rateLimitingrule 2validatingHeaderyes
validatingHeaderrule 1header == 'invalid'validation_error.authorizationyes
validatingHeaderrule 2readingTokenyes
readingTokenrule 1header == 'notOneToken'invalid_token.missing_tokenyes
readingTokenrule 2verifyingTokenyes
verifyingTokenrule 1signature == 'invalid'invalid_token.unverifiedyes
verifyingTokenrule 2expiredcheckingManagementTokenyes
verifyingTokenrule 3checkingManagementTokenyes
checkingManagementTokenrule 1issuedFor == 'otherClient'invalid_token.not_management_clientyes
checkingManagementTokenrule 2issuedFor == 'delegated'invalid_token.delegatedyes
checkingManagementTokenrule 3issuedFor == 'otherAudience'invalid_token.wrong_audienceyes
checkingManagementTokenrule 4checkingBindingyes
checkingBindingrule 1dpopBoundinvalid_token.dpop_boundyes
checkingBindingrule 2findingUseryes
findingUserrule 1subject == 'reenabled'invalid_token.issued_before_disablementyes
findingUserrule 2subject != 'user'invalid_token.subject_not_a_useryes
findingUserrule 3endingSessionsyes
endingSessionsDONEinvalidatingCodesendSessionsyes
endingSessionsFAIL.serverserver_error.storeyes
invalidatingCodesDONErevokingRefreshTokensinvalidateCodesyes
invalidatingCodesFAIL.serverserver_error.storeyes
revokingRefreshTokensDONEexpiredrecordingSignOutrevokeRefreshTokensyes
revokingRefreshTokensDONErevokingAccessTokenrevokeRefreshTokensyes
revokingRefreshTokensFAIL.serverserver_error.storeyes
revokingAccessTokenDONErecordingSignOutyes
revokingAccessTokenFAIL.serverserver_error.storeyes
recordingSignOutDONEownSessionsigned_out.cookie_clearedyes
recordingSignOutDONEsigned_outyes
recordingSignOutFAIL.serverserver_error.storeyes