> ## Documentation Index
> Fetch the complete documentation index at: https://docs.strata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Client ID Metadata Documents

> [!WARNING]
>
> Client ID Metadata Documents are an experimental feature -- see [Experimental Features](/reference/orchestrator/experimental/overview) for important caveats.

Traditional OAuth requires every client to be registered ahead of time -- an operator creates a `clientID`, issues credentials, and records redirect URIs before the client can authenticate. This works for a fixed set of applications, but breaks down for the growing population of AI agents and MCP clients that appear dynamically and cannot be pre-provisioned.

Client ID Metadata Documents (CIMD) solve this by letting a client identify itself with an `https` URL instead of a pre-registered ID. The client's `client_id` **is** a URL that serves a JSON metadata document describing the client. When the Orchestrator receives an authorization request from an unknown URL `client_id`, it fetches that document, validates it, caches it, and builds a client from it on the fly -- no manual registration required. This implements [draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/).

> [!TIP]
>
> CIMD is the mechanism modern AI agents use to authenticate. For example, [Claude Code](/guides/ai-identity/connect/claude) presents its published metadata URL as its `client_id` and requests no scope -- the Orchestrator resolves it to a public client and issues tokens without any per-client setup. See the [AI Identity Gateway](/reference/modes/ai-identity-gateway) and [MCP Proxy](/guides/ai-identity/mcp-proxy) guides for related patterns.

> [!NOTE]
>
> This page covers the Orchestrator as the **authorization server**, accepting CIMD clients that present a URL `client_id`. The Orchestrator can also act as a **CIMD client** in the other direction -- the [Generic OAuth](/reference/orchestrator/identity-fabric/oauth#client-id-metadata-document) connector publishes its own metadata document and presents that URL as its `client_id` to an upstream authorization server.

## How it works

An operator enables CIMD by adding one or more `oidcCimd` apps to an Orchestrator that already runs an OIDC Provider (the top-level `oidcProvider` configuration). Each app is a **domain-scoped policy**: it lists the domains whose metadata URLs it will resolve, and the authentication, authorization, and token settings to apply to clients from those domains.

```mermaid theme={null}
sequenceDiagram
    participant Client as CIMD Client
    participant Orch as Orchestrator (OIDC Provider)
    participant Doc as client_id URL

    Client->>Orch: authorize (client_id=https://app.example.com/oauth-client)
    Orch->>Orch: match host against oidcCimd allowedDomains
    Orch->>Doc: GET metadata document (SSRF-safe, no redirects)
    Doc-->>Orch: JSON metadata (redirect_uris, grant_types, ...)
    Orch->>Orch: validate + cache metadata, build client
    Orch->>Client: run authorization code + PKCE flow
    Client->>Orch: token request
    Orch-->>Client: access token (+ refresh token)
```

Once resolved, a CIMD client behaves like any other public or confidential OIDC client: it completes the authorization code flow (with PKCE) and receives tokens signed by the OIDC Provider. Resolved metadata is cached so subsequent requests from the same client skip the fetch.

## The metadata document

The document served at the `client_id` URL is a JSON object using the client metadata fields defined by [OAuth Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591#section-2) and profiled by [draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/). Refer to those specifications for the full field registry and semantics.

The Orchestrator reads a subset of those fields -- `client_id`, `redirect_uris`, `grant_types`, `response_types`, `token_endpoint_auth_method`, `jwks_uri`, `scope`, and `client_name` -- and applies the constraints described in [Client requirements](#client-requirements) below.

A minimal metadata document for a public client looks like:

```json theme={null}
{
  "client_id": "https://app.example.com/oauth-client",
  "client_name": "Example Agent",
  "redirect_uris": ["https://app.example.com/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

## Client requirements

The Orchestrator enforces the following rules. A metadata document that violates any of them makes the client unresolvable, and the authorization request is rejected as an invalid client.

**`client_id` URL** -- must use the `https` scheme, include a path, and must not contain dot-segments (`.` / `..`), a fragment, userinfo, or a query string. The host must match one of the configured `allowedDomains`.

**`redirect_uris`** -- at least one is required. Each must use `https`, except that `http` is permitted for loopback hosts (`localhost`, `127.0.0.1`, `::1`) per [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252). Loopback redirects may use any port.

**`grant_types`** -- only `authorization_code` and `refresh_token` are supported, and the field defaults to `authorization_code` when omitted. Declaring `refresh_token` requires `authorization_code` as well. Implicit, hybrid, client credentials, and other grants are rejected.

**`response_types`** -- only `code` (authorization code flow) is supported, and the field defaults to `code` when omitted.

**`token_endpoint_auth_method`** -- defaults to `none` when omitted:

* `none` -- the client is **public** and MUST use PKCE.
* `private_key_jwt` -- the client is **confidential**; its `jwks_uri` must be an `https` URL sharing the same origin (scheme + host + port) as the `client_id`. The Orchestrator fetches the JWKS to verify the client's signed authentication assertions.

**`scope`** -- optional. When present, requestable scopes are restricted to this set; when omitted, scopes are unrestricted (like a static client).

> [!NOTE]
>
> Declaring `refresh_token` in a client's `grant_types` does **not** automatically issue a refresh token. The operator must explicitly enable refresh token issuance via `refreshToken.allowOfflineAccess` in the CIMD app configuration. See [Refresh tokens](#refresh-tokens) below.

## Configuration

CIMD is configured as one or more apps of type `oidcCimd` alongside the top-level `oidcProvider` configuration. Each `oidcCimd` app registers a single domain-scoped policy.

| Field | Type | Required | Description |
| - | - | - | - |
| `allowedDomains` | string\[] | yes | Domains whose metadata URLs this policy resolves. Each entry is an exact hostname (`app.example.com`) or a wildcard prefix (`*.example.com`). A wildcard is only valid as the leftmost label. |
| `authentication` | object | yes | How users are authenticated -- an `idps` list, or `isAuthenticatedSE` + `authenticateSE` service extensions. Same shape as an `oidc` app. |
| `authorization` | object | no | Authorization policy applied to resolved clients. Defaults to allow-all. |
| `allowedAudiences` | string\[] | no | Audiences CIMD clients may request in access tokens. |
| `claimsMapping` | map | no | Maps user attributes to token claims, namespaced by connector (e.g. `email: azure.email`). |
| `accessToken` | object | no | Access token `type` (`jwt` / `opaque`), `length`, and `lifetimeSeconds`. |
| `refreshToken` | object | no | Refresh token settings. See [Refresh tokens](#refresh-tokens) for field details. |
| `attrProviders` | object\[] | no | Connectors used to load attributes for resolved clients. |
| `loadAttrsSE` | service extension | no | Customizes attribute loading. |
| `buildIDTokenClaimsSE` | service extension | no | Adds custom claims to the ID token. |
| `buildAccessTokenClaimsSE` | service extension | no | Adds custom claims to the access token. |

### Refresh tokens

Refresh tokens are long-lived credentials, so OIDC CIMD apps do not issue them by default. An operator must explicitly opt each CIMD app in, ensuring that persistent offline access is a deliberate policy decision rather than a consequence of client metadata.

The `refreshToken` object supports the following fields:

| Field | Type | Required | Description |
| - | - | - | - |
| `allowOfflineAccess` | bool | no | When `true`, permits clients to receive a refresh token. The client must request the `offline_access` scope in the authorization request. Defaults to `false`. |
| `autoGrant` | bool | no | When `true`, the Orchestrator automatically injects the `offline_access` scope for clients that do not request it. Requires `allowOfflineAccess: true`. Use for clients like Claude Code that omit scopes entirely but still need refresh tokens. Defaults to `false`. |
| `length` | int | no | Length of the refresh token in characters (22–256). |
| `lifetimeSeconds` | int | no | How long a refresh token is valid. |

### Example

An OIDC Provider with a single CIMD policy that resolves any client under `*.example.com` and authenticates users against an Azure connector:

```yaml maverics.yaml theme={null}
# The OIDC Provider is configured under the top-level `oidcProvider` key.
oidcProvider:
  discovery:
    issuer: https://sso.company.com
    endpoints:
      wellKnown: /.well-known/openid-configuration
      auth: /oauth2/authorize
      token: /oauth2/token
      jwks: /oauth2/jwks
      userinfo: /oauth2/userinfo
  jwks:
    - algorithm: RSA256
      privateKey: <oidc-signing-key>

# CIMD policies are registered as apps of type `oidcCimd`.
apps:
  - name: partner-agents
    type: oidcCimd
    # Resolve CIMD client_id URLs under these domains.
    allowedDomains:
      - "*.example.com"
    authentication:
      idps:
        - azure
    allowedAudiences:
      - "https://api.company.com"
    accessToken:
      type: jwt
      lifetimeSeconds: 3600
    refreshToken:
      allowOfflineAccess: true
      lifetimeSeconds: 2592000
```

### Multiple policies

You can register several `oidcCimd` apps to apply different templates to different domains. When more than one policy could match a host, the **most specific** domain pattern wins: an exact hostname outranks any wildcard, and a wildcard with more labels outranks one with fewer. Exact-duplicate domain strings across policies are rejected at startup.

```yaml maverics.yaml theme={null}
apps:
  # Broad policy for all partner subdomains.
  - name: partner-agents
    type: oidcCimd
    allowedDomains:
      - "*.example.com"
    authentication:
      idps:
        - azure

  # More specific policy that wins for this exact host, with stricter settings.
  - name: privileged-agent
    type: oidcCimd
    allowedDomains:
      - "agent.example.com"
    authentication:
      idps:
        - azure
    # Stricter authorization: only members of the privileged group may use this client.
    authorization:
      rules:
        - and:
            - contains: ["{{ azure.groups }}", "privileged-agents"]
    accessToken:
      lifetimeSeconds: 300
```

## Security and caching

Because the Orchestrator fetches a client-supplied URL, CIMD metadata retrieval is hardened against server-side request forgery (SSRF):

* **Special-use IP blocking** -- resolved IPs are checked against [RFC 6890](https://datatracker.ietf.org/doc/html/rfc6890) special-use ranges (loopback, private, link-local, etc.) and rejected. The connection is made to the validated IP to prevent DNS rebinding.
* **No redirects** -- the metadata fetch does not follow HTTP redirects.
* **Bounded requests** -- a 5-second timeout and a \~5 KB response size cap, and the response must be `application/json`.
* **Domain allowlist** -- only hosts matching a policy's `allowedDomains` are fetched at all.

Resolved metadata is cached to avoid re-fetching on every request:

* The cache TTL is derived from the response's `Cache-Control: max-age`, clamped to a minimum of **5 minutes** and a maximum of **24 hours**.
* Up to **1000** metadata documents are cached per policy.
* When a cached document is refreshed and a security-relevant field changes (`token_endpoint_auth_method`, `jwks_uri`, or `redirect_uris`), the change is logged.

## Discovery

When at least one `oidcCimd` policy is registered, the OIDC Provider advertises CIMD support in its discovery (well-known) document:

* `client_id_metadata_document_supported` is set to `true`.
* `token_endpoint_auth_methods_supported` gains `none` and `private_key_jwt` (the methods CIMD honors) alongside the static `client_secret_post`.

With no CIMD policies configured, the discovery document is unchanged and `client_id_metadata_document_supported` is `false`.

## Examples

The following `oidcCimd` app configurations allow common AI clients to authenticate using CIMD without pre-registering a client ID. In each case, the client presents an `https` URL as its `client_id`; the Orchestrator matches the host against `allowedDomains`, fetches the metadata document, and completes the authorization code flow automatically.

**Claude Code / Claude Desktop**

Both Claude Code and Claude Desktop present a metadata URL on `claude.ai` as the `client_id`. A single `oidcCimd` policy covers both.

```yaml maverics.yaml theme={null}
apps:
  - name: claude
    type: oidcCimd
    allowedDomains:
      - claude.ai
    authentication:
      idps:
        - upstream-idp
    accessToken:
      type: jwt
      lifetimeSeconds: 3600
    refreshToken:
      allowOfflineAccess: true
      # autoGrant injects offline_access automatically because Claude Code
      # does not request scopes explicitly.
      autoGrant: true
      lifetimeSeconds: 86400
    claimsMapping:
      email: upstream-idp.email
      name: upstream-idp.name
      sub: upstream-idp.sub
```

> [!NOTE]
>
> When Claude Desktop connects via [`mcp-remote`](https://github.com/geelen/mcp-remote) as a local stdio proxy, CIMD does not apply — the client ID is a plain string, not a URL, and must be registered as a standard OIDC app. See [Connect Claude to the Gateway](/guides/ai-identity/connect/claude) for mcp-remote configuration.

**ChatGPT**

ChatGPT presents a metadata URL on `chatgpt.com` as its `client_id`.

```yaml maverics.yaml theme={null}
apps:
  - name: chatgpt
    type: oidcCimd
    allowedDomains:
      - chatgpt.com
    authentication:
      idps:
        - upstream-idp
    accessToken:
      type: jwt
      lifetimeSeconds: 3600
    refreshToken:
      allowOfflineAccess: true
      lifetimeSeconds: 86400
    claimsMapping:
      email: upstream-idp.email
      name: upstream-idp.name
      sub: upstream-idp.sub
```

> [!NOTE]
>
> Verify the actual `client_id` URL ChatGPT presents during the authorization
> flow — the host determines which `allowedDomains` entry applies. If the host
> differs from `chatgpt.com` (for example, `openai.com`), update `allowedDomains`
> accordingly. You can find the `client_id` in the Orchestrator's authorization
> logs on the first connection attempt.

***

## Related Pages

> **[Experimental Features](/reference/orchestrator/experimental)**
>
> Overview of all experimental features and important caveats

> **[OIDC Provider](/reference/modes/oidc-provider)**
>
> Configure the Orchestrator as an OIDC authorization server

> **[AI Identity Gateway](/reference/modes/ai-identity-gateway)**
>
> Secure AI agent-to-tool communication via MCP

> **[Connect Claude](/guides/ai-identity/connect/claude)**
>
> Connect Claude Code and other Anthropic clients

> **[Generic OAuth](/reference/orchestrator/identity-fabric/oauth#client-id-metadata-document)**
>
> Present a CIMD client ID to an upstream authorization server
