AttestoPhoenix.ClientIdMetadata (AttestoPhoenix v0.9.3)

Copy Markdown View Source

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):

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

client()

@type client() :: map()

A resolved CIMD client: the normalized, string-keyed metadata map Attesto.ClientIdMetadata.validate_document/2 returns.

Functions

cimd_client_id?(client_id, config)

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

client_id(map)

@spec client_id(client()) :: String.t()

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

jwks(arg1)

@spec jwks(client()) :: map() | String.t() | nil

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

redirect_uris(map)

@spec redirect_uris(client()) :: [String.t()]

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(client_id, config)

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

same_origin_redirect_uri?(client_id, redirect_uri)

@spec same_origin_redirect_uri?(String.t(), String.t()) :: boolean()

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.

scopes(arg1)

@spec scopes(client()) :: [String.t()]

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.