Skip to content

Surviving OIDC Back-Channel Logout

I built single sign-on for a microservices stack. Login worked. The authorization code flow completed. JWT access tokens reached every service. User info came back correctly. Done.

Until I had to implement the logout button.

The Hotel Checkout Problem

Picture a large resort hotel. Restaurant, pool, spa, gym — each run as an independent facility. Guests get a room key card at check-in and charge everything to their room.

The problem is checkout.

The front desk completes checkout. The restaurant doesn't know. The pool attendant doesn't know. The spa staff doesn't know. A former guest flashes their key card, and the facility lets them in — the card looks valid.

This is the logout problem in microservices. The OpenID Provider (OP) is the front desk. Each Relying Party (RP) is the restaurant, pool, or spa. Login happens in one place. Logout has to reach every facility.

OIDC / OP / RP

OpenID Connect (OIDC) adds an authentication layer on top of OAuth 2.0 — it answers "who is this user?" The OpenID Provider (OP) is the auth server that handles login (Keycloak, Auth0, Entra ID, etc.). A Relying Party (RP) is an application that delegates authentication to the OP.

Two Kinds of Room Keys

There are two design philosophies for the hotel key card.

Opaque Tokens — Call the Front Desk

The first kind has a random number on it. Here's what one looks like:

a]3nF8kL9mQx2vR7wP5tY1uJ4hD6gB0s

A random string. No meaningful information. The facility staff can't tell if it's valid just by looking at it. They call the front desk every time: "Is this guest still checked in?" That's the relationship between opaque tokens and Token Introspection (RFC 7662).

Logout is easy. The front desk removes the name from the ledger. Next time a facility calls, the answer is "no longer a guest." Immediate. Reliable. Done.

The cost is load. Every facility calls the front desk for every interaction. A thousand guests, ten facilities, multiple visits a day — the front desk phone never stops ringing. The OP's introspection endpoint becomes the bottleneck.

JWT — A Self-Readable Card

The second kind has the guest name, room number, checkout time, and the hotel's cryptographic signature stamped right on it. Here's what it actually looks like:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJndWVzdF8xMjMiLCJpc3MiOiJodHRwczovL2hvdGVsLmV4YW1wbGUuY29tIiwiYXVkIjoicmVzdGF1cmFudC1hcHAiLCJleHAiOjE3NTIyMjcyMDAsInNpZCI6InNlc3Npb25fYWJjIn0.
(signature)

Base64-decode the payload:

json
{
  "sub": "guest_123",
  "iss": "https://hotel.example.com",
  "aud": "restaurant-app",
  "exp": 1752227200,
  "sid": "session_abc"
}

Who (sub), issued by whom (iss), for which app (aud), valid until when (exp). Everything is on the card. The facility staff reads the card, verifies the signature, and lets the guest in. No phone call to the front desk. That's a JWT access token.

No phones ringing. Each facility validates autonomously. Scales beautifully.

Logout, though, becomes hell.

The guest checks out at the front desk. The card doesn't change. It still says "checkout: tomorrow 11 AM." Unless you physically collect the card or notify every facility that it's no longer valid, the card works.

That's why the OIDC logout specs exist.

Three Ways to Announce a Logout

OIDC defines three methods for the OP to notify RPs of a logout. All reached Final status in September 2022 and became ISO standards (ISO/IEC 26134–26137:2024) in October 2024.

Session Management — Browser Polling

The RP polls the OP's session state via an iframe. Like facility staff periodically checking a bulletin board at the front desk.

This depends on third-party cookies. With major browsers blocking them, it's effectively broken.

Third-Party Cookies

Cookies set by a domain other than the one the browser is visiting. Widely used for ad tracking, now blocked by default in Safari and Firefox, with Chrome tightening restrictions progressively. OIDC Session Management and Front-Channel Logout rely on the OP's iframe accessing the RP's cookies across origins — directly affected by these restrictions.

Front-Channel Logout — Browser-Mediated Notification

When a user logs out at the OP, the OP's page embeds hidden iframes pointing to each RP's frontchannel_logout_uri. The browser loads each URL. The RP clears cookies and ends the session.

Same third-party cookie problem. Cross-origin iframes can't access the RP's cookies. The spec itself acknowledges that "some User Agents are starting to block access to third-party content by default."

Back-Channel Logout — Server-to-Server Notification

No browser involved. The OP sends an HTTP POST directly to each RP. In hotel terms, the front desk calls each facility on an internal phone line: "This guest has checked out."

No third-party cookies. No browser state. In a cross-domain environment, Back-Channel Logout is the only OIDC logout method that actually works.

Inside the Logout Token

The Logout Token the OP sends to RPs is a signed JWT.

The OP sends an HTTP POST with application/x-www-form-urlencoded to each RP's backchannel_logout_uri. The body is simply logout_token=eyJhbGci....

ClaimRequiredDescription
issYesIssuer (OP identifier)
audYesAudience (RP's client_id)
iatYesIssued at
expYesExpiration time
jtiYesUnique token ID (replay prevention)
eventsYesJSON object with key http://schemas.openid.net/event/backchannel-logout
subConditionalSubject identifier. Either sub or sid (or both) must be present
sidConditionalSession ID. Same as above
nonceProhibitedMust not be present (prevents confusion with ID Tokens)

If the Logout Token contains only sub and no sid, the RP must destroy all sessions for that user. To log out a specific session, sid is required. RPs can request this by setting backchannel_logout_session_required: true during client registration.

The RP validates the Logout Token through 11 steps — signature verification, claim validation, confirming nonce is absent, checking the events claim — then destroys the local session. HTTP 200 on success. 400 on failure.

The Paradox JWT Creates

Back to the hotel.

JWT's strength was "no need to call the front desk." Each facility validates autonomously. No load on the OP. Scales.

At the moment of logout, that strength inverts.

Because each facility decides on its own, you have to inject "this card is no longer valid" from the outside. And JWTs have expiration times stamped in. If the token hasn't expired, the signature is correct, the claims are valid, and the RP has no reason to reject it.

With opaque tokens, the OP removes the entry from the ledger. Done. With JWTs, the OP must actively notify every RP, and the logout isn't "complete" until each RP destroys its local session.

This is the fundamental difficulty of Back-Channel Logout. It's trying to achieve immediate consistency in a distributed system.

The Race Condition

There's a time lag between the OP processing the logout and the notification reaching RPs.

  1. T=0ms: User clicks logout at RP-A
  2. T=1ms: RP-A redirects to the OP's end_session_endpoint
  3. T=50ms: OP terminates the session, starts fan-out
  4. T=51ms: User's other tab sends a JWT to RP-B
  5. T=200ms: OP's notification reaches RP-B

At T=51ms, RP-B hasn't received the notification. The JWT's signature and expiration are valid. The request goes through. The user thinks they logged out.

Shorter JWT lifetimes (5 minutes, or 30 seconds) shrink the window. They don't close it. In finance or healthcare, where immediate cutoff after logout is required, even a few seconds may be unacceptable.

The Fan-Out Problem

In a microservices environment, a single user's session might involve 10 to 50 services. The OP must send HTTP POST to each RP's backchannel_logout_uri.

The spec says the OP "SHOULD delay retransmission for an appropriate amount of time." No retry count. No backoff strategy. No maximum wait time.

And when an RP runs multiple instances behind a load balancer? The POST hits one instance. How does that instance propagate the invalidation to the others? Shared session store (Redis)? Event bus (Kafka, Redis Pub/Sub)? The spec doesn't touch this. It's the RP's problem.

Between the Lines of the Spec

The hotel metaphor holds up here, too. The internal phone system from the front desk to each facility — the mechanism of Back-Channel Logout makes sense. But try to actually operate that phone system, and you run into problems the manual never mentioned.

Back-Channel Logout 1.0 reached Final in September 2022. The spec is "done." But implementing it reveals gaps the spec didn't address.

No Retry Semantics

What if an RP's endpoint is down? The spec says "retry." That's it. In practice, Auth0 times out after 5 seconds and records a failure. Keycloak processes synchronously. ZITADEL puts it in an async queue. Every implementation is different.

No Acknowledgment Protocol

The RP returns HTTP 200 (success) or 400 (failure). That's all. No way to express "processing," "partial success," or "will handle later." No dead-letter queue. No guaranteed delivery.

The Danger of sub-Only Logout Tokens

If the Logout Token contains sub but no sid, the RP must destroy all sessions for that user. A user logged in on multiple devices logs out on one — all devices get killed. Unintended, but spec-compliant.

Token Revocation Ordering

The execution order between RFC 7009's Token Revocation Endpoint and Back-Channel Logout is undefined. A Keycloak issue (keycloak/keycloak#34234) illustrates this well: revoking a refresh token destroys the session, so the subsequent end_session_endpoint call can't find the session, and Back-Channel Logout never fires. Keycloak closed this as "expected behavior." With the spec silent on ordering, both interpretations are valid.

Cascading Logout

In federated setups where an RP also acts as a downstream OP, logout needs to cascade. The spec says cascading logout is "desirable." It defines no protocol for propagation, no timeouts, no failure handling.

The Temperature Gap

More than three years since the spec went Final. The gap between providers is striking.

ProviderBack-Channel LogoutFront-Channel LogoutNotes
KeycloakMost comprehensive OSS implementation. Both OP and RP
Auth0Enterprise plan only. 5-second timeout
Okta (Workforce)Uses ITP for risk-based session termination instead
Entra IDNo back-channel support. No roadmap
AWS CognitoNo standard OIDC logout at all
GoogleMoving to SSF/CAEP closed beta
PingFederateBoth OP and RP. JWE encryption supported

Keycloak — The Pioneer's Scars

Keycloak has the most mature open-source back-channel logout implementation. It can send logout notifications as an OP and receive them as an RP from upstream IdPs.

Maturity reveals edge cases.

  • Logout Token was missing the exp claim (fixed in 22.0.8)
  • Multiple sessions on the same client: only one logout request sent (open)
  • "Sign out all sessions" doesn't trigger back-channel logout — iterating millions of sessions doesn't scale, so they intentionally won't fix it
  • Wrong signature algorithm when multiple clients use different alg values (fixed in 26.3.3)

Auth0 — The Enterprise Wall

Auth0 added back-channel logout support in 2023. Enterprise plan only. RPs must store the sid claim at login time.

The Enterprise restriction makes sense. Back-channel logout requires outbound HTTP calls from OP to RP — infrastructure cost jumps. For a multi-tenant SaaS like Auth0, offering it unconditionally across all tenants doesn't pay.

Integration with Okta's Identity Threat Protection (ITP) is progressing, building toward real-time session termination on risk detection. It points past back-channel logout toward risk-based continuous control.

Okta Workforce — A Different Bet

Okta Workforce doesn't natively support back-channel logout for OIDC apps. Front-Channel Single Logout is available as an Early Access feature.

Instead, Okta invests in Identity Threat Protection (ITP). It went GA in August 2024, enabling risk-based session termination across Google Workspace, Slack, Salesforce, and other major apps on threat detection.

The strategic choice: skip the standard OIDC Back-Channel Logout spec entirely and invest in real-time risk-based controls.

Entra ID — Front-Channel Only

Microsoft's Entra ID (formerly Azure AD) does not support back-channel logout. Only front-channel logout via frontchannel_logout_uri. As of March 2025, Microsoft Q&A threads asking about back-channel support remain unanswered.

Microsoft's ecosystem favors apps within the same tenant or domain. Front-channel works well enough there, which likely deprioritizes back-channel investment. For cross-domain microservices, the workaround is to use front-channel callbacks to trigger server-side propagation via Azure SignalR or Azure Web PubSub.

AWS Cognito — No Standard Logout at All

Cognito stands apart. It doesn't include end_session_endpoint in its discovery document. RP-Initiated Logout isn't supported. Logout uses a proprietary /logout endpoint (GET only) that clears the Cognito session. External IdP sign-out doesn't happen. SAML SLO is supported. OIDC SLO is not.

Cognito was born as a BaaS for mobile apps. In mobile, browser sessions barely exist — token expiration serves as the logout mechanism. OIDC SLO was never a priority. That origin still shows.

Google — Betting on the Next Generation

SSF / CAEP

The Shared Signals Framework (SSF) is a pub/sub spec for sharing security events between services in real time. The Continuous Access Evaluation Profile (CAEP) defines event types on top of SSF, including session-revoked. Where Back-Channel Logout fires a one-shot HTTP POST at logout time, SSF is designed as a continuous event stream.

Google isn't pursuing OIDC back-channel logout. They're headed toward SSF and CAEP. Google Workspace's SSF integration is in closed beta, with CAEP's session-revoked event serving the same role as back-channel logout.

SSF uses a pub/sub model — more robust delivery than Back-Channel Logout's fire-and-forget HTTP POST. Apple, Google, IBM, and Okta are participating. It may replace Back-Channel Logout in the long run. The final spec was published in September 2025.

Practical Mitigations

No perfect solution. Only trade-off combinations.

Shorten JWT lifetimes. 5 minutes, or 30 seconds. Shorter expiration shrinks the race window. But refresh frequency goes up, increasing OP load. The pattern starts resembling opaque tokens.

Introspect only for sensitive operations. Normal API calls use local JWT validation. Payments, data exports, and other high-risk operations check the OP's introspection endpoint. A two-tier approach.

Deploy a shared session store. Aggregate session state in Redis or a database. When a back-channel logout notification arrives, update the store. All RP instances read from the same store, so propagation is handled. But the store becomes a SPOF.

Propagate via event bus. Kafka or Redis Pub/Sub distributes logout events to all instances. More loosely coupled than a shared store. Eventually consistent.

StrategyLogout ImmediacyOP LoadImplementation Complexity
Short JWT lifetime + refreshWithin minutesMediumLow
Introspection for sensitive opsPer-operation instantLow–MediumMedium
Shared session storeNear-instantLowHigh
Event busSecondsLowHigh
Opaque tokens across the boardInstantHighLow

Beyond the Trade-Off

OIDC back-channel logout is a classic distributed systems problem — "propagate a state change to all nodes immediately" — expressed in the context of authentication.

The spec is Final. It's not universal. Retry policy is undefined. Acknowledgment is bare-bones. Cascading propagation is merely "desirable." Provider adoption is uneven: Cognito has no standard OIDC logout at all, Entra ID offers front-channel only, and Google is betting on SSF/CAEP — a different future.

JWT's distributed strength — "validation completes at the RP" — and the centralized demand — "reflect logout across everything immediately" — are fundamentally at odds. The choice isn't between one or the other. It's figuring out how much logout delay your system can tolerate and placing your trade-offs accordingly.

There's no right answer yet.