Skip to main content

Client ID Metadata Documents (CIMD)

Client ID Metadata Documents let an OAuth client identify itself with an HTTPS URL instead of a pre-registered client_id. The URL points at a JSON metadata document the client publishes about itself; LumoAuth fetches and validates the document at authorization time. The MCP (Model Context Protocol, the protocol AI applications use to call external tools) authorization specification (2025-11-25) adopts CIMD as the preferred client registration mechanism: MCP clients such as Claude no longer need dynamic client registration (registering themselves through the API) or manual setup before starting an OAuth flow.

LumoAuth implements draft-ietf-oauth-client-id-metadata-document-02 on the authorization-server side; the client.dev server guide is a good high-level companion.

On by default, on every plan

CIMD is enabled by default for every organization and included in every plan (Developer, Pro, Business), with no extra charge. Discovery advertises client_id_metadata_document_supported: true. Organization admins can switch it off under Security → Sign-in settings → Platform security → Client ID metadata documents (CIMD). Your plan's usage limits (monthly active users, machine-to-machine tokens, MCP servers) apply exactly as for registered clients; see Plans and limits.

The Platform security section of Sign-in settings with the Client ID metadata documents (CIMD) switch

For client developers: the 2-minute version​

  1. Publish a JSON file at a stable https:// URL with a path, for example https://client.example.com/oauth/metadata.json. Its client_id member must be that exact URL. The sample has a complete file and a runnable client.
  2. Serve it with Content-Type: application/json (or application/…+json), status 200, no redirects. Add Cache-Control: max-age=… to control how long LumoAuth caches it (60 s to 1 hour).
  3. Use that URL as client_id in the authorization request, with PKCE (S256). That is all: no registration call, no secret.
  4. Something wrong? The authorization error page and the token endpoint's invalid_client tell you the exact reason (client_id_mismatch, invalid_redirect_uri, …), and an organization admin can run the document through Applications → External clients (CIMD) → Check a metadata document or the check endpoint. Fixed documents are picked up on the very next request; failures are never cached.

Example metadata document​

https://client.example.com/oauth/metadata.json
{
"client_id": "https://client.example.com/oauth/metadata.json",
"client_name": "Example MCP Client",
"client_uri": "https://client.example.com",
"logo_uri": "https://client.example.com/logo.png",
"redirect_uris": ["https://client.example.com/callback", "http://127.0.0.1/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none",
"scope": "openid profile email mcp:read"
}

Authorization request​

GET https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/authorize
?response_type=code
&client_id=https%3A%2F%2Fclient.example.com%2Foauth%2Fmetadata.json
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&scope=openid%20profile%20mcp%3Aread
&code_challenge=<S256 challenge>&code_challenge_method=S256
&state=<random>

How it works​

  1. A client starts an authorization request with client_id=https://client.example.com/oauth/metadata.json (any https URL with a path).
  2. LumoAuth checks the organization's trust policy (switch, plan, domain allow-list, URL shape, SSRF rules) and fetches the URL: https only, public hosts, DNS pinned for the request, no redirects, 64 KB size cap, JSON content type required.
  3. The document's client_id member MUST byte-equal the URL itself, otherwise the client is rejected (client_id_mismatch).
  4. The client metadata (client_name, redirect_uris, logo_uri, token_endpoint_auth_method, grant_types, scope) is applied and the flow continues as with any registered client. Each organization keeps its own copy of the client, so the same metadata-document URL works in any number of organizations, and tokens issued in one organization are unknown (active: false) to another.
  5. A valid document is cached honoring its Cache-Control: max-age (default 300 s, clamped between 60 s and 1 hour). Error responses and invalid documents are never cached (draft §5.2); instead each URL gets at most 10 fetch attempts per minute (fetch_rate_limited beyond that).

CIMD-resolved clients are always badged on the consent screen as an external client together with the host they identify as (draft §8.5). When the redirect target is the loopback interface, the user additionally sees a warning that they are about to be redirected to an application on their own machine.

Validation rules​

RuleDetail
client_id matchThe document's client_id member must exactly equal the fetched URL.
URL shapeAbsolute https:// URL with a path (not just /), no query, no fragment, no userinfo, no ./.. path segments, publicly resolvable host, at most 191 characters.
ResponseStatus 200, Content-Type: application/json or application/*+json, at most 64 KB, no redirects.
redirect_urisRequired for authorization-code clients. Each URI must be https, or http strictly on the loopback interface (localhost, 127.0.0.1, [::1]) as the OAuth for native apps best practice allows. No fragments. Custom schemes (myapp://…) and javascript:, data: or other non-web schemes are rejected; a single bad URI rejects the whole document (invalid_redirect_uri).
token_endpoint_auth_methodnone (default, public client) or private_key_jwt with jwks or an https jwks_uri in the document. Shared-secret methods are impossible for a self-published client (draft §4.1).
grant_typesauthorization_code and refresh_token; client_credentials additionally allowed for private_key_jwt clients. The client receives refresh tokens only if refresh_token is listed.
client_nameControl characters, <, >, backticks and ${ are stripped and the name is cut to 100 characters; an empty result falls back to the URL's host.
logo_uri, client_uriKept only when they are well-formed https URLs; otherwise dropped.
PKCEAlways required (S256) for CIMD clients, regardless of auth method.
scopeOnly identity scopes are taken from the document: openid, profile, email, phone, address and mcp:read, plus the scopes your active MCP servers advertise (never LumoAuth management scopes). Anything else the document asks for (admin, admin:*, scim, offline_access, *:write, …) is dropped silently. See Allowing more scopes.
Standard

The OAuth for native apps best practice allows a native client to receive the authorization code on a loopback http listener, because that traffic never leaves the machine. What you must do: publish http://127.0.0.1/callback or http://localhost/callback in redirect_uris (any port matches at runtime) and use PKCE. Everything else must be https.

Check a document before you use it​

Two tools show a document exactly as the authorization server sees it, without registering anything and even while CIMD is switched off for the organization (the result then says enabled: false).

Portal. Applications → External clients (CIMD) lists every metadata-document client that has connected to the organization and has a Check a metadata document form: paste the URL, optionally tick Ignore the cached copy, and read the verdict, the effective client (redirect URIs, grant types, scopes after policy, loopback warnings) and the document as fetched.

Admin API. Needs clients.view and the admin:clients:read key scope.

Check a metadata document
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/clients/cimd/check \
-H "Authorization: ApiKey lmk_..." \
-H "Content-Type: application/json" \
-d '{ "client_id": "https://client.example.com/oauth/metadata.json", "refresh": true }'
Response (rejected)
{
"data": {
"ok": false,
"client_id": "https://client.example.com/oauth/metadata.json",
"enabled": true,
"reason": "client_id_mismatch",
"message": "The \"client_id\" member of the document must be byte-for-byte equal to the URL it was fetched from (watch trailing slashes, \"www.\" and the port).",
"warnings": [],
"document": null,
"effective": null,
"shadow_client": null
}
}
Response (accepted)
{
"data": {
"ok": true,
"client_id": "https://client.example.com/oauth/metadata.json",
"enabled": true,
"reason": null,
"message": null,
"warnings": [],
"document": { "client_id": "https://client.example.com/oauth/metadata.json", "client_name": "Example MCP Client", "...": "..." },
"effective": {
"client_name": "Example MCP Client",
"application_type": "native",
"token_endpoint_auth_method": "none",
"public_client": true,
"pkce_required": true,
"redirect_uris": ["https://client.example.com/callback", "http://127.0.0.1/callback"],
"dropped_redirect_uris": [],
"grant_types": ["authorization_code", "refresh_token"],
"scopes": ["openid", "profile", "email", "mcp:read"],
"dropped_scopes": [],
"logo_uri": "https://client.example.com/logo.png",
"client_uri": "https://client.example.com",
"has_loopback_redirect": true
},
"shadow_client": null
}
}

refresh: true discards the cached copy first, so you can verify a change immediately even inside the cache window.

Why was my document rejected?​

Every rejection carries a stable reason. The authorization error page shows it under Technical details, the token endpoint puts it in error_description, the check tools return it as reason, and the oauth.cimd_fetch_failed audit event records it.

reasonWhat to fix
invalid_client_id_urlThe client_id is not an absolute https:// URL with a path; or it has a query, fragment, userinfo or ./.. segment.
cimd_disabled_for_tenantThe organization switched CIMD off. Ask its admin to enable it, or register the client.
cimd_not_in_planNot expected on hosted plans (every plan includes CIMD); only possible with a custom contract or self-hosted license that removes it.
client_id_too_longMore than 191 characters. Publish at a shorter URL.
unsafe_urlThe host is loopback, private, link-local or does not resolve. Use a public https host (for local development, see the sample).
domain_not_allowedThe organization restricts CIMD to an allow-list of domains.
fetch_rate_limitedMore than 10 fetch attempts for this URL in the last minute. Fix the document, wait a moment, retry.
fetch_failedConnection, TLS or timeout error (10 s per request). Redirects are not followed, so a URL shortener or www. redirect fails here.
http_status_NNNThe URL answered something other than 200.
invalid_content_typeServe application/json (or application/*+json).
document_too_largeLarger than 64 KB.
invalid_jsonThe body is not a JSON object.
client_id_mismatchThe client_id member is not byte-for-byte the fetched URL (trailing slash, www., port, case).
unsupported_token_endpoint_auth_methodOnly none or private_key_jwt.
private_key_jwt_requires_jwksPublish jwks or an https jwks_uri.
invalid_grant_typesgrant_types must be an array of strings.
missing_redirect_urisAuthorization-code clients need a non-empty redirect_uris.
invalid_redirect_uriEvery redirect URI must be https, or http on loopback only; no fragments or custom schemes. A client that needs myapp:// must use Dynamic Client Registration instead.
client_id_collisionA registered application in the organization already owns this client_id.

Other common surprises:

  • A scope in the document never appears on the consent screen. It is not an identity scope and not in cimd_allowed_scopes, so it was dropped; the check tools list it under dropped_scopes.
  • Loopback redirect refused although the document is valid. The organization's loopback policy is block; loopback URIs are dropped (dropped_redirect_uris) and the authorize request fails as unregistered.
  • No refresh_token in the token response. The document's grant_types does not list refresh_token.
  • A change is not picked up. A valid document is cached per its Cache-Control: max-age (60 s to 1 hour). Lower the max-age while iterating, or run a check with refresh: true.

Trust policy (organization admins)​

Because CIMD clients are self-declared, each organization controls a trust policy. All of it lives under Security → Sign-in settings → Platform security and in the settings/auth Admin API.

SettingKeyDefaultDescription
Client ID metadata documents (CIMD)cimd_enabledtrueMaster switch; also controls the discovery advertisement. Off means only applications registered in the organization can sign users in.
Allowed domainscimd_allowed_domains(empty: any public https host)Restrict client_id URLs to a domain allow-list (subdomains included). The draft's §8.9 pattern for "treat *.example.com as a known operator".
Localhost redirect URIscimd_localhost_redirect_policywarnwarn shows an extra consent warning when the redirect target is loopback; block rejects loopback redirect URIs entirely. The MCP spec calls out loopback redirects as an impersonation risk, because local malware can pose as a legitimate client.
Extra scopescimd_allowed_scopes[]Scopes beyond the identity set that CIMD clients may request (Admin API only).
Switch CIMD off for an organization
curl -X PATCH https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/settings/auth \
-H "Authorization: ApiKey lmk_..." \
-H "Content-Type: application/json" \
-d '{ "cimd_enabled": false }'

Allowing more scopes​

A metadata document is hosted by whoever controls the client_id URL, so it cannot widen its own client into privileged scopes. To let CIMD clients request a scope beyond the identity set, add it to the organization allow-list cimd_allowed_scopes:

Allow CIMD clients to request mcp:write
curl -X PATCH https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/settings/auth \
-H "Authorization: ApiKey lmk_..." \
-H "Content-Type: application/json" \
-d '{ "cimd_allowed_scopes": ["mcp:write"] }'

The call needs admin:settings:write and the settings.manage permission. Allow-list only what an unknown, self-declared client may safely hold; the user still sees and approves every scope on the consent screen.

Plans and limits​

CIMD itself is free on every plan (catalog feature cimd), and there is no cap on how many distinct metadata-document clients may connect. The organization's plan limits apply to what those clients do, exactly as they do for registered clients:

LimitHow CIMD traffic counts
Monthly active users (mau)A user who signs in through a CIMD client is an active user that month. On the Developer plan the MAU hard cap applies.
Machine-to-machine tokens (m2m_tokens)client_credentials tokens issued to private_key_jwt CIMD clients are M2M tokens.
MCP servers (mcp_servers)Unchanged: the limit is on servers you register, not on clients that connect to them.
Active agents (active_agents)Unchanged: a CIMD client is an OAuth client, not an agent, unless it also uses an agent identity.

See the pricing matrix for the numbers per plan.

Token requests​

  • Public clients (token_endpoint_auth_method: none) authenticate token requests with client_id + PKCE code_verifier only.
  • private_key_jwt clients send a JWT client assertion (iss/sub = the metadata-document URL) signed with a key from the document's jwks/jwks_uri; LumoAuth requires this authentication for every token request of such a client (draft §8.2).
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/token \
-d grant_type=authorization_code \
-d code=<AUTH_CODE> \
-d redirect_uri=https://client.example.com/callback \
-d client_id=https://client.example.com/oauth/metadata.json \
-d code_verifier=<PKCE_VERIFIER>

A rejected document answers 401:

{
"error": "invalid_client",
"error_description": "Client ID Metadata Document rejected (invalid_redirect_uri): Every redirect URI must be an https:// URL, or http:// strictly on the loopback interface (localhost, 127.0.0.1, [::1]); no fragments, no custom schemes."
}

Auditing​

EventWhen
oauth.cimd_client_resolvedA metadata document was fetched and validated (once per cache window).
oauth.cimd_fetch_failedResolution failed; includes the reason above for probing/SSRF visibility.
oauth.cimd_document_checkedAn admin ran a document through the portal check tool.

Limitations​

  • client_id URLs are limited to 191 characters.
  • Redirects from the metadata URL are not followed; URL shorteners cannot be Client Identifier URLs.
  • Changes to a client's keys (jwks/jwks_uri) do not revoke tokens already issued (draft §8.4.1 leaves this to the server); revoke them from the client's page if you need to.
  • The metadata document's logo_uri is linked, not prefetched and cached (draft §8.8).

Standards used on this page​

Plain nameStandard
Client ID metadata documentsOAuth Client ID Metadata Document, draft 02
Model Context Protocol (MCP) authorizationMCP Authorization specification
Dynamic client registrationRFC 7591
OAuth for native appsRFC 8252
Proof Key for Code Exchange (PKCE)RFC 7636
JWT client authentication (private_key_jwt)RFC 7523

See the standards reference for what each one covers and how LumoAuth uses it.