ACC-68Recommended
End the session when the person signs out at the provider
For single sign-on through OpenID Connect, register a back-channel logout address. End the matching sessions when a valid logout token arrives.
How to do it
OWASP ASVS 5.0 requirement 7.1.3 asks that logouts line up across every system in a single sign-on setup. OpenID Connect Back-Channel Logout 1.0 lets the provider post a signed logout token straight to your server. Store the provider's sid and sub with each session. Verify the token as verify every token says, with iss, aud, iat and exp. Require the events claim with the back-channel logout member and at least one of sid or sub. Refuse a token that holds a nonce or a jti seen before. Answer 200 with Cache-Control: no-store after deleting the sessions, and 400 when any check fails. SAML providers use single logout instead. Account-wide events come through shared signals.
Examples
JavaScript example
import { jwtVerify } from 'jose';
const LOGOUT_EVENT = 'http://schemas.openid.net/event/backchannel-logout';
export async function handleLogoutToken(db, token, provider) {
const { payload } = await jwtVerify(token, provider.keys, { issuer: provider.issuer, audience: provider.clientId, algorithms: ['RS256', 'ES256'] });
const isLogoutToken = payload.events?.[LOGOUT_EVENT] && (payload.sid || payload.sub) && !('nonce' in payload);
if (!isLogoutToken || !(await db.seenTokenIds.addOnce(payload.jti))) throw new BadRequestError('invalid logout token');
await db.none('DELETE FROM sessions WHERE issuer = $1 AND (provider_sid = $2 OR provider_sub = $3)', [payload.iss, payload.sid ?? null, payload.sub ?? null]);
}
Python example
import jwt
LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout"
def handle_logout_token(db, token: str, provider) -> None:
claims = jwt.decode(token, provider.signing_key(token), algorithms=["RS256", "ES256"],
issuer=provider.issuer, audience=provider.client_id, options={"require": ["iat", "exp", "jti"]})
is_logout_token = LOGOUT_EVENT in claims.get("events", {}) and (claims.get("sid") or claims.get("sub")) and "nonce" not in claims
if not is_logout_token or not db.seen_token_ids.add_once(claims["jti"]):
raise BadRequest("invalid logout token")
db.execute("DELETE FROM sessions WHERE issuer = %s AND (provider_sid = %s OR provider_sub = %s)",
(claims["iss"], claims.get("sid"), claims.get("sub")))
How to check
A test provider posts a valid logout token for a signed-in test user. The user's next request answers 401. Tokens with a nonce, without events or with a wrong aud each get 400, and the session stays.
Sources
Related rules
- Verify every token in Security testing
- Listen for security signals from the customer's provider in Accounts
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/accounts/business-sign-on-and-provisioning/back-channel-logout">Agent Wranglers accounts playbook</a>. In code you write, add a comment next to it with the same link.