# `AttestoPhoenix.AuthorizationServer.SenderConstraint`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/authorization_server/sender_constraint.ex#L1)

Sender-constraint resolution for the token endpoint (RFC 9449 / RFC 8705),
as conn-free core.

This is the single place that turns the sender-constraint facts of a token
request - a presented DPoP proof (RFC 9449), a presented client certificate
(RFC 8705), and the canonical request URL/method the proof is bound to
(RFC 9449 §4.2 / §4.3) - together with the configured policy and the client's
binding requirements into either a resolved binding or an
`AttestoPhoenix.OAuthError`. The controller parses these facts off the
`Plug.Conn` (via `AttestoPhoenix.RequestContext` and the `DPoP` request
header) and passes them as a plain map; this module reads only data, never
touches a conn, and never emits an event.

## Input

`resolve/3` takes the validated `%AttestoPhoenix.Config{}`, the resolved
client, and an `input` map the controller builds from the request:

  * `:dpop_proof` - the first `DPoP` request-header value (RFC 9449 §4.1), or
    `nil` when the request carries no proof.
  * `:mtls_cert_der` - the peer certificate DER (RFC 8705 §3), or `nil` when
    no client certificate was presented.
  * `:http_uri` - the canonical request URL (`htu`) the proof is bound to
    (RFC 9449 §4.3).
  * `:http_method` - the HTTP method (`htm`) the proof is bound to
    (RFC 9449 §4.2); the token endpoint is reached by POST.

## Return value

`{:ok, binding, token_type}` where `binding` is one of `{:dpop, jkt}`,
`{:mtls, thumbprint}`, or `:none`, and `token_type` is the RFC 9449 §7.1 /
RFC 6750 presentation type (`"DPoP"` for a DPoP binding, `"Bearer"`
otherwise). On failure, `{:error, %AttestoPhoenix.OAuthError{}}`.

## Precedence and fail-closed policy

The client's *required* sender constraint is resolved first, and only the
matching constraint type can satisfy it:

  * A client that requires DPoP (RFC 9449) is bound only by a DPoP proof. A
    request that omits the proof - even one presenting a client certificate -
    is refused (`DPoP proof required`), never silently mTLS-bound.
  * A client that requires mTLS (RFC 8705 §3) is bound only by a client
    certificate. A request that omits the certificate - even one presenting a
    DPoP proof - is refused (`client certificate required`), never silently
    DPoP-bound.

A client's required constraint therefore cannot be satisfied by presenting a
*different* valid constraint: the per-client policy is enforced on its own
terms before any opportunistic binding is considered.

Only when the client requires neither constraint does opportunistic
precedence apply: DPoP takes precedence when a proof is presented
(RFC 9449 §5); otherwise an mTLS certificate binds the token to its
thumbprint; otherwise the token is an unbound Bearer.

RFC 8705 §3: a client configured to require certificate-bound tokens MUST NOT
be silently downgraded to a Bearer token when it calls without a certificate.
RFC 9449 is the DPoP equivalent: a client configured for DPoP-bound issuance
must present a proof at the token endpoint. The host's
`:client_requires_mtls?` / `:client_requires_dpop?` callbacks gate this; both
are read defensively and fail open only to "not required" when the host has
not supplied the callback (the constraints are off by default per
`:dpop_enabled` / `:mtls_enabled`).

## DPoP nonce challenge preserved

When a fresh DPoP nonce is required (RFC 9449 §8 / §9), the returned
`%AttestoPhoenix.OAuthError{}` carries the `use_dpop_nonce` code and the fresh
`DPoP-Nonce` value in its `:headers`, so the controller renders the header
verbatim alongside the error.

# `binding`

```elixir
@type binding() :: {:dpop, String.t()} | {:mtls, String.t()} | :none
```

The resolved sender-constraint binding.

# `input`

```elixir
@type input() :: %{
  optional(:dpop_proof) =&gt; String.t() | nil,
  optional(:mtls_cert_der) =&gt; binary() | nil,
  optional(:http_uri) =&gt; String.t() | nil,
  optional(:http_method) =&gt; String.t() | nil
}
```

The sender-constraint facts the controller derives from the request.

# `audit_metadata`

```elixir
@spec audit_metadata(AttestoPhoenix.Config.t(), input()) :: %{
  token_type: String.t(),
  sender_constraint: :none | :dpop | :mtls,
  cnf: nil
}
```

Sender-constraint audit metadata derivable from a token request.

This records the sender-constraint method attempted at the request boundary
using the same precedence as `resolve/3`, without verifying a DPoP proof or
certificate. It is intended for denial events, including failures that happen
before a binding can be resolved.

# `binding_jkt`

```elixir
@spec binding_jkt(binding()) :: String.t() | nil
```

The DPoP thumbprint a stateful grant (authorization-code redemption, refresh
rotation) binds to. Only DPoP flows through those engines' `:dpop_jkt` opt;
an mTLS binding carries no DPoP thumbprint.

# `client_requires_dpop?`

```elixir
@spec client_requires_dpop?(AttestoPhoenix.Config.t(), term()) :: boolean()
```

Whether the client requires DPoP-bound token issuance (RFC 9449).

Read defensively; fails open to "not required" when the host supplies no
`:client_requires_dpop?` callback.

# `client_requires_mtls?`

```elixir
@spec client_requires_mtls?(AttestoPhoenix.Config.t(), term()) :: boolean()
```

Whether the client requires certificate-bound token issuance (RFC 8705).

Read defensively; fails open to "not required" when the host supplies no
`:client_requires_mtls?` callback.

# `mint_opts`

```elixir
@spec mint_opts(binding()) :: keyword()
```

The `Attesto.Token.mint/3` confirmation opt for a resolved `binding`
(RFC 9449 / RFC 8705).

DPoP binds `cnf.jkt`; mTLS binds `cnf.x5t#S256` (the certificate thumbprint,
threaded so a real `cnf` is minted rather than dropped); an unbound binding
carries no opt.

# `refresh_binding_jkt`

```elixir
@spec refresh_binding_jkt(AttestoPhoenix.Config.t(), term(), binding()) ::
  String.t() | nil
```

The DPoP thumbprint to bind a refresh token to (RFC 9449 §8).

Public clients get DPoP-bound refresh tokens; for confidential clients the
refresh token stays bound to the authenticated `client_id` (RFC 6749 §6 /
§10.4) rather than one DPoP proof key, so no DPoP thumbprint is threaded.

# `resolve`

```elixir
@spec resolve(AttestoPhoenix.Config.t(), input(), term()) ::
  {:ok, binding(), String.t()} | {:error, AttestoPhoenix.OAuthError.t()}
```

Resolve the sender-constraint binding for a token request.

Returns `{:ok, binding, token_type}` or `{:error, %OAuthError{}}`. See the
module docs for the precedence rules and the input shape.

---

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