AttestoPhoenix.Controller.EndSessionController (AttestoPhoenix v1.2.0)

Copy Markdown View Source

End-session endpoint (OpenID Connect RP-Initiated Logout 1.0 §2 + Back-Channel Logout 1.0 + Front-Channel Logout 1.0).

Where a Relying Party sends the End-User's browser to log out. This controller owns the protocol — it verifies the id_token_hint, validates the post_logout_redirect_uri against the RP's registered set, fans a logout_token out to every other RP holding the session, renders each front-channel RP's frontchannel_logout_uri in an iframe, and either redirects to the validated return URI or hands off to the host's logged-out page — while the host owns the browser session and the HTML through two callbacks:

  • :terminate_session (REQUIRED when logout is enabled) — (conn, context -> {:ok, conn} | {:ok, conn, session} | {:halt, conn}). Clears the host's browser login session. context is %{subject, sid, client_id} carrying the id_token_hint's values (any may be nil). The host is the authority on the session:

    • {:ok, conn} — the current session was cleared; run front-channel logout only (no back-channel fan-out).
    • {:ok, conn, %{sid: ..., subject: ...}} — the current session was cleared; fan out a logout_token to the RPs of this host-confirmed session. The fan-out scope comes from here, NOT from the request's id_token_hint, so a replayed or stolen ID Token cannot force-log-out an arbitrary session.
    • {:halt, conn} — the host has taken over the response entirely (e.g. to render a logout-confirmation step); nothing further runs.
  • :render_logged_out (optional) — (conn, context -> conn). Renders the "you are now logged out" page when the request asked for no post_logout_redirect_uri. A minimal 200 is sent when unset.

Host responsibility: session binding + CSRF (REQUIRED)

RP-Initiated Logout is a state-changing action reachable by GET (the RP redirects the browser here). The id_token_hint is not an authenticator — it is signed by the OP and any party that holds a copy (a malicious RP, a leaked token) can present it. The library therefore makes the host the session authority: :terminate_session MUST verify that the request corresponds to the current OP browser session (its cookie) before clearing it and before returning an {:ok, conn, session} that drives back-channel fan-out. A host that wants an explicit confirmation step returns {:halt, conn} and renders its own page. The controller additionally requires HTTPS. Without this binding, /end_session is a logout-CSRF / forced-logout primitive.

Redirect safety

A post_logout_redirect_uri is honored only when it exactly matches one the client registered (RP-Initiated Logout §2/§3); the RP is identified from the verified id_token_hint's aud (or the client_id parameter). A request that names an unregistered URI — or supplies one with no way to identify the client — is refused before any session is touched, so the endpoint cannot be turned into an open redirector.

Back-Channel Logout (Back-Channel Logout 1.0 §2.5)

When :terminate_session returns a confirmed session, the OP atomically takes (enumerates-and-deletes) that session's recorded (sid|subject) rows and POSTs a signed logout_token to each RP's backchannel_logout_uri (recorded at mint time, see Attesto.LogoutSessionStore). The take is atomic so concurrent logouts cannot double-deliver. Delivery is best-effort: a slow or failing RP is logged, never allowed to stall the user's logout. Requires a :logout_session_store; without one, only RP-Initiated logout runs.

Front-Channel Logout (Front-Channel Logout 1.0 §3)

The same atomically-taken rows also drive front-channel logout: for every RP in the session that registered a frontchannel_logout_uri, a browser caller (Accept: text/html) gets a logout page embedding each URI in a hidden iframe — with iss and sid query parameters whenever the session's sid is known (see Attesto.FrontChannelLogout) — before the flow completes. When the request carried a validated post_logout_redirect_uri, the page continues there via JavaScript once the iframes have loaded (bounded by a short timeout, with a meta-refresh and a visible link as fallbacks) so the RP-Initiated redirect still happens; with no return URI the page itself is the logged-out page. Front-channel delivery is inherently best-effort (the browser loads the iframes); a non-browser caller cannot run iframes, so front-channel targets are skipped (logged) and the response is unchanged.

Session Management (Session Management 1.0 §3.2)

When Session Management is enabled, a completed logout also expires the JavaScript-readable OP browser-state cookie, so an RP polling the check_session_iframe observes changed.

Summary

Functions

end_session(conn, params)

@spec end_session(Plug.Conn.t(), map()) :: Plug.Conn.t()