# `AttestoPhoenix.ConsentGrant`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/consent_grant.ex#L1)

The request binding a single-use consent grant is tied to, and the canonical
hash over it (RFC 6749 §4.1.1).

A consent grant must approve *exactly* the authorization request the resource
owner saw — the same client, redirect URI, scope set, PKCE challenge, and
PKCE method — and nothing else. This module builds that binding from either
raw authorization params (`binding_from_params/2`, used by the consent-screen
mint action) or from a validated `%Attesto.AuthorizationRequest{}` (`binding/2`,
used by the live `/authorize` consume side). Both builders feed the same
canonical field list and therefore yield the same `binding_hash/1` for the
equivalent request.

## Canonical binding

The binding is the tuple
`(subject, client_id, redirect_uri, scope, code_challenge, code_challenge_method)`:

  * `subject` - the OIDC `sub` of the resource owner who consented. Binding to
    the subject stops one user's consent token from approving another's request.
  * `client_id` / `redirect_uri` - the requesting client and the exact
    redirect URI the code will be returned to (RFC 6749 §3.1.2). Binding both
    stops a consent shown for one client/redirect from authorizing a different
    one.
  * `scope` - the requested scope set. Order is **not** significant (RFC 6749
    §3.3), so the set is sorted before hashing: a request with
    `scope=openid profile` and one with `scope=profile openid` hash
    identically, while adding or dropping a scope changes the hash.
  * `code_challenge` - the PKCE challenge (RFC 7636 §4.3), or the empty string
    when the request carries none. Binding it stops a consent from being
    replayed against a request that swapped in a different PKCE challenge.
  * `code_challenge_method` - the PKCE method (`S256`, RFC 7636 §4.3), or the
    empty string when the request carries no PKCE challenge. Binding it stops
    a consent granted for an `S256` request from being reused for a `plain`
    request with the same challenge value.

`binding_hash/1` is SHA-256 over the newline-joined canonical fields,
URL-base64 encoded (no padding). It is stable across the mint and consume
sides because both derive it from this one function.

# `binding`

```elixir
@type binding() :: %{
  subject: String.t(),
  client_id: String.t(),
  redirect_uri: String.t(),
  scope: [String.t()],
  code_challenge: String.t() | nil,
  code_challenge_method: String.t() | nil
}
```

The fields a consent grant is bound to. `subject`, `client_id`, and
`redirect_uri` are required; `scope` is a (possibly empty) list whose order is
normalized away; `code_challenge` and `code_challenge_method` are `nil` when
the request carries no PKCE challenge.

# `binding`

```elixir
@spec binding(Attesto.AuthorizationRequest.t(), String.t()) :: binding()
```

Builds the consent binding for `request` consented to by `subject`.

`request` is the validated `%Attesto.AuthorizationRequest{}` (the front-channel
request whose `client_id`, `redirect_uri`, `scope`, `code_challenge`, and
`code_challenge_method` the user saw); `subject` is the authenticated resource
owner's OIDC `sub`. The returned map feeds `binding_hash/1` and the store's
`mint/2` / `consume/2`.

# `binding_from_params`

```elixir
@spec binding_from_params(map(), String.t()) :: binding()
```

Builds the consent binding from raw OAuth authorization params and `subject`.

Use this on the consent-screen mint side, where the host has the raw
string-keyed params that reached `/authorize` / the consent action rather than
the validated `%Attesto.AuthorizationRequest{}`. The consume side should keep
using `binding/2`.

`params` is read by string keys (`"client_id"`, `"redirect_uri"`, `"scope"`,
`"code_challenge"`, and `"code_challenge_method"`). Missing `"scope"` becomes
`[]`; a present scope string is split on spaces; missing PKCE fields become
`nil`. Unknown params are ignored. For the equivalent validated request,
`binding_hash(binding_from_params(params, subject)) ==
binding_hash(binding(request, subject))`.

# `binding_hash`

```elixir
@spec binding_hash(binding()) :: String.t()
```

The canonical binding hash for `binding`.

SHA-256 over the newline-joined canonical fields, URL-base64 encoded without
padding. The scope set is order-normalized (sorted then space-joined) so scope
order is not significant (RFC 6749 §3.3); a missing `code_challenge` and
`code_challenge_method` each hash as the empty string. Identical on the mint
and consume sides for the same request.

---

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