Logout and Session Management

This section documents OAuth 2.0 (RFC 6749) as amended by the OAuth 2.0 Security Best Current Practice (RFC 9700 / BCP 240), the OAuth 1.0 Protocol (RFC 5849) for historical context, and OpenID Connect Core 1.0, as published at the IETF Datatracker and the OpenID Foundation specifications — and, on the Spring pages, Spring Boot 4.1.x and Spring Security 7.1.x as published at the Spring Security reference documentation — which are the references these pages are written and verified against.

OAuth 2.1 is still an Internet-Draft (draft-ietf-oauth-v2-1-16, 3 September 2026) and is flagged as such everywhere it appears on these pages. It is a working-group consolidation in progress, not a published standard; nothing here should be read as saying otherwise.

This content was generated with the assistance of AI and should be verified against those official specifications before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

An OAuth/OIDC login creates two sessions that live in different places: the client application’s own session (a cookie or server-side session tied to the browser) and the authorization server’s session (the SSO cookie that lets the user skip the login form on the next client that redirects them there). Ending only the first one is the single most common logout bug in federated login, and it is the subject of this whole page.

Why "logging out of the client" is not "logging out of the IdP"

Calling POST /logout on a client application typically does exactly one thing: it destroys that application’s own session (clears its cookie, invalidates its server-side session record). The authorization server’s session is untouched. The practical consequence:

  • The user clicks "Log out" on Client A, sees the logged-out page, and believes they are signed out.

  • They then visit Client B, which also uses the same authorization server for login. Client B redirects to /authorize; the AS still has an active SSO session; it silently re-authenticates the user (no login form) and hands Client B a fresh code — the user is logged back in without ever entering credentials again.

  • On a shared or public machine, the next person to open a browser tab pointed at the AS’s login page may inherit that still-live SSO session.

None of this is a bug in Client A or Client B individually — both behaved correctly for a pure OAuth/OIDC login. It is a gap that only single logout, described below, closes.

RP-Initiated Logout

OpenID Connect RP-Initiated Logout 1.0 gives the client (the "Relying Party", RP) a way to ask the authorization server to end its own session too, and to send the browser back somewhere sensible afterwards:

GET /connect/logout?id_token_hint=eyJhbGciOiJSUzI1NiJ9...
    &post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2Flogged-out
    &state=af0ifjsldkj HTTP/1.1
Host: auth.example.com
Parameter Purpose

id_token_hint

The ID token from the session being ended — proves to the AS which session this logout request is actually for, and stops one party from ending an arbitrary other user’s session by guessing a post_logout_redirect_uri

post_logout_redirect_uri

Where to send the browser once the AS session is gone; must be pre-registered on the client, exactly like an authorization redirect_uri, or the AS must refuse it

state

Opaque value echoed back on the redirect, exactly like the authorization endpoint’s state

logout_hint

Optional hint (e.g. sub or a login hint) when the AS session is ambiguous and no id_token_hint is available

The AS clears its own SSO session cookie and redirects:

HTTP/1.1 302 Found
Location: https://app.example.com/logged-out?state=af0ifjsldkj

This closes the gap above for a single client-initiated logout — but it only ends the AS’s session and redirects one browser. It does nothing, by itself, for the other clients that browser is still logged into elsewhere; that is what front-channel and back-channel logout add.

Front-Channel Logout

Front-Channel Logout 1.0 lets the authorization server notify every other client with an active session at the moment it processes a logout (RP-initiated or otherwise): it renders a hidden <iframe> per registered client, pointed at that client’s frontchannel_logout_uri, so each client’s own logout endpoint runs inside the user’s browser and can clear its own cookie:

<!-- rendered by the AS on its own /connect/logout page, one iframe per client with an active session -->
<iframe src="https://client-b.example.com/oidc/frontchannel-logout?iss=https%3A%2F%2Fauth.example.com&sid=08a5019c-17e1-4977-8f42-65a12843ea02" style="display:none"></iframe>
<iframe src="https://client-c.example.com/oidc/frontchannel-logout?iss=https%3A%2F%2Fauth.example.com&sid=08a5019c-17e1-4977-8f42-65a12843ea02" style="display:none"></iframe>

This depends on the browser actually loading and executing each client’s page inside a third-party iframe context — exactly the context modern browsers increasingly restrict or partition (Safari’s Intelligent Tracking Prevention, Chrome’s phase-out of unpartitioned third-party cookies, Firefox’s Total Cookie Protection). A client whose own session cookie is SameSite=Lax or Strict, or whose storage is partitioned per top-level site, may simply fail to see or clear its session when its logout endpoint runs inside someone else’s page. Front-channel logout therefore now fails silently, unpredictably, and in a browser-dependent way — it cannot be relied on as the only single-logout mechanism.

Front-channel vs. back-channel, side by side

Front-Channel Logout 1.0 Back-Channel Logout 1.0

Transport

Browser-mediated: a hidden <iframe> per client, loaded by the browser

Server-to-server: the AS posts directly to each client’s backend, no browser involved

Credential

None — relies on the client’s own session cookie still being readable in that iframe context

A signed logout token (JWT), authenticated the same way an ID token is

Depends on third-party cookies

Yes — this is exactly what current browser policy restricts

No

Works if the user already closed the tab

No — there is no browser left to load the iframes

Yes — it never needed the browser

Failure mode

Silent: the iframe may load but find no readable session, and the AS has no way to know it failed

Observable: the AS gets an HTTP response (or timeout) per client and can log, retry or alert on failures

Back-Channel Logout: the reliable answer

Back-Channel Logout 1.0 avoids the browser and third-party-cookie problem entirely by having the authorization server call each client’s logout endpoint directly, server to server, with a signed logout token:

POST /oidc/backchannel-logout HTTP/1.1
Host: client-b.example.com
Content-Type: application/x-www-form-urlencoded

logout_token=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiIyNDgyODk3NjEwMDEiLCJhdWQiOiJjbGllbnQtYiIsImlhdCI6MTc1Nzg2NTIwMCwiZXhwIjoxNzU3ODY1MzIwLCJqdGkiOiJibGE2NmQ4Zi04OTlhLTRhMWEtOGZhZS04Zjg0ZmM5M2EzOGUiLCJldmVudHMiOnsiaHR0cDovL3NjaGVtYXMub3BlbmlkLm5ldC9ldmVudC9iYWNrY2hhbm5lbC1sb2dvdXQiOnt9fSwic2lkIjoiMDhhNTAxOWMtMTdlMS00OTc3LThmNDItNjVhMTI4NDNlYTAyIn0.signature

Decoded, the logout token is a purpose-built JWT, not a reused ID token:

{
  "iss": "https://auth.example.com",
  "sub": "248289761001",
  "aud": "client-b",
  "iat": 1757865200,
  "exp": 1757865320,
  "jti": "bla66d8f-899a-4a1a-8fae-8f84fc93a38e",
  "events": {
    "http://schemas.openid.net/event/backchannel-logout": {}
  },
  "sid": "08a5019c-17e1-4977-8f42-65a12843ea02"
}

The client verifies the signature, iss, aud, exp and jti (replay protection) exactly as it would an ID token, confirms events contains the back-channel-logout member, then ends the local session identified by sid (or by sub, if it does not track individual sid values) — entirely server-side, with no browser, no third-party cookie, and no dependency on the user’s current tab even being open. Because it runs out of band, it is the mechanism that actually works under current browser third-party-cookie policy, and it is the one to build single logout on for any new integration; front-channel logout should be treated as a legacy fallback for clients that cannot expose a server-to-server endpoint, not the primary mechanism.

Logout token vs. ID token

The logout token is deliberately shaped so it cannot be confused with, or substituted for, an ID token:

ID token Logout token

Purpose

States that a user authenticated

States that a session ended

events claim

Absent

Required; must contain http://schemas.openid.net/event/backchannel-logout

nonce claim

Present when the authorization request sent one

Must not be present

sub / sid

sub identifies the user

Must contain sub, sid, or both — whichever the client can use to find the local session(s) to end

Typical audience

The client that ran the login

The client(s) registered for back-channel logout, which may be a different set from who happened to be logged in at that moment

A client’s back-channel logout endpoint must reject any token missing the events claim’s specific member value, and must reject one that does carry a nonce — both are signs of a forged or misrouted token rather than a genuine logout notification. Signature, iss and aud are checked exactly as for an ID token, using the same cached JWKS described in Discovery, Metadata and Client Registration.

Trying it by hand

A client’s back-channel logout endpoint is a plain, unauthenticated-by-the-caller POST — its security comes entirely from validating the logout token, not from an Authorization header — which makes it easy to exercise directly while integrating:

# 1. trigger RP-initiated logout for a session, capturing the redirect
curl -si "https://auth.example.com/connect/logout?id_token_hint=${ID_TOKEN}\
&post_logout_redirect_uri=https://app.example.com/logged-out&state=xyz" | head -n 5

# 2. from the client's own logs, or a debug endpoint, confirm each registered
#    backchannel_logout_uri actually received a POST and returned 2xx
curl -si https://client-b.example.com/oidc/backchannel-logout \
  -d "logout_token=${LOGOUT_TOKEN_CAPTURED_FROM_STEP_1}"

# 3. decode the logout token payload (never do this with a *production* token in a public web decoder)
python3 -c "import base64,json,sys; print(json.dumps(json.loads(base64.urlsafe_b64decode(sys.argv[1] + '==')), indent=2))" \
  "$(echo "$LOGOUT_TOKEN_CAPTURED_FROM_STEP_1" | cut -d. -f2)"

A client endpoint that returns anything other than 200 OK (or 204 No Content) for a validated logout token should be treated as a defect: the authorization server has no reliable way to retry indefinitely, and a client that silently fails to act on a logout token leaves that session alive contrary to what RP-initiated logout just told the user happened.

Session Management 1.0 and its dependence on third-party cookies

OpenID Connect Session Management 1.0 predates both logout specifications above and takes a different, poll-based approach: the client embeds a hidden <iframe> pointed at the AS’s check_session_iframe, and periodically posts a message into it (via postMessage) asking "has this session changed?". The AS’s iframe script reads its own first-party session cookie (first-party from the AS’s point of view, third-party from the client page embedding the iframe) and posts back "changed", "unchanged" or "error".

This mechanism is built entirely on the AS’s session cookie being readable inside a third-party iframe context on the client’s page — precisely the access browsers now restrict by default. Where third-party cookies are blocked or partitioned, the check-session iframe cannot see the AS’s real session state, so it reports stale or incorrect results (typically "changed" on every poll, since the AS can no longer tell the browser apart from a fresh, cookie-less visitor). Session Management 1.0 should therefore be treated as effectively obsolete in any browser environment; Back-Channel Logout is the reliable current answer to the same underlying problem (learning that a session ended elsewhere), because it does not route through the browser’s cookie jar at all.

Multiple concurrent sessions and sid

A user is frequently logged into the same client from several browsers or devices at once, and a single logout action — "log out of this browser" — should not necessarily end every one of them. The sid (session ID) claim, present in both the ID token and the logout token, scopes a logout to one authorization-server session rather than to the user account as a whole:

  • At login, the client should store the sid from the ID token alongside its own local session, not just sub.

  • On receiving a back-channel logout token that carries sid, the client ends only the local session(s) tied to that sid — typically one browser’s worth of activity — leaving the user’s other, independently-authenticated sessions untouched.

  • A logout token that carries sub but no sid is a broader signal ("end every session for this user everywhere", the shape an administrative forced-logout or a detected account compromise would use) and should be handled as ending all of that user’s local sessions at this client, not just one.

Getting this distinction wrong in either direction is user-visible: treating every sid-scoped logout as account-wide logs a user out of devices they never asked to end; treating every sub-scoped forced logout as sid-scoped leaves compromised sessions alive on other devices exactly when an administrator most needed them gone.

Token revocation’s role

Ending a session and revoking a token are related but distinct: a session end should trigger token revocation, but a still-valid, unexpired access or refresh token is a separate live credential that keeps working against a resource server until either it expires or is explicitly revoked. POST /oauth2/revoke (RFC 7009) is how a client or authorization server invalidates a specific token immediately; the full mechanics, including why a locally validated JWT access token cannot be revoked before its own exp, are covered in Opaque Tokens, Introspection and Revocation. A thorough single-logout implementation calls revocation for every token issued in the ending session, in addition to clearing cookies and acting on any back-channel logout token received.

Single logout in a microservice / BFF topology

In a backend-for-frontend deployment (see Browser-Based Apps), the browser only ever holds a session cookie to the BFF; the BFF alone holds the OAuth tokens. This concentrates single logout into one place instead of spreading it across every downstream microservice:

  • The BFF registers a frontchannel_logout_uri and/or backchannel_logout_uri with the authorization server, exactly like any other OIDC client.

  • On receiving a back-channel logout token, the BFF looks up the local session(s) matching sid / sub, deletes them from its session store (Redis, a database table, whatever backs it), and revokes the corresponding access and refresh tokens at the AS.

  • Downstream microservices never see the browser’s cookie and never hold a session of their own — they only see short-lived access tokens the BFF attaches per call, so once the BFF’s session and tokens are gone, downstream access stops on the next call without any additional fan-out.

A topology without a BFF — several independently-cookied client applications, each with its own session — has to implement front-channel and/or back-channel logout on every single one of them, which is exactly the fan-out the diagram below shows.

sequenceDiagram participant U as Resource owner (browser) participant AS as Authorization server participant A as Client A participant B as Client B participant C as Client C U->>A: click "Log out" A->>AS: GET /connect/logout (id_token_hint, post_logout_redirect_uri) AS->>AS: end its own SSO session par back-channel fan-out, server to server AS->>B: POST backchannel_logout_uri (logout_token) B->>B: verify token, end local session for sid AS->>C: POST backchannel_logout_uri (logout_token) C->>C: verify token, end local session for sid end AS->>U: redirect to post_logout_redirect_uri