AttestoPhoenix.RequestContext (AttestoPhoenix v0.6.1)

Copy Markdown View Source

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-For only for trusted proxies;
  • whether the request effectively arrived over HTTPS (RFC 8446), honoring a trusted X-Forwarded-Proto: https hop;
  • 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 cnf binding.

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 - matches 127.0.0.0/8 and ::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

canonical_url(conn, config)

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

cert_der(conn, config)

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

check_https(conn, config)

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

client_ip(conn, config)

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

from_trusted_proxy?(conn, config)

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

http_method(conn)

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

https?(conn, config)

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