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 ( 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 |
|---|---|
|
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 |
|
Where to send the browser once the AS session is gone; must be pre-registered on the client, exactly like an authorization |
|
Opaque value echoed back on the redirect, exactly like the authorization endpoint’s |
|
Optional hint (e.g. |
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 |
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 |
|
Absent |
Required; must contain |
|
Present when the authorization request sent one |
Must not be present |
|
|
Must contain |
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
sidfrom the ID token alongside its own local session, not justsub. -
On receiving a back-channel logout token that carries
sid, the client ends only the local session(s) tied to thatsid— typically one browser’s worth of activity — leaving the user’s other, independently-authenticated sessions untouched. -
A logout token that carries
subbut nosidis 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_uriand/orbackchannel_logout_uriwith 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.