# `AttestoPhoenix.Schema.Authorization`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/schema/authorization.ex#L1)

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). Only `S256` is a valid method.
  * `:cnf` - the optional confirmation/key-binding map (RFC 7800). When
    present it holds a `jkt` (DPoP key thumbprint, RFC 9449 §6) and/or an
    `x5t#S256` (mTLS certificate thumbprint, RFC 8705 §3.1); a bound code
    MUST be redeemed presenting the same binding.
  * `:nonce` - the OIDC request `nonce` (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

  * `:family_id` - the grant family this code will mint into, used to revoke
    descendants when a redeemed code is replayed.
  * `:access_token_jti` / `:access_token_expires_at` - the access token
    produced by the successful code redemption. Stored only after issuance,
    and used to deny the token if the code is later replayed.
  * `:access_token_revoked_at` - set when code reuse revokes that token.
  * `:expires_at` - absolute expiry as a `utc_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 an atomic claim in the store; this
    column also lets a later presentation be recognized as reuse instead of
    an unknown code.
  * `:consumed_success` - whether the first presentation completed all
    redemption checks. Only successful redemption is replayed as reuse.
  * `: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.

# `store_record`

```elixir
@type store_record() :: %{code_hash: String.t(), data: map(), expires_at: integer()}
```

The plain map exchanged with `Attesto.CodeStore`: the code hash, the
opaque grant `:data`, and the absolute expiry in unix seconds.

# `t`

```elixir
@type t() :: %AttestoPhoenix.Schema.Authorization{
  __meta__: term(),
  access_token_expires_at: DateTime.t() | nil,
  access_token_jti: String.t() | nil,
  access_token_revoked_at: DateTime.t() | nil,
  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,
  consumed_success: boolean(),
  expires_at: DateTime.t() | nil,
  family_id: String.t() | nil,
  inserted_at: DateTime.t() | nil,
  nonce: String.t() | nil,
  redirect_uri: String.t() | nil,
  resource: [String.t()] | nil,
  scope: [String.t()] | nil,
  subject: String.t() | nil
}
```

A persisted authorization-code row.

# `code_challenge_method`

```elixir
@spec code_challenge_method() :: String.t()
```

The only accepted PKCE code-challenge method (RFC 7636 §4.3, `S256`).

# `from_record`

```elixir
@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 a `DateTime`. Defaults to
    `DateTime.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.

# `table`

```elixir
@spec table() :: String.t()
```

The default table name for this schema.

# `to_record`

```elixir
@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.

---

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