Client ID Metadata Documents - 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 fetch a JSON client metadata document - the RFC 7591 Dynamic Client
Registration metadata field set - and uses it as the client.
This module is the pure, conn-free, HTTP-free half of that feature: it
decides whether a client_id is a CIMD URL (client_id_url?/1), validates
the URL against the draft §2 grammar (validate_client_id/1), and validates a
fetched document against the draft §2 content rules, normalizing it into the
client shape attesto's resolution expects (validate_document/2). The
load-bearing network half - the SSRF-guarded GET, redirect refusal, size cap,
and caching - lives in the Phoenix layer; this module never touches a socket
and pulls in no dependencies, the same discipline as
Attesto.AuthorizationRequest.
URL grammar (draft §2)
A CIMD client_id MUST:
- use the
httpsscheme; - have a path component;
- NOT contain a fragment;
- NOT contain userinfo (a
user:password@component); - NOT contain single-dot (
.) or double-dot (..) path segments.
Ports are allowed; a query is discouraged but allowed. A client_id that is
not a binary, or does not parse as a URL, is not a CIMD client_id - it is an
opaque identifier the host resolves through its own registry.
Document content (draft §2)
The fetched document's fields are the OAuth Dynamic Client Registration Metadata registry values (RFC 7591 §2). On top of that field set the draft requires:
- a
client_idmember equal to the URL by simple string comparison (mismatch ->{:error, :client_id_mismatch}); - NO shared symmetric secret:
client_secret/client_secret_expires_atMUST NOT be present ({:error, :symmetric_secret}), andtoken_endpoint_auth_methodMUST NOT be one ofclient_secret_basic,client_secret_post, orclient_secret_jwt({:error, :symmetric_auth_method}) - a CIMD client authenticates as a public client (none+ PKCE) or withprivate_key_jwt.
RFC 9700 requires registered redirect URIs, so a CIMD document MUST carry a
non-empty redirect_uris array of strings
({:error, :invalid_redirect_uris} otherwise).
Normalized client shape
validate_document/2 returns a string-keyed map carrying the RFC 7591 §2
client-metadata members the document supplied (client_id, redirect_uris,
and any of grant_types, response_types, scope, jwks, jwks_uri,
client_name, client_uri, logo_uri, token_endpoint_auth_method,
contacts). The shape matches the validated metadata the RFC 7591
registration path persists, so a CIMD client is consumed downstream (scope
resolution, redirect match, JARM, DPoP) exactly like a registered one. Absent
members are omitted rather than rendered as nil; an out-of-shape member
(e.g. a redirect_uris that is not a list of strings) is a validation error,
never silently dropped.
Summary
Types
A reason a fetched document fails the draft §2 content rules.
A reason a client_id URL fails the draft §2 grammar.
Functions
Returns true iff value is a CIMD client_id: a binary that parses as an
HTTPS URL satisfying the draft §2 grammar (a path, and no fragment, userinfo,
or single-/double-dot path segments).
Validate a client_id against the CIMD URL grammar
(draft-ietf-oauth-client-id-metadata-document-01 §2).
Validate a fetched client metadata document against the draft §2 content rules and normalize it into attesto's client shape.
Types
@type document_error() ::
:client_id_mismatch
| :symmetric_secret
| :symmetric_auth_method
| :invalid_redirect_uris
| :invalid_metadata
A reason a fetched document fails the draft §2 content rules.
@type url_error() ::
:not_a_url
| :not_https
| :no_path
| :has_fragment
| :has_userinfo
| :dot_segments
A reason a client_id URL fails the draft §2 grammar.
Functions
Returns true iff value is a CIMD client_id: a binary that parses as an
HTTPS URL satisfying the draft §2 grammar (a path, and no fragment, userinfo,
or single-/double-dot path segments).
This is the fast, allocation-light predicate the resolver uses to decide
whether a client_id is a CIMD URL before any network work; a client_id
that is not a binary, or fails the grammar, returns false. For the specific
failure reason use validate_client_id/1.
Validate a client_id against the CIMD URL grammar
(draft-ietf-oauth-client-id-metadata-document-01 §2).
Returns {:ok, %URI{}} for a well-formed CIMD client_id, or
{:error, reason} for the first rule it violates:
:not_a_url- not parseable as a URL with a host;:not_https- the scheme is nothttps;:no_path- no path component (or a bare/with nothing after it);:has_fragment- a fragment is present;:has_userinfo- auser:password@userinfo component is present;:dot_segments- the path contains a single-dot (.) or double-dot (..) segment.
The checks run in that order, so the returned reason is the first the URL fails. A query is permitted (discouraged by the draft, not rejected here).
@spec validate_document(String.t(), map()) :: {:ok, map()} | {:error, document_error()}
Validate a fetched client metadata document against the draft §2 content rules and normalize it into attesto's client shape.
client_id is the URL the document was fetched from (already validated by
validate_client_id/1); doc is the decoded JSON object. Returns
{:ok, metadata} - a string-keyed map carrying the validated, normalized
RFC 7591 §2 metadata members - or {:error, reason}:
:client_id_mismatch-doc["client_id"]is not equal toclient_idby simple string comparison (draft §2);:symmetric_secret-client_secretorclient_secret_expires_atis present (draft §2: no shared symmetric secret);:symmetric_auth_method-token_endpoint_auth_methodis one ofclient_secret_basic,client_secret_post, orclient_secret_jwt(draft §2);:invalid_redirect_uris-redirect_urisis absent, empty, or not a list of strings (RFC 9700 requires registered redirect URIs);:invalid_metadata- a carried-through member is present with the wrong shape (e.g. a non-stringscope, or agrant_typesthat is not a list of strings).
The returned map always carries client_id and redirect_uris; any of the
other RFC 7591 §2 members the document supplied
(grant_types, response_types, scope, jwks, jwks_uri, client_name,
client_uri, logo_uri, token_endpoint_auth_method, contacts) are
carried through, and absent members are omitted.