# `AttestoPhoenix.ClientIdMetadata`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/client_id_metadata.ex#L1)

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_id` is 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).

# `client`

```elixir
@type client() :: map()
```

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

# `cimd_client_id?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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).

# `same_origin_redirect_uri?`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
