Agent Wranglers

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

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.