# `Attesto.ClientIdMetadata`
[🔗](https://github.com/XukuLLC/attesto/blob/v0.13.0/lib/attesto/client_id_metadata.ex#L1)

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.

# `document_error`

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

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

# `client_id_url?`

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

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

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

---

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