Attesto.LogoutSessionStore behaviour (Attesto v1.2.1)

Copy Markdown View Source

Storage seam for OpenID Connect Back-Channel Logout 1.0 and Front-Channel Logout 1.0.

Both logout mechanisms require the OP to remember which Relying Parties hold a session for the End-User so it can notify them when that session ends: Back-Channel Logout by POSTing a signed logout_token to the RP's backchannel_logout_uri (Back-Channel Logout 1.0 §2.5), Front-Channel Logout by rendering the RP's frontchannel_logout_uri in an iframe on the logout page (Front-Channel Logout 1.0 §3). This behaviour is that memory: a row is recorded each time an ID Token is minted to a logout-capable client (the OP is the party that mints those tokens, so it is the natural place to record the binding), and enumerated when the end-session endpoint fires.

This is not the browser login session — attesto never models that; the host owns login and supplies the sid. This store only persists the OP-side (sid, client_id) -> logout URIs delivery map, exactly as Attesto.RefreshStore persists issued refresh families. Killing the actual login state remains a host concern (the end-session controller's :terminate_session callback).

Record shape

A stored record (record/1 input) is a map with:

  • :sid - the session id (the value asserted in the ID Token's sid claim). The per-session key the fan-out matches on.
  • :subject - the sub the session authenticated. The per-subject key, used when logout is requested without a sid.
  • :client_id - the Relying Party that received the ID Token.
  • :backchannel_logout_uri - where the logout_token is POSTed, or nil for an RP that is not back-channel-logout capable.
  • :session_required - the client's backchannel_logout_session_required (whether its logout_token MUST carry sid).
  • :frontchannel_logout_uri - the RP URL the logout page renders in an iframe (Front-Channel Logout 1.0 §2), or nil for an RP that is not front-channel-logout capable.
  • :frontchannel_session_required - the client's frontchannel_logout_session_required (whether the rendered URI must carry iss and sid query parameters).
  • :expires_at - absolute expiry, unix seconds (so abandoned sessions are swept; mirror the ID Token / session lifetime).

A record carries at least one of the two logout URIs — a client with neither registers no logout session at all.

record/1 is idempotent on (sid, client_id): re-issuing an ID Token for a session the RP already has refreshes the row rather than duplicating it.

Summary

Types

The criteria that select which sessions to log out. :sid scopes logout to one session (across each RP that holds it); :subject (used when no sid is known) scopes it to every session for the subject. At least one is set.

A stored logout session record (see the module docs).

A fan-out target: one RP to notify. :backchannel_logout_uri drives the logout_token POST; :frontchannel_logout_uri drives the logout page's iframe. Either may be nil (never both).

Callbacks

Remove the session rows matched by criteria (same :sid/:subject precedence as targets/1). Called after the fan-out so a session is enumerated for logout exactly once.

Record (idempotently on (sid, client_id)) that client_id holds a logout-capable session sid for subject, reachable at the entry's back-channel and/or front-channel logout URI. Called when an ID Token is minted to such a client.

Atomically enumerate and remove the RP targets matched by criteria, returning them (same :sid/:subject precedence as targets/1). This is the end-session endpoint's fan-out primitive: doing the enumerate and the delete in one statement (e.g. DELETE ... RETURNING) means two concurrent logouts of the same session cannot both observe the rows and double-deliver, and no row that is inserted mid-logout is silently dropped. Already-expired rows are ignored.

List the RP targets to notify for a logout. When criteria carries a :sid, scope to that session (every RP that received an ID Token under it); with no :sid but a :subject, scope to all of that subject's sessions. Returns the matching target/0s (possibly empty). Implementations SHOULD ignore already-expired rows.

Types

criteria()

@type criteria() :: %{
  optional(:sid) => String.t() | nil,
  optional(:subject) => String.t() | nil
}

The criteria that select which sessions to log out. :sid scopes logout to one session (across each RP that holds it); :subject (used when no sid is known) scopes it to every session for the subject. At least one is set.

entry()

@type entry() :: %{
  :sid => String.t(),
  :subject => String.t(),
  :client_id => String.t(),
  optional(:backchannel_logout_uri) => String.t() | nil,
  optional(:session_required) => boolean(),
  optional(:frontchannel_logout_uri) => String.t() | nil,
  optional(:frontchannel_session_required) => boolean(),
  expires_at: non_neg_integer()
}

A stored logout session record (see the module docs).

target()

@type target() :: %{
  client_id: String.t(),
  backchannel_logout_uri: String.t() | nil,
  sid: String.t() | nil,
  session_required: boolean(),
  frontchannel_logout_uri: String.t() | nil,
  frontchannel_session_required: boolean()
}

A fan-out target: one RP to notify. :backchannel_logout_uri drives the logout_token POST; :frontchannel_logout_uri drives the logout page's iframe. Either may be nil (never both).

Callbacks

delete(criteria)

@callback delete(criteria()) :: :ok

Remove the session rows matched by criteria (same :sid/:subject precedence as targets/1). Called after the fan-out so a session is enumerated for logout exactly once.

record(entry)

@callback record(entry()) :: :ok

Record (idempotently on (sid, client_id)) that client_id holds a logout-capable session sid for subject, reachable at the entry's back-channel and/or front-channel logout URI. Called when an ID Token is minted to such a client.

take_targets(criteria)

@callback take_targets(criteria()) :: [target()]

Atomically enumerate and remove the RP targets matched by criteria, returning them (same :sid/:subject precedence as targets/1). This is the end-session endpoint's fan-out primitive: doing the enumerate and the delete in one statement (e.g. DELETE ... RETURNING) means two concurrent logouts of the same session cannot both observe the rows and double-deliver, and no row that is inserted mid-logout is silently dropped. Already-expired rows are ignored.

targets(criteria)

@callback targets(criteria()) :: [target()]

List the RP targets to notify for a logout. When criteria carries a :sid, scope to that session (every RP that received an ID Token under it); with no :sid but a :subject, scope to all of that subject's sessions. Returns the matching target/0s (possibly empty). Implementations SHOULD ignore already-expired rows.