AttestoPhoenix.AuthorizationServer.RequestPolicy (AttestoPhoenix v2.2.0)

Copy Markdown View Source

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

client_native?(config, client)

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

client_public?(config, client)

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

redirect_uri_matching(config, client)

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

registered_redirect_uris(config, client)

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

require_nonce?(config)

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

require_pkce?(config, client)

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

validate(config, client, params, extra_opts \\ [])

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.