# `AttestoPhoenix.Controller.EndSessionController`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/controller/end_session_controller.ex#L1)

End-session endpoint (OpenID Connect RP-Initiated Logout 1.0 §2 +
Back-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, 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 (front-channel) logout
runs.

# `end_session`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
