# Logout Strategy And Operational Guidance
This guide is the operator-facing reference for SAML Single Logout (SLO). Use it
to understand the architectural realities of front-channel logout, configure
stateful sessions correctly, and establish reliable security boundaries when
cross-origin cookie deletion inevitably fails.
## Overview
SAML Single Logout (SLO) is structurally unreliable in modern browsers. The
protocol was designed in an era where cross-origin third-party cookies were
freely exchanged via hidden iframes and redirect chains. Today, browser privacy
protections block these mechanics by default.
Treat front-channel SLO as a best-effort user experience feature, not a
security boundary. If your compliance framework requires "guaranteed session
termination on IdP logout," you cannot satisfy it with SAML front-channel
SLO alone.
This guide provides the exact vocabulary to push back on rigid compliance
checklists and establishes the mandatory fallbacks required to actually secure
your application's sessions.
## Relyra owns / Host owns
## Relyra owns
- Processing incoming `LogoutRequest` and `LogoutResponse` payloads.
- Validating signatures on logout messages to prevent denial-of-service via
unauthenticated session termination.
- Emitting the `SessionIndex` (if provided by the IdP) to your adapter.
- Providing the `Relyra.SessionAdapter` behaviour seam for your host app to
manage session lifecycle.
## Host owns
- Choosing the session storage mechanism (must be stateful/durable).
- Mapping the SAML `SessionIndex` to the local durable session.
- Terminating the local session when Relyra signals a valid logout request.
- Enforcing absolute and idle session timeouts as the true security boundary.
- Explaining front-channel limitations to security auditors.
## 1. The Compliance Trap
Auditors often expect that clicking "Logout" in a central Identity Provider (IdP)
will synchronously and reliably terminate the user's session in all connected
Service Providers (SPs).
In SAML front-channel SLO, this works by the IdP embedding hidden iframes or
triggering redirect chains to the SP's logout endpoints. The SP receives the
request, reads its local session cookie, and deletes the session.
This fails in modern browsers because it relies on third-party cookies in a
cross-origin context:
- **Safari ITP (Intelligent Tracking Prevention):** Blocks third-party cookies
by default. An iframe from the IdP cannot send the SP's session cookie to the
SP's logout endpoint.
- **Firefox ETP (Enhanced Tracking Protection):** Blocks cross-site tracking
cookies, frequently interfering with SLO redirect chains.
- **Chrome Privacy Sandbox:** Phases out third-party cookies, breaking
cross-origin session termination iframes.
When these mechanisms block the cookie, the SP receives a valid `LogoutRequest`
but cannot identify *which* local session to terminate, because the browser
refused to attach the session cookie to the request. The logout silently fails,
and the SP session remains active.
Equip your developers with this vocabulary (ITP, ETP, Privacy Sandbox). Front-channel
SLO is fundamentally broken by modern browser privacy controls. It is not an
implementation flaw in Relyra or your application; it is a permanent architectural
reality of the web platform.
## 2. Mandatory Stateful Sessions
Because front-channel requests may arrive without a session cookie, you cannot
rely on cookie-deletion alone to terminate a session. You must be able to
terminate a session by its SAML `SessionIndex` from a server-side store.
**Stateless sessions (e.g., standard encrypted Plug cookies) cannot be revoked
reliably during SLO.** If the `LogoutRequest` arrives without the cookie, the
server has no way to invalidate the stateless token sitting in the user's browser.
If you enable SLO, you **must** use a stateful, durable session store (e.g.,
PostgreSQL via Ecto, or Redis). When a valid `LogoutRequest` arrives containing a
`SessionIndex`, your server must look up the corresponding local session in the
database and mark it as deleted or expired. The next time the user's browser
sends the (still present) cookie, the server-side check will fail, and the user
will be logged out.
## 3. Session Index Mapping
To connect SAML SLO to your stateful sessions, implement the `Relyra.SessionAdapter`
behaviour. Specifically, you must implement `index_session/4` and
`terminate_by_session_index/4`.
When a user logs in, the IdP may provide a `SessionIndex` inside the
`AuthnStatement`. Relyra surfaces this value at
`login_result.principal.session_index` on the `%Relyra.LoginResult{}` returned
from `Relyra.consume_response/3`. The host application is responsible for
invoking `index_session/4` to map the SAML `SessionIndex` to its local
durable session — see the host-linkage note after the code example below.
```elixir
defmodule MyApp.Relyra.SessionAdapter do
@behaviour Relyra.SessionAdapter
alias MyApp.Accounts.SessionStore
@impl true
def index_session(session_index, issuer, context, _opts) do
# Called by the host's ACS controller (NOT auto-invoked by Relyra)
# to map the IdP-issued SessionIndex to the host's local session.
# `context` typically carries the host-side local session id and the
# Relyra connection id; shape is host-defined.
case SessionStore.map_saml_index(
context.connection_id,
session_index,
issuer,
context.local_session_id
) do
:ok ->
{:ok, %{session_index: session_index}}
{:error, reason} ->
# Host-namespaced error atom (`:host_*`); Relyra reserves the typed
# atoms documented in `guides/troubleshooting.md`. Pick your own
# vocabulary for host-owned failure modes.
{:error, Relyra.Error.new(:host_session_index_store_failed, inspect(reason))}
end
end
@impl true
def terminate_by_session_index(session_index, issuer, context, _opts) do
# Called automatically by Relyra.consume_logout/3 when a valid
# IdP-initiated LogoutRequest arrives carrying this SessionIndex.
# `context` carries the Relyra connection_id derived from the inbound
# message; the host looks up its local session and terminates it.
case SessionStore.delete_by_saml_index(context.connection_id, session_index, issuer) do
:ok ->
{:ok, %{terminated: session_index}}
{:error, reason} ->
# Host-namespaced error atom; see comment above.
{:error, Relyra.Error.new(:host_session_terminate_failed, inspect(reason))}
end
end
end
```
By linking the `SessionIndex` to your durable session record, you bypass the
need for the browser to send the session cookie during the `LogoutRequest`. The
termination happens entirely server-side.
### Host-owned linkage (you must invoke `index_session/4` yourself)
Relyra does not auto-invoke `index_session/4` from `Relyra.consume_response/3`.
Session-index registration is host-owned: after a successful login, your ACS
controller reads `login_result.principal.session_index` from the
`%Relyra.LoginResult{}` Relyra returns, derives a `context` map containing the
host-side `local_session_id` and the Relyra `connection_id`, then calls
`MyApp.Relyra.SessionAdapter.index_session/4` directly. Relyra _does_
auto-invoke `terminate_by_session_index/4` from `Relyra.consume_logout/3`
because terminate operates entirely on inbound-message data (the IdP's
`LogoutRequest` carries the `SessionIndex` and issuer Relyra needs); index has
no such inbound trigger and depends on a host-only value (the local session
id), so the host owns the call site.
## 4. The Real Security Boundary
Because you cannot guarantee that an IdP logout will successfully reach your SP
(due to network failures, closed laptops, or browser cookie blocking), SLO
can never be your primary security boundary.
**Your primary security boundary must be absolute and idle session timeouts.**
- **Absolute Timeout (e.g., 8-12 hours):** The session must cryptographically or
durably expire after a fixed duration, regardless of user activity. This forces
a fresh authentication through the IdP daily.
- **Idle Timeout (e.g., 30 minutes):** The session should expire if the user has
not interacted with the application recently.
Do not attempt to build "IdP polling" (where your SP periodically pings the IdP
to ask if the user is still active). This is inefficient, brittle, and introduces
unnecessary coupling.
Rely on your local timeouts. If the IdP terminates the session early and SLO
works, excellent. If SLO fails due to ITP/ETP, your absolute timeout guarantees
the session dies anyway. Document this dual-layer approach for your auditors:
SLO is the optimization; local timeouts are the guarantee.