Attesto.ClientIdMetadata (Attesto v0.7.1)

Copy Markdown View Source

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 https scheme;
  • 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_id member equal to the URL by simple string comparison (mismatch -> {:error, :client_id_mismatch});
  • NO shared symmetric secret: client_secret / client_secret_expires_at MUST NOT be present ({:error, :symmetric_secret}), and token_endpoint_auth_method MUST NOT be one of client_secret_basic, client_secret_post, or client_secret_jwt ({:error, :symmetric_auth_method}) - a CIMD client authenticates as a public client (none + PKCE) or with private_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

document_error()

@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.

url_error()

@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

client_id_url?(value)

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

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_client_id(client_id)

@spec validate_client_id(String.t()) :: {:ok, URI.t()} | {:error, url_error()}

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 not https;
  • :no_path - no path component (or a bare / with nothing after it);
  • :has_fragment - a fragment is present;
  • :has_userinfo - a user: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).

validate_document(client_id, doc)

@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 to client_id by simple string comparison (draft §2);
  • :symmetric_secret - client_secret or client_secret_expires_at is present (draft §2: no shared symmetric secret);
  • :symmetric_auth_method - token_endpoint_auth_method is one of client_secret_basic, client_secret_post, or client_secret_jwt (draft §2);
  • :invalid_redirect_uris - redirect_uris is 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-string scope, or a grant_types that 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.