Ecto schema for the single-use authorization codes backing an
Attesto.CodeStore.
This is the persistent record shape behind the authorization-code grant
(RFC 6749 §4.1). The store layer mints one row per code at the
authorization endpoint and consumes it at the token endpoint; this module
only describes the row and translates it to and from the protocol struct
Attesto.AuthorizationCode.Grant. All protocol decisions (code
generation and hashing, PKCE verification, DPoP/mTLS binding checks,
expiry, single-use semantics) live in attesto; nothing here re-derives
them.
What is stored, and what is not
Only the hash of the code is persisted (:code_hash), never the
plaintext code handed to the client. The plaintext is a bearer secret
(RFC 6749 §10.5): a database disclosure must not yield a usable code, so
the column is the output of Attesto.Secret.hash/1 and is the unique
lookup key.
The remaining columns are the authorization-request context that must be reproduced at redemption time:
:client_id- the client the code was issued to (RFC 6749 §4.1.3: the code MUST be redeemed by that same client).:subject- the resource owner the code authenticates.:scope- the granted scope, a list of scope tokens.:redirect_uri- the registered redirect URI, compared by exact string match at redemption (RFC 6749 §3.1.2 / §4.1.3).:code_challenge/:code_challenge_method- the PKCE challenge and its transform (RFC 7636). OnlyS256is a valid method.:cnf- the optional confirmation/key-binding map (RFC 7800). When present it holds ajkt(DPoP key thumbprint, RFC 9449 §6) and/or anx5t#S256(mTLS certificate thumbprint, RFC 8705 §3.1); a bound code MUST be redeemed presenting the same binding.:nonce- the OIDC requestnonce(OpenID Connect Core §3.1.2.1), round-tripped into the eventual ID Token.:claims- an opaque map of additional request context carried from the authorization request to redemption.
Lifecycle columns
:expires_at- absolute expiry as autc_datetime. Authorization codes are short-lived (RFC 6749 §4.1.2 recommends a maximum of ten minutes).:consumed_at- set when the code is spent. The single-use contract (RFC 6749 §4.1.2) is enforced by the store's atomic take; this column is an audit marker for a consumed-then-rejected redemption, not a second gate.:inserted_at- insertion timestamp.
Record bridge
Attesto.CodeStore exchanges plain maps with a :code_hash, a
:data map, and an integer :expires_at (unix seconds). from_record/2
builds an Ecto changeset from such a map for insertion, and to_record/1
rebuilds the map from a loaded row so the protocol layer can hydrate the
authorization-code grant from record.data.
Table name and prefix
The table is attesto_authorization_codes by default and is namespaced
by the optional schema prefix passed via from_record/2's :prefix
option (or the schema-wide prefix configured through
AttestoPhoenix.Config), letting a host isolate the
authorization-server tables in their own schema.
Summary
Types
The plain map exchanged with Attesto.CodeStore: the code hash, the
opaque grant :data, and the absolute expiry in unix seconds.
A persisted authorization-code row.
Functions
The only accepted PKCE code-challenge method (RFC 7636 §4.3, S256).
Builds an insertable changeset from a Attesto.CodeStore record map.
The default table name for this schema.
Rebuilds the Attesto.CodeStore record map from a loaded row.
Types
The plain map exchanged with Attesto.CodeStore: the code hash, the
opaque grant :data, and the absolute expiry in unix seconds.
@type t() :: %AttestoPhoenix.Schema.Authorization{ __meta__: term(), claims: map() | nil, client_id: String.t() | nil, cnf: map() | nil, code_challenge: String.t() | nil, code_challenge_method: String.t() | nil, code_hash: String.t() | nil, consumed_at: DateTime.t() | nil, expires_at: DateTime.t() | nil, inserted_at: DateTime.t() | nil, nonce: String.t() | nil, redirect_uri: String.t() | nil, scope: [String.t()] | nil, subject: String.t() | nil }
A persisted authorization-code row.
Functions
@spec code_challenge_method() :: String.t()
The only accepted PKCE code-challenge method (RFC 7636 §4.3, S256).
@spec from_record( store_record(), keyword() ) :: Ecto.Changeset.t()
Builds an insertable changeset from a Attesto.CodeStore record map.
record is the map the protocol layer persists: a :code_hash, the
opaque grant :data, and an integer :expires_at in unix seconds. The
fields inside :data (client, subject, scope, redirect URI, PKCE
challenge, optional DPoP thumbprint, OIDC nonce, and request claims) are
spread across the row's columns so they can be queried and audited
individually while still round-tripping losslessly via to_record/1.
Options:
:prefix- the Ecto schema prefix (database schema) to write the row into. Defaults to no prefix.:now- the insertion clock as aDateTime. Defaults toDateTime.utc_now/0. Provided for deterministic tests.
Validation is fail-closed: a missing required field (hash, client,
subject, redirect URI, or expiry) is rejected rather than defaulted. PKCE
is optional at persistence (a confidential client the host exempted from
PKCE via Attesto.AuthorizationRequest's :require_pkce issues a code with
no challenge); when a code_challenge_method is present it is constrained to
S256 (RFC 7636 §4.3), and a challenge-less code stores a NULL method.
@spec table() :: String.t()
The default table name for this schema.
@spec to_record(t()) :: store_record()
Rebuilds the Attesto.CodeStore record map from a loaded row.
The columns are folded back into the opaque grant :data map in exactly
the shape the protocol layer expects, and the
:expires_at utc_datetime is converted back to unix seconds. The
protocol layer re-checks expiry after taking the record, so a row that is
past :expires_at is still returned here and rejected downstream.