# `Attesto.AuthorizationCode`
[🔗](https://github.com/XukuLLC/attesto/blob/v0.13.0/lib/attesto/authorization_code.ex#L1)

RFC 6749 §4.1 authorization-code grant, with mandatory PKCE (RFC 7636,
S256) and optional DPoP binding of the code (RFC 9449 §10).

This module is pure logic over a `Attesto.CodeStore`: `issue/3` mints a
single-use code at the authorization endpoint, `redeem/4` validates and
consumes it at the token endpoint and returns the grant context the host
uses to mint an access token. The store decides where codes live and
guarantees single use; everything validated here (expiry, exact
redirect-URI match, the PKCE transform, the DPoP key binding) is
protocol.

## PKCE (S256), required by default

`issue/3` accepts a valid S256 `code_challenge` and `redeem/4` checks the
matching `code_verifier`; only S256 is accepted (see `Attesto.PKCE`). This
closes authorization-code interception and is the modern default (OAuth 2.0
Security BCP / RFC 9700). PKCE enforcement at the authorization endpoint is
governed by `Attesto.AuthorizationRequest`'s `:require_pkce` option (default
`true`); a host MAY relax it for a *confidential* client (public clients MUST
use PKCE, RFC 9700 §2.1.1), in which case a code is issued with no challenge
and redeemed with no verifier. A `code_challenge` that is present is always
fully enforced. `issue/3` therefore treats `:code_challenge` as optional: when
given it must be a valid S256 challenge, when absent the code is unbound and a
later redemption MUST present no `code_verifier`.

## Single use even on failure

`redeem/4` consumes the code via `c:Attesto.CodeStore.take/1` **before**
validating it, so a presented code is spent whether or not the
redemption succeeds. An attacker who captures a code cannot make
repeated validation attempts against it.

## Code-reuse detection (when the store supports it)

Single use alone cannot distinguish a *replay of an already-redeemed
code* from a *never-issued code*: once `take/1` removes the row, both
look absent. OAuth 2.0 Security BCP §4.13 (and RFC 6749 §4.1.2) say the
AS SHOULD, on a second presentation of a code, revoke the tokens already
issued from its first redemption, because a re-presented code is an
attack signal.

`redeem/4` enables that when - and only when - the `Attesto.CodeStore`
implements the optional reuse-tracking pair (`c:Attesto.CodeStore.take/1`
returning `{:error, :consumed, meta}` plus
`c:Attesto.CodeStore.mark_consumed/2`). The reuse marker is recorded by
`finalize/3`, which the caller invokes AFTER the full token response has been
successfully built - NOT by `redeem/4` itself. So a code whose redemption
validated but whose downstream issuance then failed (a mint or refresh-token
fault, a host callback returning a bad principal) is left single-use-spent
but NOT reuse-flagged: a replay is `{:error, :invalid_grant}`, and a
legitimate retry of a transient failure is never mistaken for a reuse attack
(which would wrongly revoke the family). Once `finalize/3` has run, a later
redemption of the same code yields `{:error, {:reuse, meta}}`, where `meta`
carries that first redemption's context so the caller can revoke the
descendant family (e.g. via `Attesto.Revocation`). A store that does not
implement the pair behaves exactly as before: a re-presented code is
`{:error, :invalid_grant}`.
This is additive and fail-safe (see `Attesto.CodeStore`).

Pass a `:family_id` to `issue/3` to link the code to the refresh-token
family it will spawn; it rides onto the returned `Grant` so the host
mints the family under that id, and it is what reuse detection replays.

## DPoP-bound codes

If `issue/3` is given a `:dpop_jkt`, the code is bound to that DPoP key
(RFC 9449 §10): redemption MUST present the same `:dpop_jkt` (the
thumbprint of the key in the token-request's DPoP proof) or it is
rejected. A code minted without a binding MAY still be redeemed with a
token-request DPoP proof; in that case this module treats the proof as a
token-endpoint sender constraint for the access token the host is about to
mint, not as a pre-existing authorization-code binding.

# `issue_attrs`

```elixir
@type issue_attrs() :: %{
  :client_id =&gt; String.t(),
  :redirect_uri =&gt; String.t(),
  optional(:code_challenge) =&gt; String.t() | nil,
  :subject =&gt; String.t(),
  optional(:scope) =&gt; [String.t()],
  optional(:resource) =&gt; [String.t()],
  optional(:code_challenge_method) =&gt; String.t(),
  optional(:dpop_jkt) =&gt; String.t() | nil,
  optional(:family_id) =&gt; String.t() | nil,
  optional(:claims) =&gt; map()
}
```

# `issue_error`

```elixir
@type issue_error() ::
  :invalid_client_id
  | :invalid_redirect_uri
  | :invalid_code_challenge
  | :unsupported_code_challenge_method
  | :invalid_subject
  | :invalid_scope
  | :invalid_resource
  | :invalid_dpop_jkt
  | :invalid_family_id
  | :invalid_claims
```

# `redeem_error`

```elixir
@type redeem_error() ::
  :invalid_grant
  | :expired
  | :client_required
  | :client_mismatch
  | :redirect_uri_mismatch
  | :pkce_failed
  | :dpop_proof_required
  | :dpop_binding_mismatch
  | {:reuse, Attesto.CodeStore.consumed_meta()}
```

# `redeem_params`

```elixir
@type redeem_params() :: %{
  :redirect_uri =&gt; String.t(),
  :code_verifier =&gt; String.t(),
  optional(:client_id) =&gt; String.t(),
  optional(:dpop_jkt) =&gt; String.t() | nil
}
```

# `dpop_bound?`

```elixir
@spec dpop_bound?(module(), String.t()) :: boolean()
```

Returns `true` iff a stored code for `code` is bound to a DPoP key (RFC 9449
§10) - i.e. its redemption requires a matching DPoP proof (holder-of-key).

Reads the code via the store's OPTIONAL `c:Attesto.CodeStore.get/1` WITHOUT
consuming it, so a legitimate redemption is unaffected. Returns `false` when
the store has no `get/1`, the code is unknown, or it carries no `:dpop_jkt`.
This lets the token endpoint surface a holder-of-key (`invalid_dpop_proof`)
rejection ahead of the client-authentication error (FAPI2) without burning the
single-use code.

# `finalize`

```elixir
@spec finalize(module(), String.t(), Attesto.AuthorizationCode.Grant.t()) :: :ok
```

Finalize a fully completed redemption: record the reuse marker
(`consumed_success`) for `code`'s grant.

Call this only AFTER the full token response has been successfully built. It
is split from `redeem/4` so redemption is atomic - `redeem/4` claims the code
(single use, via `take/1`) and validates it, but defers this marker so a
failure in the caller's downstream issuance (mint, refresh-token persistence,
a host callback fault) does NOT leave a spent-but-tokenless code recorded as a
completed redemption (which would make a legitimate retry look like a reuse
attack and revoke the family). A no-op for stores that do not implement
`c:Attesto.CodeStore.mark_consumed/2`.

# `issue`

```elixir
@spec issue(module(), issue_attrs(), keyword()) ::
  {:ok, String.t()} | {:error, issue_error()}
```

Mint a single-use authorization code and persist it via `store`.

`attrs` MUST carry `:client_id`, `:redirect_uri`, and `:subject`.
Optional `:code_challenge` binds the code to PKCE; when present,
`:code_challenge_method` must be `"S256"` if given. Optional `:scope` (a
list of strings, default `[]`), `:dpop_jkt` (binds the code to a DPoP key),
`:family_id` (a
non-empty string linking this code to the refresh-token family it will
spawn; rides onto the redeemed `Grant` and is what code-reuse detection
replays - see the moduledoc), and `:claims` (an opaque host context map
round-tripped to `redeem/4`).

Options: `:ttl` (seconds the code is valid, default
60) and `:now` (clock override).

Returns `{:ok, code}` with the plaintext code to hand the client. Only
the code's hash is stored. Returns `{:error, reason}` on malformed
`attrs`.

# `redeem`

```elixir
@spec redeem(module(), String.t(), redeem_params(), keyword()) ::
  {:ok, Attesto.AuthorizationCode.Grant.t()} | {:error, redeem_error()}
```

Validate and consume a code at the token endpoint.

`params` MUST carry the `:redirect_uri` (matched exactly against the one
in the authorization request), the `:code_verifier` (checked against the
stored PKCE challenge), and the `:client_id` of the redeeming client. By
default client binding is fail-closed: since every stored code carries a
`client_id`, redemption MUST present one (`:client_required` if absent,
`:client_mismatch` if wrong) - this stops a code issued to one client
being redeemed by another (RFC 6749 §4.1.3). A caller that cannot
authenticate the client and relies on PKCE alone passes
`allow_missing_client_id?: true` in `opts`. `:dpop_jkt` is required iff
the code was DPoP-bound at `issue/3`; if the code was not bound, a presented
`:dpop_jkt` is allowed and can be used by the caller to mint a DPoP-bound
access token.

The code is consumed (single use) before validation. Returns
`{:ok, %Attesto.AuthorizationCode.Grant{}}` with the validated grant
context, or `{:error, reason}`.

When the `store` implements optional reuse tracking (see
`Attesto.CodeStore`), a second redemption of a code that was already
successfully redeemed returns `{:error, {:reuse, meta}}` rather than
`{:error, :invalid_grant}`. `meta` carries the first redemption's
`:family_id` and `:subject` so the caller can revoke the descendant
family (OAuth 2.0 Security BCP §4.13). Codes the store has never seen
remain `{:error, :invalid_grant}`.

---

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