Conn-free resolution of the per-request authorization-request validation policy shared by the authorization endpoint and the PAR endpoint.
Both endpoints validate the same authorization request the same way (RFC 9126
§2.1: "validate the pushed request as it would an authorization request sent
to the authorization endpoint"), so the policy inputs Attesto.AuthorizationRequest.validate/2
needs - the client's registered redirect URIs (RFC 6749 §3.1.2.3), how they
are matched (RFC 8252 §7.3), whether PKCE is required (RFC 9700 §2.1.1), and
whether nonce is required (OIDC Core §3.1.2.1) - are resolved here once,
from %AttestoPhoenix.Config{} and the opaque host client, rather than
duplicated per endpoint. This module reads only data: it touches no conn and
carries no policy of its own beyond the fail-closed defaults documented on
each function.
Summary
Functions
Classify the client as an installed native application (RFC 8252 / BCP 212)
via the host's :client_native? callback.
Classify the client as public via the host's :client_public? callback.
How the request redirect_uri is matched against the registered set
(RFC 6749 §3.1.2.3, RFC 8252 §7.3).
The client's registered redirect URIs (RFC 6749 §3.1.2.3).
The host's OP nonce policy flag (OIDC Core §3.1.2.1).
Whether PKCE is required for this client (RFC 7636 §4.3 / RFC 9700 §2.1.1, RFC 8252 §8.1).
Validate params as an authorization request for client, resolving the
redirect-URI/PKCE/nonce policy from config and delegating to
Attesto.AuthorizationRequest.validate/2.
Functions
@spec client_native?(AttestoPhoenix.Config.t(), term()) :: boolean()
Classify the client as an installed native application (RFC 8252 / BCP 212)
via the host's :client_native? callback.
Absent the callback, the client is NOT native. Unlike client_public?/2 this
cannot fail closed by defaulting to true: "native" gates one relaxation
(§7.3 loopback ports) alongside its restrictions, and a host that has not
classified its clients must get the unmodified RFC 6749 behavior.
@spec client_public?(AttestoPhoenix.Config.t(), term()) :: boolean()
Classify the client as public via the host's :client_public? callback.
Absent the callback, fail closed by treating the client as public, so PKCE stays required (a confidential exemption demands a deliberate host classification).
@spec redirect_uri_matching(AttestoPhoenix.Config.t(), term()) :: Attesto.RedirectURI.matching()
How the request redirect_uri is matched against the registered set
(RFC 6749 §3.1.2.3, RFC 8252 §7.3).
:exact - the RFC 6749 §3.1.2.3 simple string comparison - unless BOTH gates
are open: the host enabled native_apps: [loopback_redirect: true] AND marked
this client native via :client_native?. Only then does the client get
:exact_allow_loopback_port, under which its http://127.0.0.1/... /
http://[::1]/... redirect URI matches on any port (see
Attesto.RedirectURI for exactly how narrow that exception is).
Two gates rather than one: this is the only rule in the RFC 8252 profile that
relaxes a check, so it takes both a server-wide decision and a per-client
classification, and it can never widen matching for a client the host did not
deliberately mark. A CIMD client is never native - its client_id is an
https URL and its redirect_uris come from the document, which the
authorization endpoint additionally holds to the same origin.
@spec registered_redirect_uris(AttestoPhoenix.Config.t(), term()) :: [String.t()]
The client's registered redirect URIs (RFC 6749 §3.1.2.3).
For a CIMD client ({:cimd, metadata},
draft-ietf-oauth-client-id-metadata-document-01) the document is the
registration, so the registered set is the document's own redirect_uris
(RFC 9700) and the host's per-client callback is never consulted. For a
registered client the set is resolved through the host's
:client_redirect_uris callback; an absent callback or a non-list return
resolves to [], which rejects every request with an unregistered redirect
URI (fail closed).
@spec require_nonce?(AttestoPhoenix.Config.t()) :: boolean()
The host's OP nonce policy flag (OIDC Core §3.1.2.1).
Returns the raw :require_nonce configuration. The OIDC openid-scope gate is
NOT applied here: it must run on the EFFECTIVE request (after any signed
request object is merged), which only Attesto.AuthorizationRequest.validate/2
sees. Applying the gate on the raw outer params here would let a direct JAR
carrying scope=openid only inside the signed object bypass the requirement.
@spec require_pkce?(AttestoPhoenix.Config.t(), term()) :: boolean()
Whether PKCE is required for this client (RFC 7636 §4.3 / RFC 9700 §2.1.1, RFC 8252 §8.1).
A public client MUST use PKCE, so client_public?/2 forces it regardless of
config. A sender-constrained client (DPoP or mTLS) is a FAPI 2.0 client, and
FAPI 2.0 Security Profile §5.3.1.2 / RFC 9700 §2.1.1 require PKCE for it even
though it authenticates confidentially - so client_requires_dpop?/2 and
client_requires_mtls?/2 force it too. RFC 8252 §8.1 requires it for a native
app, so client_native?/2 forces it as well; the requirement is stated for
public native clients, and applying it to any native client is strictly
safer - a native app that claims to be confidential cannot actually hold a
secret (§8.4). For any other confidential client the global :require_pkce
flag applies (default true). Fail closed: absent the host's deliberate
opt-out, PKCE is required.
RFC 8252 §8.1 also requires S256 rather than plain. That needs no policy
here: Attesto.AuthorizationRequest rejects code_challenge_method=plain
and any non-S256 method unconditionally, for every client.
@spec validate(AttestoPhoenix.Config.t(), term(), map(), keyword()) :: {:ok, Attesto.AuthorizationRequest.t()} | {:error, Attesto.AuthorizationRequest.error()}
Validate params as an authorization request for client, resolving the
redirect-URI/PKCE/nonce policy from config and delegating to
Attesto.AuthorizationRequest.validate/2.
This is the shared entry point both the authorization endpoint and the PAR
endpoint use so a request is validated identically wherever it arrives
(RFC 9126 §2.1). Pass extra_opts to thread request-object verification
inputs (:request_object_jwks, :request_object_audience,
:request_object_policy) when the params still carry an unverified signed
request object; omit them when the object has already been verified and
merged.