AttestoPhoenix.ClientIdMetadata.Resolver (AttestoPhoenix v0.9.3)

Copy Markdown View Source

Resolves a Client ID Metadata Document URL into a client - CIMD (draft-ietf-oauth-client-id-metadata-document-01, IETF OAuth WG).

CIMD lets a client identify itself with no prior registration by using an HTTPS URL as its client_id; the authorization server dereferences that URL to a JSON client metadata document and uses it as the client. This module is the orchestrator that turns a presented CIMD client_id into the normalized client map attesto's resolution expects, gluing together the four collaborators the rest of the feature provides:

Algorithm (resolve/2)

Each step errors closed; later steps run only when every earlier one passed:

  1. Grammar (fail fast, no network). validate_client_id/1 rejects a non-CIMD client_id before any host check or socket - a request whose client_id is not a well-formed CIMD URL never reaches the resolver in normal operation, but the check is repeated here so a direct caller is never trusted. Failure -> {:error, {:invalid_client_id, reason}}.
  2. Host policy. The URL's host is screened against the configured :blocked_hosts (always refused) and, when set, :allowed_hosts (only these are permitted). A blocked or non-allowlisted host is refused before the cache or the network is consulted, so a policy change takes effect immediately. Failure -> {:error, {:blocked_host, host}}.
  3. Cache. Cache.get/1 is consulted; a live (unexpired) entry is returned as the client without any fetch. The cache only ever holds a previously validated document, so a hit needs no re-validation.
  4. Fetch. On a miss the configured :fetcher performs the SSRF-guarded GET, honoring :max_document_bytes, :request_timeout_ms, and :allow_loopback. Any transport failure, non-200, redirect, non-JSON content type, or oversize body is an {:error, _} and is never cached (draft §6 / RFC 9111).
  5. Decode + validate. The body is JSON-decoded and handed to validate_document/2, which enforces the client_id match and the no-symmetric-secret rules and normalizes the document into the client shape. A malformed JSON body or an invalid document is an {:error, _} and is never cached.
  6. Cache + return. Only after validation succeeds is the document stored via Cache.put/3, with an expires_at derived from the response's Cache-Control: max-age / Expires freshness directives clamped to the configured :cache_ttl_bounds (RFC 9111). The normalized client map is returned.

The returned client is shaped identically to a host :load_client result, so downstream resolution (scopes, redirect-URI match, JARM, DPoP) needs no CIMD-specific handling.

Summary

Types

A reason resolve/2 refused to produce a client. :invalid_client_id and :blocked_host are local policy failures (no network); {:fetch, reason} wraps a fetcher error; :invalid_json is an undecodable body; and a bare Attesto.ClientIdMetadata.document_error/0 is a validation failure.

Functions

Resolve a CIMD client_id URL into a normalized client map.

Types

error()

@type error() ::
  {:invalid_client_id, Attesto.ClientIdMetadata.url_error()}
  | {:blocked_host, String.t()}
  | {:fetch, term()}
  | :invalid_json
  | Attesto.ClientIdMetadata.document_error()

A reason resolve/2 refused to produce a client. :invalid_client_id and :blocked_host are local policy failures (no network); {:fetch, reason} wraps a fetcher error; :invalid_json is an undecodable body; and a bare Attesto.ClientIdMetadata.document_error/0 is a validation failure.

Functions

resolve(client_id, config)

@spec resolve(String.t(), AttestoPhoenix.Config.t()) ::
  {:ok, map()} | {:error, error()}

Resolve a CIMD client_id URL into a normalized client map.

Runs the algorithm documented on this module against the host's :client_id_metadata configuration in config. Returns {:ok, client} for a freshly fetched-and-validated document (now cached) or a live cache hit, or {:error, reason} for any local-policy, fetch, decode, or validation failure - none of which are ever cached.