backend / authentication / sso / 05_single_logout.md

Single Logout (SLO)

6 interview angles 7 min read source

Single Logout (SLO)

The flip side of SSO. If you signed in once and got into 5 apps, what happens when you log out? Without SLO, you’ve logged out of one app while still being signed in elsewhere — confusing and a security risk on shared computers.

SLO is harder than SSO because it must reach every app the user was signed into, often without browser cooperation. The specs exist; the reality is uneven.

Three logout scopes

Scope What it clears
Local logout just this app’s session — IdP session keeps running
IdP logout end the IdP session — but app sessions stay until they expire
Single Logout (SLO) end IdP session + propagate to all apps

Most apps implement just local logout. Real SLO needs IdP cooperation and per-app endpoints.

Local logout (the minimum)

@app.post("/logout")
def logout(response: Response, sid: str = Depends(get_sid)):
    redis.delete(f"sess:{sid}")
    response.delete_cookie("session")
    return RedirectResponse("/")

Clears the local session. Doesn’t tell the IdP. Doesn’t tell other apps. User opens app B, redirect to IdP, IdP session still valid → still logged in there.

Acceptable for many apps. Insufficient for “logout from everywhere” requirements.

OIDC RP-initiated logout (end_session_endpoint)

OIDC defines a logout endpoint at the OP. The app redirects the user there:

1. App                      clear local session
2. App → User               302 → IdP/logout?id_token_hint=<id_token>
                                  &post_logout_redirect_uri=https://app/logged-out
                                  &state=<random>
3. IdP                      validates id_token_hint, ends IdP session
4. IdP → User               (optional) confirmation page
5. IdP → User               302 → https://app/logged-out?state=<random>

Key params:

Param Effect
id_token_hint the ID token from the original login (so IdP knows which session to end)
post_logout_redirect_uri where to send the user after — must be registered
state CSRF on the callback

After this, the IdP session is gone. Other apps still have local sessions until those expire — but the next time they redirect to IdP, the user has to re-authenticate.

This is “session-level” SLO — eliminates the auto-login problem without actively terminating every app’s session.

OIDC front-channel logout

To actively terminate other apps’ sessions, OIDC defines two channels:

Front-channel (browser-mediated)

The IdP returns a page with hidden iframes pointing at each RP’s logout endpoint:

<iframe src="https://app1.example.com/logout?iss=...&sid=..."></iframe>
<iframe src="https://app2.example.com/logout?iss=...&sid=..."></iframe>
<iframe src="https://app3.example.com/logout?iss=...&sid=..."></iframe>

Each iframe load triggers the RP’s logout endpoint with the user’s browser cookies. The RP clears its session.

Issues:

  • Requires the browser to load all iframes — if the user closes the tab, partial logout.
  • Third-party cookies are restricted (Safari, Firefox by default) — RP cookies may not be sent.
  • All-or-nothing visibility — you can’t tell if every app actually logged out.

Back-channel (server-to-server)

The IdP makes a server-to-server POST to each RP’s backchannel_logout_uri:

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

logout_token=eyJ...

The logout_token is a JWT identifying the session to terminate. The RP validates the JWT and ends the session for that sub / sid.

Pros:

  • No reliance on browser cookies.
  • Reliable; IdP can retry on failure.

Cons:

  • Each RP needs an endpoint reachable from the IdP.
  • More complex to deploy.

For high-security multi-tenant SaaS: implement back-channel logout. For most apps: skip and rely on session expiry.

SAML Single Logout

SAML SLO has the same problems and similar bindings. The flow:

1. App → IdP            SAML LogoutRequest (Redirect or POST)
2. IdP                  validates request, ends IdP session
3. IdP → each SP        SAML LogoutRequest to each SP that had a session
                        (via HTTP-Redirect through user's browser, in sequence)
4. Each SP              clears its local session, returns LogoutResponse
5. IdP → original app   final LogoutResponse

Each “SP cleanup” hop bounces through the user’s browser. If the user closes the tab mid-sequence, the remaining SPs don’t get the LogoutRequest.

SAML also has a SOAP-bound back-channel SLO, but support is uneven.

Why SLO is rarely truly “single”

The combination of:

  • Per-app session lifetimes (sometimes hours/days).
  • Browser limitations on third-party cookies.
  • Mobile apps with cached tokens (back-channel works; front-channel doesn’t).
  • Tabs closed mid-sequence.

means SLO is best-effort, not guaranteed. Most enterprise IdPs implement it; results vary.

Pragmatic alternatives

For “user clicked logout, end everything”:

  1. Local logout + redirect to IdP end_session_endpoint — covers most users (IdP session ends, future logins require re-auth).
  2. Short access tokens (5–15 min) so revocation propagates quickly via token expiry.
  3. Refresh token revocation on logout — call /revoke so refresh fails next time.
  4. Push-based session invalidation — apps subscribe to a logout event stream (Kafka, Redis pub/sub) and end matching sessions.

For audit/compliance (“the user must be logged out of all apps within 60 seconds”): combine short access tokens + IdP session end + push-based revocation. SLO is a nice-to-have on top.

Logging out a stolen session

If a user reports session theft:

  1. Revoke their refresh token(s) at the IdP.
  2. Invalidate all server-side sessions for that user (delete by user_id index in Redis).
  3. Force re-auth — short access tokens expire within minutes, can’t refresh, user re-authenticates next time.

This is the “log out everywhere” admin operation. Different from SLO (which is “user clicked logout”).

OAuth 2.0 Token Revocation (RFC 7009)

POST /revoke HTTP/1.1
Authorization: Basic <client_creds>

token=<rt>&token_type_hint=refresh_token

Revokes the refresh token (and optionally the access token, depending on OP). On logout:

@app.post("/logout")
def logout(rt: str = Depends(get_refresh_token_from_cookie)):
    requests.post(
        f"{IDP}/revoke",
        auth=(CLIENT_ID, CLIENT_SECRET),
        data={"token": rt, "token_type_hint": "refresh_token"},
    )
    # also clear local session and redirect to IdP end_session

This stops the refresh chain — the user’s existing access token still works until its (short) expiry, but they can’t get new ones without re-authenticating.

OIDC end_session_endpoint discovery

From the OP’s discovery doc:

{
  "end_session_endpoint": "https://idp.example.com/connect/endsession",
  "backchannel_logout_supported": true,
  "frontchannel_logout_supported": true,
  "backchannel_logout_session_supported": true
}

Indicates what the OP supports. Your app checks at startup and configures the logout flow accordingly.

Implementation checklist

For a typical web app supporting SSO:

  • Local logout endpoint clears server-side session and cookie.
  • Logout redirects to IdP end_session_endpoint with id_token_hint.
  • post_logout_redirect_uri registered with the IdP.
  • Refresh token revoked on logout via /revoke.
  • Session cookie has HttpOnly, Secure, SameSite=Lax.

For enterprise SSO:

  • Implement back-channel logout endpoint if IdP supports it.
  • Validate logout_token JWT (signature, iss, aud, sub or sid).
  • Delete sessions matching sub and/or sid from session store.
  • Test SLO with the customer’s IdP — behavior varies.

Common pitfalls

  • Forgetting to revoke the refresh token — user “logs out,” but stolen RT keeps working for days.
  • post_logout_redirect_uri not registered — IdP rejects logout request.
  • Validating logout_token insufficiently — accept signed JWT but skip aud/iss — accept malicious logout claims, DoS the user.
  • No state on logout callback — CSRF attacker triggers logout for the user.
  • Relying on front-channel SLO with third-party cookies blocked — silent partial logout.

Common interview confusions

  • “Logout in OIDC ends sessions everywhere.” — only if SLO is configured and works. By default, RP-initiated logout ends the IdP session (so future app accesses require re-auth) but doesn’t actively terminate other RP sessions.
  • “Front-channel SLO is reliable.” — depends on browser cookie policies and the user not closing the tab. Less reliable than back-channel.
  • “SAML SLO is universally supported.” — supported but flaky. Many IdPs implement it; many SPs don’t bother. Real-world SLO is best-effort.

Interview angle

  • “What does ‘logout’ mean in an SSO system?” — three levels: local (just this app), IdP (end the IdP session), and SLO (end every app’s session). Most apps implement local + IdP redirect; full SLO is harder.
  • “What’s the OIDC end_session_endpoint for?” — RP-initiated IdP logout. App redirects user there with id_token_hint and post_logout_redirect_uri. Ends the IdP session, returns user to a designated page.
  • “Front-channel vs back-channel logout?” — front-channel uses browser iframes/redirects to hit each RP’s logout (requires browser cooperation). Back-channel is server-to-server POST from IdP to each RP’s logout endpoint (more reliable, but each RP must be reachable from IdP).
  • “How do you handle ‘log out everywhere’ for a compromised account?” — revoke all refresh tokens at the IdP, invalidate all server-side sessions for that user, rely on short access token lifetime to propagate via expiry.
  • “Why is SLO often best-effort?” — third-party cookie restrictions break front-channel; user closes tab mid-sequence; app sessions outlast IdP session; mobile apps with cached tokens. Use short access tokens + revocation for reliable propagation.
  • “What does id_token_hint do in OIDC logout?” — tells the IdP which user’s session to end. Without it the IdP may show a “select user” page or not know what to log out.