Integration façade for 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 a JSON client metadata document and uses it as the client. The pure URL/
document validation lives in Attesto.ClientIdMetadata, the SSRF-guarded
fetch in AttestoPhoenix.ClientIdMetadata.Fetcher, the cache in
AttestoPhoenix.ClientIdMetadata.Cache, and the orchestration in
AttestoPhoenix.ClientIdMetadata.Resolver. This module is the thin seam the
HTTP endpoints (the authorization endpoint and the token / PAR client
authentication path) call to decide whether a presented client_id is a CIMD
URL and, when it is, to resolve it into a client.
When CIMD applies
A presented client_id is resolved through CIMD only when both hold
(cimd_client_id?/2):
- the feature is enabled for the deployment
(
AttestoPhoenix.Config.client_id_metadata_enabled?/1), and - the
client_idis a well-formed CIMD URL (Attesto.ClientIdMetadata.client_id_url?/1).
An opaque (non-URL) client_id, or any client_id when the feature is off,
is left to the host's :load_client registry exactly as before - CIMD never
changes the resolution of a registered client.
The resolved client
resolve/2 returns the normalized, string-keyed metadata map
Attesto.ClientIdMetadata.validate_document/2 produced (carrying at least
client_id and redirect_uris). It is not the host's opaque client value,
so the host's :client_id / :client_redirect_uris / :client_jwks
callbacks do not apply to it; the accessors here
(client_id/1, redirect_uris/1, jwks/1) read the document directly, and
the calling endpoint uses them in place of the host callbacks for a CIMD
client. A resolved CIMD client authenticates only as a public client
(none + PKCE) or with private_key_jwt; the no-symmetric-secret rule the
document validation enforces guarantees client_secret_* can never apply.
redirect_uri policy (RFC 9700 + draft §2)
RFC 9700 requires the request redirect_uri to exact-match one of the
document's redirect_uris; the calling endpoint performs that match through
the same Attesto.AuthorizationRequest path it uses for a registered client,
feeding it redirect_uris/1 as the registered set. The draft additionally
permits requiring the redirect_uri to be same-origin (scheme + host + port)
with the client_id URL; same_origin_redirect_uri?/2 is that check, applied
by the authorization endpoint when :require_same_origin_redirect_uri is set
(the default).
Summary
Types
A resolved CIMD client: the normalized, string-keyed metadata map
Attesto.ClientIdMetadata.validate_document/2 returns.
Functions
Returns true iff client_id must be resolved through CIMD for config:
the feature is enabled and client_id is a well-formed CIMD URL.
The CIMD client's client_id - the URL the document was fetched from and is
bound to (Attesto.ClientIdMetadata.validate_document/2 guarantees the
document's client_id equals it).
The CIMD client's verification keys for private_key_jwt client
authentication (RFC 7523 / OIDC Core §9), taken from the document's inline
jwks (preferred) or its jwks_uri. Returns nil when the document carried
neither, which makes private_key_jwt impossible for the client (it then
authenticates only as a public client).
The CIMD client's registered redirect URIs (RFC 9700), used by the
authorization endpoint as the exact-match set in place of the host's
:client_redirect_uris callback. Document validation guarantees a non-empty
list of strings.
Resolve a CIMD client_id URL into its normalized client metadata map.
Returns true iff redirect_uri is same-origin (scheme, host, and port) with
the CIMD client_id URL (draft §2's optional same-origin tightening).
The scopes the CIMD document declares, as a list.
Types
@type client() :: map()
A resolved CIMD client: the normalized, string-keyed metadata map
Attesto.ClientIdMetadata.validate_document/2 returns.
Functions
@spec cimd_client_id?(term(), AttestoPhoenix.Config.t()) :: boolean()
Returns true iff client_id must be resolved through CIMD for config:
the feature is enabled and client_id is a well-formed CIMD URL.
This is the single gate the endpoints consult before reaching for the
resolver, so an opaque client_id (or any client_id while the feature is
disabled) is never sent to the network and always flows through the host's
:load_client registry.
The CIMD client's client_id - the URL the document was fetched from and is
bound to (Attesto.ClientIdMetadata.validate_document/2 guarantees the
document's client_id equals it).
The CIMD client's verification keys for private_key_jwt client
authentication (RFC 7523 / OIDC Core §9), taken from the document's inline
jwks (preferred) or its jwks_uri. Returns nil when the document carried
neither, which makes private_key_jwt impossible for the client (it then
authenticates only as a public client).
The CIMD client's registered redirect URIs (RFC 9700), used by the
authorization endpoint as the exact-match set in place of the host's
:client_redirect_uris callback. Document validation guarantees a non-empty
list of strings.
@spec resolve(String.t(), AttestoPhoenix.Config.t()) :: {:ok, client()} | {:error, AttestoPhoenix.ClientIdMetadata.Resolver.error()}
Resolve a CIMD client_id URL into its normalized client metadata map.
Delegates to AttestoPhoenix.ClientIdMetadata.Resolver.resolve/2. The caller
is expected to have gated on cimd_client_id?/2 first; the resolver
re-validates the URL grammar regardless, so a non-CIMD client_id reaching
here still fails closed. Returns {:ok, client} or {:error, reason} (a
fetch, decode, validation, or host-policy failure - never cached).
Returns true iff redirect_uri is same-origin (scheme, host, and port) with
the CIMD client_id URL (draft §2's optional same-origin tightening).
The port comparison uses each URI's effective port, so an explicit default
port and an omitted one compare equal. A redirect_uri that does not parse as
an absolute URL with a host is not same-origin.
The scopes the CIMD document declares, as a list.
draft-ietf-oauth-client-id-metadata-document-01 §7 carries the RFC 7591 §2
client-metadata field set, in which scope is an OPTIONAL, space-delimited
string. A document that omits it declares no scopes, so this returns [] — an
empty declared set, never a missing key. A CIMD client therefore has no
registered scope cap of its own; what an empty declared set grants is host
policy (typically the scopes the resource owner consents to).
This is the value AttestoPhoenix.AuthorizationServer.Token exposes to a host
:authorize_scope policy as client.scopes, so a callback written for a
registered client's scope list reads a CIMD client as an empty set instead of
raising KeyError.