AttestoPhoenix.AuthorizationServer.SenderConstraint (AttestoPhoenix v0.8.0)

Copy Markdown View Source

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

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 - but only if the client does not require a sender constraint.

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.

Summary

Types

The resolved sender-constraint binding.

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

Functions

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.

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

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

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

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

Resolve the sender-constraint binding for a token request.

Types

binding()

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

The resolved sender-constraint binding.

input()

@type input() :: %{
  optional(:dpop_proof) => String.t() | nil,
  optional(:mtls_cert_der) => binary() | nil,
  optional(:http_uri) => String.t() | nil,
  optional(:http_method) => String.t() | nil
}

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

Functions

binding_jkt(arg1)

@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?(config, client)

@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?(config, client)

@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(arg1)

@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(config, client, binding)

@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(config, input, client)

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