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:
Attesto.ClientIdMetadata- the pure, network-free URL grammar (validate_client_id/1) and document validation (validate_document/2);AttestoPhoenix.ClientIdMetadata.Cache- remembers a validated document so not every authorization request reaches the network;AttestoPhoenix.ClientIdMetadata.Fetcher- the single SSRF-guarded outboundGET;AttestoPhoenix.Config- the host's:client_id_metadataoptions (fetcher, cache, size/timeout caps, cache-TTL bounds, host allow/block lists).
Algorithm (resolve/2)
Each step errors closed; later steps run only when every earlier one passed:
- Grammar (fail fast, no network).
validate_client_id/1rejects a non-CIMDclient_idbefore any host check or socket - a request whoseclient_idis 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}}. - 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}}. - Cache.
Cache.get/1is 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. - Fetch. On a miss the configured
:fetcherperforms the SSRF-guardedGET, 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). - Decode + validate. The body is JSON-decoded and handed to
validate_document/2, which enforces theclient_idmatch 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. - Cache + return. Only after validation succeeds is the document stored
via
Cache.put/3, with anexpires_atderived from the response'sCache-Control: max-age/Expiresfreshness 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
@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
@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.