Neutral request-fact helpers the OAuth 2.0 / OIDC flows derive from a Plug.Conn.
Authorization-server endpoints need a handful of transport-level facts that are
not safe to read straight off the Plug.Conn when the listener sits behind a
reverse proxy:
- the client IP, honoring
X-Forwarded-Foronly for trusted proxies; - whether the request effectively arrived over HTTPS (RFC 8446), honoring
a trusted
X-Forwarded-Proto: httpshop; - the canonical request URL (
htu) and method (htm) a DPoP proof is bound to, per RFC 9449 §4.2 / §4.3; - the peer certificate DER presented at the TLS layer, used for the
RFC 8705 §3 mutual-TLS
cnfbinding.
Every forwarded-header-derived fact is gated on a trusted-proxy allowlist. A
request that arrives from a peer outside that allowlist with forged
X-Forwarded-* headers is a spoofing attempt: the headers are dropped and the
fact is derived from the direct connection only. This is fail-closed by
construction, an untrusted peer cannot assert https, cannot redirect the
DPoP htu, and cannot forge a client IP.
The trust boundary, the HTTPS requirement, and the optional certificate
extractor are read from AttestoPhoenix.Config; this module never hardcodes
deployment policy.
Trusted-proxy allowlist
config.trusted_proxies controls whether X-Forwarded-* headers are honored.
It accepts a list whose elements are any of:
:loopback- matches127.0.0.0/8and::1.:any- matches every peer. Only safe when another mechanism (firewall, ingress ACL) guarantees that only the proxy can reach the app port. Prefer explicit CIDRs.- an IP tuple (
{10, 0, 0, 1}/ an 8-element IPv6 tuple) - exact match. - a binary CIDR string (
"10.0.0.0/8","::1/128") - subnet match.
The default ([]) trusts no proxy, so forwarded headers are never honored
unless the host opts in.
Summary
Functions
Returns the canonical request URL (htu) the DPoP proof is bound to, per
RFC 9449 §4.3: the request URI without its query or fragment.
Returns the peer certificate DER for the RFC 8705 §3 mutual-TLS cnf binding,
or nil when no client certificate was presented.
Returns :ok when the request satisfies the configured transport policy, or
{:error, :insecure_transport} when config.require_https is set and the
request did not effectively arrive over HTTPS.
Returns the client IP as a string, or nil if it cannot be determined.
Returns true when conn.remote_ip falls inside config.trusted_proxies.
Returns the HTTP method (htm) the DPoP proof is bound to, per RFC 9449 §4.2.
Returns true when the request effectively arrived over HTTPS.
Functions
@spec canonical_url(Plug.Conn.t(), AttestoPhoenix.Config.t()) :: String.t()
Returns the canonical request URL (htu) the DPoP proof is bound to, per
RFC 9449 §4.3: the request URI without its query or fragment.
When config.htu is set, that callback is used so a host can fully override
URL reconstruction (e.g. for a proxy topology this module does not model).
Otherwise the URL is built from the effective scheme/host/port, which honor
X-Forwarded-Proto / X-Forwarded-Host / X-Forwarded-Port only when the
request comes from a trusted proxy. An untrusted peer therefore cannot
redirect the htu check by injecting forwarded headers: the URL falls back to
the direct connection's authority and the proof either verifies against the
real listener URL or fails on its signature.
@spec cert_der(Plug.Conn.t(), AttestoPhoenix.Config.t()) :: binary() | nil
Returns the peer certificate DER for the RFC 8705 §3 mutual-TLS cnf binding,
or nil when no client certificate was presented.
When config.cert_der is set (required by AttestoPhoenix.Config whenever
mtls_enabled is true), that callback extracts the DER; this is the supported
override for proxy topologies that surface the client certificate in a header
rather than on the TLS socket. Otherwise the certificate is read from the
connection's peer data, which the underlying adapter populates when the TLS
socket negotiated client authentication.
@spec check_https(Plug.Conn.t(), AttestoPhoenix.Config.t()) :: :ok | {:error, :insecure_transport}
Returns :ok when the request satisfies the configured transport policy, or
{:error, :insecure_transport} when config.require_https is set and the
request did not effectively arrive over HTTPS.
This is the fail-closed transport check the token and protected-resource endpoints run before touching a credential: a bearer token or client secret that has already crossed a plain-HTTP hop must be treated as compromised, so the request is refused rather than served or redirected (a redirect would have the client replay the exposed credential).
@spec client_ip(Plug.Conn.t(), AttestoPhoenix.Config.t()) :: String.t() | nil
Returns the client IP as a string, or nil if it cannot be determined.
When the request comes from a trusted proxy and carries X-Forwarded-For, the
left-most entry (the original client per RFC 7239 / the de-facto
X-Forwarded-For convention) is returned. Otherwise the direct connection's
remote_ip is used. An untrusted peer cannot forge the client IP this way: its
X-Forwarded-For is ignored entirely.
@spec from_trusted_proxy?(Plug.Conn.t(), AttestoPhoenix.Config.t()) :: boolean()
Returns true when conn.remote_ip falls inside config.trusted_proxies.
This is the single trust gate that governs whether any X-Forwarded-* header
is honored. It is exposed so callers that need a custom forwarded-header read
can apply the same boundary rather than re-implementing it and risking drift.
@spec http_method(Plug.Conn.t()) :: String.t()
Returns the HTTP method (htm) the DPoP proof is bound to, per RFC 9449 §4.2.
The method is taken verbatim from the request; it is not derived from any forwarded header.
@spec https?(Plug.Conn.t(), AttestoPhoenix.Config.t()) :: boolean()
Returns true when the request effectively arrived over HTTPS.
The effective scheme is the connection scheme, upgraded to https when a
trusted proxy forwards X-Forwarded-Proto: https. An untrusted peer's
forwarded scheme is ignored, so a plain-HTTP hop cannot masquerade as TLS.