# `AttestoPhoenix.ClientIdMetadata.Fetcher.Req`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/client_id_metadata/fetcher/req.ex#L2)

The default, SSRF-guarded Client ID Metadata Document fetcher - CIMD
(`draft-ietf-oauth-client-id-metadata-document-01`, IETF OAuth WG).

This is the only component of the CIMD feature that makes an outbound request,
so it is where the draft's Security Considerations are enforced. Implements
`AttestoPhoenix.ClientIdMetadata.Fetcher` over `Req` -> `Finch` -> `Mint`.

## SSRF algorithm (draft Security Considerations)

`fetch/2` runs the following, erroring closed at the first failure:

  1. **Re-validate** the URL is `https` and satisfies the draft §2 grammar via
     `Attesto.ClientIdMetadata.validate_client_id/1` (defense in depth - the
     caller is never trusted). Failure -> `{:error, {:invalid_url, reason}}`.
  2. **Resolve** the host to A/AAAA records through the injectable `:resolver`
     (defaults to `:inet.getaddrs/2` for both `:inet` and `:inet6`). No
     records -> `{:error, :unresolvable}`. The resolver seam lets tests inject
     addresses without real DNS, and is the hook the DNS-rebinding defense
     below pins against.
  3. **Reject special-use IPs (RFC 6890).** Every resolved address is checked
     against `special_use_ip?/1`; if any is special-use the fetch is refused
     with `{:error, {:blocked_ip, ip}}`. Loopback is permitted only when
     `:allow_loopback` is `true` (development).
  4. **Pin to a validated IP.** The request is dialed at one checked address
     (`Mint`'s `:hostname` connect option keeps TLS SNI, certificate hostname
     verification, and the `Host` header on the original hostname while the
     socket targets the pinned IP), closing the DNS-rebinding TOCTOU between
     the check in step 3 and the connect: the name cannot be re-resolved to an
     internal address after it was validated.
  5. **GET** with `Accept: application/json`, connect and receive timeouts
     (`:request_timeout_ms`, default `5_000`), and **redirects disabled**
     (draft MUST). Any redirect is surfaced as its 3xx status.
  6. **Status** must be `200`; any other status -> `{:error, {:status, n}}`.
  7. **Content-Type** must be `application/json` or `application/<x>+json`;
     otherwise `{:error, :bad_content_type}`.
  8. **Size cap.** The body is refused once it exceeds `:max_document_bytes`
     (default `5_120`) -> `{:error, :too_large}`.

On success returns `{:ok, %{body: body, cache_control: directives}}` where
`directives` are the parsed `Cache-Control` / `Expires` freshness hints
(`RFC 9111`) for the caller to clamp and store.

## Options

  * `:resolver` - a 2-arity DNS resolver
    `(charlist_host, :inet | :inet6 -> {:ok, [:inet.ip_address()]} | {:error, term()})`.
    Defaults to `:inet.getaddrs/2`. Injected by tests to exercise the SSRF
    guard and the DNS-rebinding pin without real DNS.
  * `:allow_loopback` - when `true`, loopback addresses (`127.0.0.0/8`, `::1`)
    are permitted (the draft's "AS runs on loopback" exception). Default
    `false`.
  * `:max_document_bytes` - body size cap. Default `5_120` (draft's
    recommended 5 KB).
  * `:request_timeout_ms` - connect and receive timeout. Default `5_000`.
  * `:req_options` - extra options merged into the underlying `Req` request
    (e.g. test transport overrides). Escape hatch; not part of the public
    contract.

# `ip`

```elixir
@type ip() :: :inet.ip_address()
```

A resolved IP address, as returned by `:inet.getaddrs/2`.

# `fetch`

```elixir
@spec fetch(
  String.t(),
  keyword()
) :: {:ok, AttestoPhoenix.ClientIdMetadata.Fetcher.result()} | {:error, term()}
```

Fetch a validated CIMD `client_id` URL under the SSRF algorithm documented on
this module. See `AttestoPhoenix.ClientIdMetadata.Fetcher` for the contract.

# `special_use_ip?`

```elixir
@spec special_use_ip?(ip()) :: boolean()
```

Returns `true` iff `ip` is a special-use address (RFC 6890) an authorization
server MUST NOT dereference for CIMD: loopback, private, link-local, CGNAT,
`0.0.0.0/8`, multicast, the reserved/documentation ranges, the IPv6
equivalents (`fc00::/7`, `fe80::/10`, `ff00::/8`, `::1`, `::`), and every IPv6
form that embeds an IPv4 - IPv4-mapped (`::ffff:0:0/96`), NAT64
(`64:ff9b::/96`), 6to4 (`2002::/16`), and IPv4-compatible (`::/96`) - which is
unwrapped to its embedded IPv4 and re-checked, so an internal IPv4 cannot be
smuggled past the guard through any of them.

This is the single source of truth for the guard's CIDR table; the
`:allow_loopback` exception is applied by the caller (`fetch/2`), not here, so
this predicate always reports loopback as special-use.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
