AttestoPhoenix.Store.EctoConsentGrantStore (AttestoPhoenix v0.19.0)

Copy Markdown View Source

Postgres-backed AttestoPhoenix.ConsentGrantStore (RFC 6749 §4.1.1).

A consent grant ties one consent decision to the exact authorization request the resource owner saw, so a single Authorize click cannot approve a different client, redirect URI, scope set, or PKCE challenge. The host consent screen mint/2s a grant when the user authorizes; the host :consent callback consume/2s it before a code is issued. The single-use guarantee is enforced here, at the database, not advisory: it must hold against two concurrent presentations of one token, on any node sharing the database.

Behaviour callbacks

  • mint/2 inserts a grant keyed on an unguessable token, with the canonical binding_hash and a TTL-derived expires_at, and returns the token.
  • consume/2 issues one conditional UPDATE ... WHERE token AND binding_hash AND consumed_at IS NULL AND expires_at > now that stamps consumed_at. Postgres serialises the update on the row, so exactly one of any number of concurrent callers observes an affected-row count of 1 and gets :ok. A count of 0 is disambiguated into a precise {:error, reason} by reading the row back — fail closed: every reason still refuses consent.

The repository module is supplied by the host application (:repo under the :attesto_phoenix app) and read at call time; a store with no backing repository cannot enforce single use, so it fails closed rather than silently no-opping.

Summary

Functions

Atomically consumes the grant for token iff it matches binding, is unconsumed, and is unexpired.

Mints a single-use consent grant bound to binding, valid for ttl_seconds.

Functions

consume(token, binding)

Atomically consumes the grant for token iff it matches binding, is unconsumed, and is unexpired.

Returns :ok to the single winning caller and {:error, reason} to every other (:not_found, :binding_mismatch, :expired, :consumed). A nil or blank token short-circuits to {:error, :not_found} without touching the database.

mint(binding, ttl_seconds)

@spec mint(AttestoPhoenix.ConsentGrant.binding(), pos_integer()) ::
  {:ok, String.t()} | {:error, Ecto.Changeset.t()}

Mints a single-use consent grant bound to binding, valid for ttl_seconds.

Returns {:ok, token} with the opaque token the consent screen carries forward to the authorization endpoint, or {:error, changeset} if the row could not be persisted (e.g. an astronomically unlikely token collision).