Client-ID Metadata Documents: a client_id that is itself an https URL serving
the client's own metadata.
This is how claude.ai avoids registering with every MCP server it meets — and
the only reason it is safe is that the fetch is fenced. The URL is chosen by
whoever is calling, so Noizu.MCP.Auth.Server.SSRF runs first on every fetch:
https only, port 443, every resolved address public (IPv4, IPv6 and
v4-mapped-v6, 169.254.169.254 included), no redirects followed, 64 KiB cap,
5 s timeout.
Claude only offers CIMD when the metadata document advertises both
client_id_metadata_document_supported: true and "none" in
token_endpoint_auth_methods_supported; MetadataPlug emits both when
cimd: [enabled: true].
The fetcher is injectable
cimd: [enabled: true, fetcher: {MyApp.Fetcher, []}]Defaults to Noizu.MCP.Auth.Server.CIMD.ReqFetcher when :req is available.
A fetcher receives (url, opts) and must honour Noizu.MCP.Auth.Server.SSRF.fetch_opts/0
— the caps are the security property, not a suggestion.
Caching
A fetched document is stored as a :cimd client with cimd_expires_at; a stale
entry is re-fetched with If-None-Match and a 304 just extends it. A fetch
failure with a cached document still in hand keeps the cached one: a
transient DNS blip should not lock every agent out.
Summary
Types
A fetcher's answer. {:ok, %{status:, body:, headers:}}, where body is a
decoded map, or {:not_modified, etag} for a 304.
Functions
True when client_id looks like a CIMD URL rather than a registered id.
Fetch and validate a document, ignoring any cache. Returns the Client built
from it.
Resolve a CIMD client_id to a Client, fetching if there is no fresh cached
document.
Types
@type response() :: {:ok, %{status: pos_integer(), body: map(), headers: map()}} | {:not_modified, String.t() | nil} | {:error, term()}
A fetcher's answer. {:ok, %{status:, body:, headers:}}, where body is a
decoded map, or {:not_modified, etag} for a 304.
Functions
True when client_id looks like a CIMD URL rather than a registered id.
@spec fetch( String.t(), Noizu.MCP.Auth.Server.Client.t() | nil, Noizu.MCP.Auth.Server.Config.t() ) :: {:ok, Noizu.MCP.Auth.Server.Client.t()} | {:error, Noizu.MCP.Auth.Server.Errors.t()}
Fetch and validate a document, ignoring any cache. Returns the Client built
from it.
@spec resolve( String.t(), Noizu.MCP.Auth.Server.Client.t() | nil, Noizu.MCP.Auth.Server.Config.t() ) :: {:ok, Noizu.MCP.Auth.Server.Client.t(), :cached | :fetched} | {:error, Noizu.MCP.Auth.Server.Errors.t()}
Resolve a CIMD client_id to a Client, fetching if there is no fresh cached
document.
Returns {:ok, client, :cached | :fetched} so the caller knows whether to
persist.