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/2inserts a grant keyed on an unguessable token, with the canonicalbinding_hashand a TTL-derivedexpires_at, and returns the token.consume/2issues one conditionalUPDATE ... WHERE token AND binding_hash AND consumed_at IS NULL AND expires_at > nowthat stampsconsumed_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
@spec consume(String.t() | nil, AttestoPhoenix.ConsentGrant.binding()) :: :ok | {:error, AttestoPhoenix.ConsentGrantStore.consume_error()}
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.
@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).