AshAuthentication.Oauth2Server.CIMD (ash_authentication_oauth2_server v0.3.0)

Copy Markdown View Source

OAuth Client ID Metadata Documents (draft-ietf-oauth-client-id-metadata-document).

A CIMD client uses an HTTPS URL as its client_id; the URL points at a JSON document describing the client (client_name, redirect_uris, …). This lets clients and servers with no prior relationship interoperate without a registration endpoint — the mechanism the MCP spec (2026-07-28) now recommends over Dynamic Client Registration.

Enable it with cimd_enabled?: true on your Oauth2Server module. When enabled, the RFC 8414 metadata document advertises client_id_metadata_document_supported: true and the /oauth/authorize endpoint accepts URL-shaped client_ids.

How resolved clients are stored

A validated metadata document is upserted into your regular client resource, keyed by a cimd_url attribute — so authorization codes, refresh tokens, and consent records keep referencing clients by their UUID primary key exactly as they do for registered clients. The document is re-fetched (subject to HTTP caching, honoured via AshAuthentication.Oauth2Server.CIMD.Cache) on each authorization request, so renamed clients and rotated redirect URIs are picked up; the token, revocation, and error-redirect paths resolve the URL from the database only and never trigger a fetch.

Client resource requirements

Your client resource needs (the installer scaffolds these for new apps):

attribute :cimd_url, :string, public?: true

identity :by_cimd_url, [:cimd_url]

create :register_cimd do
  upsert? true
  upsert_identity :by_cimd_url
  accept [:cimd_url, :client_name, :redirect_uris, :grant_types,
          :response_types, :token_endpoint_auth_method, :scope]
end

Fetching and SSRF

The fetch of an attacker-suppliable URL is the risky part; it goes through the :cimd_fetcher module (default: AshAuthentication.Oauth2Server.CIMD.ReqFetcher, which requires the optional req dependency and applies a strict outbound policy — see its docs). The authorize endpoint is the only place a fetch can be triggered, and it is unauthenticated by protocol design — rate-limit it like the other protocol endpoints (see the "Rate limiting" section in AshAuthentication.Oauth2Server).

Summary

Functions

Look up an already-resolved CIMD client by its URL — database only, no fetch. Used by the token, revocation, and error-redirect paths, where the client must have been resolved during authorization already.

Resolve a URL client_id to a client record: fetch the metadata document (through the cache), validate it, and upsert it into the client resource.

Is this client_id value a CIMD-style URL?

Validate a fetched metadata document against the CIMD draft and this server's supported client shape.

Functions

find_client(server, url, opts \\ [])

@spec find_client(server :: module(), url :: String.t(), opts :: keyword()) ::
  {:ok, Ash.Resource.record()} | :error

Look up an already-resolved CIMD client by its URL — database only, no fetch. Used by the token, revocation, and error-redirect paths, where the client must have been resolved during authorization already.

resolve_client(server, url, opts \\ [])

@spec resolve_client(server :: module(), url :: String.t(), opts :: keyword()) ::
  {:ok, Ash.Resource.record()} | {:error, String.t()}

Resolve a URL client_id to a client record: fetch the metadata document (through the cache), validate it, and upsert it into the client resource.

Returns {:ok, client} or {:error, description} — the description is safe to include in an OAuth invalid_client error response; fetch and data-layer details are logged, not returned.

url_client_id?(client_id)

@spec url_client_id?(term()) :: boolean()

Is this client_id value a CIMD-style URL?

Per the draft, a CIMD client_id must be an https URL — anything else is treated as an ordinary (opaque) client identifier.

validate_document(document, url)

@spec validate_document(document :: map(), url :: String.t()) ::
  :ok | {:error, String.t()}

Validate a fetched metadata document against the CIMD draft and this server's supported client shape.

Checks, in order: the document's client_id matches the URL it was fetched from exactly (no normalization — per the draft), a client_name is present (required by the MCP spec; it's what the consent screen shows the user), and the redirect_uris / grant_types / response_types / token_endpoint_auth_method fields pass the same validation Dynamic Client Registration applies.