From 0c8e6123b83770b23dd438de284bd352a4c4387e Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 11 Aug 2026 13:58:21 +0530 Subject: [PATCH 1/2] docs(mcp): document CIMD and the consent screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the CIMD section: the document shape, that client_id must equal its own URL, the SSRF-hardened fetch, and the consent screen that comes with it — plus why consent applies to self-registered clients and not to ones an operator registered. Corrects the Claude Code row: the OAuth path works with --enable-client-id-metadata-document, where before it was refused outright. The row says plainly that the browser leg has not been verified end to end here, because the previous version of this table overstated exactly that. --- docs/core/mcp.md | 33 ++++++++- specs/2026-08-11-cimd-and-consent.md | 105 +++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 specs/2026-08-11-cimd-and-consent.md diff --git a/docs/core/mcp.md b/docs/core/mcp.md index 19be976..8f49779 100644 --- a/docs/core/mcp.md +++ b/docs/core/mcp.md @@ -97,7 +97,7 @@ happens rather than what the specs allow. | Client | Works | How | | --- | --- | --- | | **Claude Code, VS Code** — static token | **yes, verified** | Mint a token bound to `/mcp` and pass it as a fixed header (below) | -| **Claude Code** — OAuth | **no** | Claude Code refuses: *"Incompatible auth server: does not support dynamic client registration"* | +| **Claude Code** — OAuth | with `--enable-client-id-metadata-document` | Without it Claude Code refuses the server outright. With it the client engages and runs the OAuth flow; the browser leg (login + consent) has not been verified end to end here | | **Claude.ai custom connector** — pasted client ID | unverified | Anthropic documents an OAuth Client ID field under *Advanced settings*; not confirmed here | | Any client that needs to self-register | no | Needs RFC 7591 DCR or a Client ID Metadata Document; Authorizer has neither yet | @@ -131,6 +131,37 @@ nothing useful and permission checks resolve to `service_account:`. F per-user identity you need the OAuth flow, which is why DCR/CIMD support matters and is tracked for a future release. +### Connecting a client that self-registers (CIMD) + +Set `--enable-client-id-metadata-document` alongside `--mcp-enabled` and a client +can identify itself with an HTTPS URL pointing at a JSON document, instead of a +`client_id` you registered in advance: + +```json +{ + "client_id": "https://app.example.com/oauth/client.json", + "client_name": "Example MCP Client", + "redirect_uris": ["http://127.0.0.1:0/callback"], + "token_endpoint_auth_method": "none" +} +``` + +The `client_id` must equal the URL the document is served from — that equality is +what stops any host claiming to be any client. Authorizer fetches it through an +SSRF-hardened client (one-shot DNS, dial pinned to the validated IP, private and +loopback addresses refused), validates the presented `redirect_uri` against the +document's list, and caches it with a clamped TTL. + +Because such a client asserts its own identity, **a consent screen is shown +before any code is issued**. It leads with the redirect host — the only fact +about the client the server has verified — and warns when a client's redirect +URIs are all loopback, since any local process can bind the same port and present +the same document. Clients you registered yourself are unaffected. + +Restrict which hosts may serve a document with +`--client-id-metadata-allowed-domains` if you want a closed deployment; leaving it +empty accepts any HTTPS host, which is what a public MCP server wants. + ### Why there is no `/register` endpoint Authorizer deliberately does **not** implement RFC 7591 dynamic client diff --git a/specs/2026-08-11-cimd-and-consent.md b/specs/2026-08-11-cimd-and-consent.md new file mode 100644 index 0000000..5ed48be --- /dev/null +++ b/specs/2026-08-11-cimd-and-consent.md @@ -0,0 +1,105 @@ +# Client ID Metadata Documents + consent screen + +Follow-up to `2026-08-10-mcp-http-transport.md` (shipped as authorizerdev/authorizer#757). + +**Why now**: verified against Claude Code 2.1.226, the OAuth path to `/mcp` does +not work — the client refuses with *"Incompatible auth server: does not support +dynamic client registration"*. Everything #757 built (PKCE, discovery, audience +binding, loopback redirects) is unreachable by the flagship client without a +client-registration mechanism. CIMD is that mechanism. + +--- + +## 1. Why CIMD and not DCR + +The MCP authorization spec (2025-11-25) demoted DCR: authorization servers +**SHOULD** support Client ID Metadata Documents and **MAY** support DCR, which it +keeps only *"for backwards compatibility with earlier versions"*. Client priority +order is pre-registered → CIMD → DCR → prompt. + +Auth0 ships DCR Enterprise-only, disabled by default, behind ACLs or a reverse +proxy, and recommends CIMD instead. Anthropic steers directory traffic away from +DCR because it registers a fresh client on every connection — unbounded row +growth in every self-hosted deployment. + +CIMD avoids all of it: the `client_id` **is** an HTTPS URL the authorization +server fetches. No write endpoint, no rows, no schema change. + +## 2. What has to be built + +### 2.1 The resolver + +When `client_id` is an HTTPS URL with a path component: + +1. Fetch it through `validators.SafeHTTPClient` — already exists, resolves the + host once and pins the dial to the validated IP, so DNS-rebinding TOCTOU is + closed. This is the SSRF mitigation the spec's §6 security considerations + demand, and it is why this feature is small rather than dangerous. +2. Require `client_id` in the document to equal the URL **exactly** (spec MUST). +3. Require `client_name` and `redirect_uris` (spec MUST). +4. Validate the presented `redirect_uri` against the document's list (spec MUST), + reusing `redirectURIMatches` so RFC 8252 loopback rules apply identically. +5. Cache respecting HTTP cache headers (spec SHOULD), bounded, with a floor and + ceiling so a hostile `max-age` cannot pin or thrash the cache. + +### 2.2 The consent screen + +Mandatory, not optional. CIMD makes client identity **self-asserted**: anyone can +host a metadata document. The spec requires the authorization server to display +the redirect URI hostname, and to warn when the redirect URIs are localhost-only +(because any local process can bind a port and claim to be the legitimate client). + +Scoped to **CIMD clients only**. Pre-registered and first-party clients keep +today's silent approval — the operator already vouched for them, and changing +that would be an unrelated behaviour break. + +No persistence. `/authorize` is hit once per connector setup; refreshes go to +`/oauth/token`. A "remember this grant" table would mean a schema change across +13 providers for no benefit here. + +### 2.3 Discovery + +Advertise `client_id_metadata_document_supported: true` — but only when the +feature is enabled, because advertising a capability that is not implemented +makes a client select it and then fail. + +## 3. Configuration + +| Flag | Default | Why | +|---|---|---| +| `--enable-client-id-metadata-document` | `false` | Changes the authorization endpoint's trust model for *every* client, not just MCP. Explicit opt-in. | +| `--client-id-metadata-allowed-domains` | empty (any HTTPS) | The spec's optional domain trust policy. Empty = open server, which is what public MCP servers want; an allowlist is for locked-down deployments. | + +## 4. Threat model + +| Threat | Mitigation | +|---|---| +| SSRF to internal endpoints via a hostile `client_id` URL | `validators.SafeHTTPClient` — one-shot DNS, IP pinned, private/loopback rejected | +| Client impersonation (attacker hosts a doc claiming another's name) | Consent screen shows the redirect URI **hostname**, which is what actually receives the code | +| Localhost port race (any local process claims to be the client) | Explicit warning on the consent screen for loopback-only clients; the spec says this cannot be fully solved server-side | +| Cache poisoning / pinning via hostile cache headers | Clamp TTL to a floor and ceiling; bound total entries | +| Resource exhaustion via many distinct client_id URLs | Response size cap, request timeout, bounded cache, existing rate limiting on `/authorize` | +| Token minted for an unintended audience | Unchanged: RFC 8707 `resource` still binds `aud`, still enforced at `/mcp` | + +## 5. Test plan + +- Resolver: happy path; `client_id` mismatch rejected; non-HTTPS rejected; no + path component rejected; missing required fields rejected; oversized response + rejected; private/loopback URL rejected (SSRF); cache hit does not refetch; + hostile `max-age` clamped. +- `/authorize`: a CIMD client reaches consent, not a silent redirect; a + pre-registered client is unaffected; a `redirect_uri` absent from the document + is rejected; loopback port-agnostic matching still applies. +- Consent: approving issues a code; denying returns `access_denied` to the + registered redirect; the redirect hostname is displayed; a loopback-only client + shows the warning; CSRF-protected. +- Discovery: `client_id_metadata_document_supported` present only when enabled. +- **End-to-end against a real Claude Code client** — the step whose absence + produced a wrong claim last time. Not "believed to work": observed connecting. + +## 6. Out of scope + +- RFC 7591 DCR — see §1. +- `private_key_jwt` for CIMD clients (spec MAY). +- Persisted consent grants. +- Delegated (RFC 8693) tokens at `/mcp` — still deferred, see the transport spec. From b583138315b36c62b92ec069bc680c74a49a9472 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 13 Aug 2026 01:26:51 +0530 Subject: [PATCH 2/2] docs(mcp): document opt-in RFC 7591 dynamic client registration The docs said Authorizer "deliberately does not implement RFC 7591" and that self-registering clients could not connect. Both shipped in 2.4.0: CIMD as the preferred mechanism, DCR behind --enable-dynamic-client-registration for clients that predate it. The vendor arguments collected against DCR still stand as reasons to keep it OFF by default, and they are kept rather than deleted - with the mitigations answering each one. What changed is the premise: the reasoning was about what should be preferred, not about what shipping clients can do. Claude Code reads client_id_metadata_document_supported and still refuses without a registration_endpoint. Updates the compatibility matrix (self-registering clients now "yes", Claude Code OAuth now verified through registration), replaces "Why there is no /register endpoint" with "Self-registering clients: CIMD vs DCR", and corrects the OIDC, SSO and spec pages that listed DCR as unimplemented. --- docs/core/mcp.md | 71 +++++++++++++++++++++----- docs/core/oauth2-oidc.md | 6 ++- docs/core/sso-guide.md | 6 +-- specs/2026-08-10-mcp-http-transport.md | 9 +++- specs/2026-08-11-cimd-and-consent.md | 7 ++- 5 files changed, 78 insertions(+), 21 deletions(-) diff --git a/docs/core/mcp.md b/docs/core/mcp.md index 8f49779..38eb8dd 100644 --- a/docs/core/mcp.md +++ b/docs/core/mcp.md @@ -97,14 +97,14 @@ happens rather than what the specs allow. | Client | Works | How | | --- | --- | --- | | **Claude Code, VS Code** — static token | **yes, verified** | Mint a token bound to `/mcp` and pass it as a fixed header (below) | -| **Claude Code** — OAuth | with `--enable-client-id-metadata-document` | Without it Claude Code refuses the server outright. With it the client engages and runs the OAuth flow; the browser leg (login + consent) has not been verified end to end here | +| **Claude Code** — OAuth | with `--enable-dynamic-client-registration` | Claude Code's released version predates CIMD: it reads `client_id_metadata_document_supported`, ignores it, and refuses unless a `registration_endpoint` is advertised. With DCR enabled it registers itself (verified: `POST /oauth/register` → 201, public client, loopback callback) and runs the flow | | **Claude.ai custom connector** — pasted client ID | unverified | Anthropic documents an OAuth Client ID field under *Advanced settings*; not confirmed here | -| Any client that needs to self-register | no | Needs RFC 7591 DCR or a Client ID Metadata Document; Authorizer has neither yet | +| Any client that needs to self-register | yes | Enable `--enable-client-id-metadata-document` (preferred) or `--enable-dynamic-client-registration` (RFC 7591, for clients that predate CIMD) | -Authorizer does not implement RFC 7591 dynamic client registration, and Claude -Code will not fall back to anything else — it refuses the server outright rather -than prompting for a client ID. Until DCR or CIMD lands, **the static-token path -is the supported way to connect Claude Code.** +Both self-registration mechanisms ship in 2.4.0 and are **off by default**. Turn +on the one your client can use — see [Self-registering clients](#self-registering-clients) +below. The static-token path remains the simplest option when you control the +client and do not want an interactive flow at all. ```sh # 1. Create a service account: dashboard → Identity → Clients (note the id + secret) @@ -162,10 +162,9 @@ Restrict which hosts may serve a document with `--client-id-metadata-allowed-domains` if you want a closed deployment; leaving it empty accepts any HTTPS host, which is what a public MCP server wants. -### Why there is no `/register` endpoint +### Self-registering clients: CIMD vs DCR -Authorizer deliberately does **not** implement RFC 7591 dynamic client -registration, and this is unlikely to change. +Authorizer implements **both**, off by default, and CIMD is the one to prefer. The [MCP authorization spec (2025-11-25)](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) demoted it. Authorization servers **SHOULD** support Client ID Metadata @@ -187,12 +186,58 @@ probing, unvetted misconfigured clients, audit gaps — apply with more force to self-hosted product, where every operator would inherit an open, unauthenticated write endpoint and unbounded client-row growth. -**CIMD is the planned path instead.** It makes the `client_id` an HTTPS URL that -the authorization server fetches and validates — no write endpoint, no row -growth, no schema change. It also requires a consent screen, because CIMD makes -client identity self-asserted: the spec requires the authorization server to +**CIMD is therefore the preferred path.** It makes the `client_id` an HTTPS URL +that the authorization server fetches and validates — no write endpoint, no row +growth, no schema change. + +**RFC 7591 DCR ships anyway, behind `--enable-dynamic-client-registration`, +because the clients have not caught up.** Claude Code reads +`client_id_metadata_document_supported: true` from our metadata and still +refuses without a `registration_endpoint`. Without DCR those clients cannot +connect at all. Enabling it does not downgrade anyone: the spec's priority order +is pre-registered → CIMD → DCR, so a CIMD-capable client never reaches the DCR +path. + +Auth0's objections are answered rather than ignored: + +| Risk | Mitigation | +| --- | --- | +| Mass registration / resource depletion | Per-IP rate limiting plus a hard ceiling on registry rows | +| Unvetted, misconfigured clients | PUBLIC clients only — `token_endpoint_auth_method` must be `none`, `client_credentials` is refused, `redirect_uris` must be https or loopback http | +| Impersonation of a known product | Consent screen on every authorization, naming the client and leading with the verified redirect host ([RFC 7591 §5](https://www.rfc-editor.org/rfc/rfc7591.html#section-5) asks for this warning) | +| Weak client authentication | S256 PKCE required at `/authorize`; implicit response types refused | +| Standing client management surface | RFC 7592 not implemented — a self-registered client cannot be read back, modified or deleted through this endpoint | + +Both mechanisms make client identity **self-asserted**, which is why both go +through the same consent screen: the spec requires the authorization server to display the redirect URI hostname and to warn on `localhost`-only clients. +```sh +# Preferred: clients that support Client ID Metadata Documents +authorizer --mcp-enabled --url=https://auth.example.com \ + --enable-client-id-metadata-document + +# Add DCR only if your client cannot do CIMD (e.g. current Claude Code) +authorizer --mcp-enabled --url=https://auth.example.com \ + --enable-client-id-metadata-document \ + --enable-dynamic-client-registration +``` + +Registration itself is one unauthenticated POST: + +```sh +curl -X POST https://auth.example.com/oauth/register \ + -H 'Content-Type: application/json' \ + -d '{ + "client_name": "My MCP Client", + "redirect_uris": ["http://127.0.0.1:5599/callback"], + "grant_types": ["authorization_code", "refresh_token"], + "token_endpoint_auth_method": "none" + }' +# 201 Created -> {"client_id": "...", "client_id_issued_at": ..., ...} +# No client_secret is ever issued: these are public clients. +``` + ## Exposed tools | Tool | Auth required | Description | diff --git a/docs/core/oauth2-oidc.md b/docs/core/oauth2-oidc.md index cc2eac9..55b6569 100644 --- a/docs/core/oauth2-oidc.md +++ b/docs/core/oauth2-oidc.md @@ -33,7 +33,9 @@ This page is the one-stop reference for every endpoint, parameter, and integrati | [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) (Token Exchange) | Implemented | [Delegation-only profile](../enterprise/token-exchange) with nested `act` chain | | RFC 8707 (Resource Indicators) | Implemented | Optional `resource` on `/authorize` + `/oauth/token` (authorization code flow); exactly one required on the token-exchange grant | -**Not yet implemented** (tracked for future releases): RFC 7591 dynamic client registration, RFC 9101 JAR / Request Object, OIDC Session Management iframe, front-channel logout, automated time-based key rotation. +**Not yet implemented** (tracked for future releases): RFC 9101 JAR / Request Object, OIDC Session Management iframe, front-channel logout, automated time-based key rotation. + +RFC 7591 dynamic client registration IS implemented as of 2.4.0, but it is **off by default** — see [Self-registering clients](./mcp#self-registering-clients-cimd-vs-dcr). --- @@ -858,7 +860,7 @@ You will need `curl`, `jq`, `openssl`, and a web browser. curl -s $AUTHORIZER_URL/.well-known/openid-configuration | jq ``` -**Check:** `issuer` matches `$AUTHORIZER_URL`; `response_types_supported` contains the hybrid combinations; `introspection_endpoint` is present; `registration_endpoint` is absent; `backchannel_logout_supported` is `true` iff the flag is set. +**Check:** `issuer` matches `$AUTHORIZER_URL`; `response_types_supported` contains the hybrid combinations; `introspection_endpoint` is present; `registration_endpoint` is absent unless `--enable-dynamic-client-registration` is set; `backchannel_logout_supported` is `true` iff the flag is set. ### 2. JWKS diff --git a/docs/core/sso-guide.md b/docs/core/sso-guide.md index 09d99cb..d3da369 100644 --- a/docs/core/sso-guide.md +++ b/docs/core/sso-guide.md @@ -293,7 +293,7 @@ Several capabilities that used to live on this page's roadmap have shipped. Each Authorizer maintains a client registry: admins can register additional clients — machine service accounts with their own `client_id`, one-time-revealed `client_secret`, and a per-client scope allow-list — alongside the reserved interactive client. Service accounts authenticate via the OAuth2 `client_credentials` grant. See the [Client Registry guide](./client-registry). -> Programmatic *self-service* registration (RFC 7591 Dynamic Client Registration) is still on the roadmap — today clients are registered by an admin via the admin API or dashboard. +> Programmatic *self-service* registration (RFC 7591 Dynamic Client Registration) ships in 2.4.0 but is **off by default** and is scoped to public MCP-style clients — see [Self-registering clients](./mcp#self-registering-clients-cimd-vs-dcr). For everything else, clients are registered by an admin via the admin API or dashboard. ### Organizations @@ -331,7 +331,7 @@ Still planned for future releases: ### Dynamic Client Registration (RFC 7591) -Self-service programmatic client registration (the `registration_endpoint`). Today, new clients are created by an admin through the [client registry](./client-registry) admin API. +Self-service programmatic client registration (the `registration_endpoint`) is available from 2.4.0 behind `--enable-dynamic-client-registration`, and is deliberately narrow: it registers **public** clients only, for the MCP onboarding case. Confidential clients are still created by an admin through the [client registry](./client-registry) admin API. See [Self-registering clients](./mcp#self-registering-clients-cimd-vs-dcr). ### LDAP / Active Directory Integration @@ -353,7 +353,7 @@ Authorizer gives you a **self-hosted, single-binary SSO server** that speaks sta | What You Get Today | What's Coming | |---|---| -| Full OIDC IdP with discovery | Dynamic client registration (RFC 7591) | +| Full OIDC IdP with discovery | Dynamic client registration for *confidential* clients (RFC 7591 covers public clients only) | | 10+ social login providers | LDAP/AD integration | | MFA (TOTP, email OTP, SMS OTP) | Front-channel logout | | RBAC with JWT claims | Automated JWKS rotation | diff --git a/specs/2026-08-10-mcp-http-transport.md b/specs/2026-08-10-mcp-http-transport.md index 782cdfd..6ab3cee 100644 --- a/specs/2026-08-10-mcp-http-transport.md +++ b/specs/2026-08-10-mcp-http-transport.md @@ -67,8 +67,13 @@ onboarding, and the `/authorize` consent screen that CIMD requires (§8.4); per- scope gating; MCP audit events; SSE / stateful sessions; MCP resources and prompts; delegated (RFC 8693) tokens at `/mcp` (§7). -**Not planned**: RFC 7591 dynamic client registration — see §8.3 for why the vendor -documentation argues against it. +**Superseded**: this spec listed RFC 7591 dynamic client registration as "not +planned" (§8.3 collects the vendor arguments against it, and they still stand as +reasons to keep it OFF by default). It shipped anyway in 2.4.0 behind +`--enable-dynamic-client-registration`, because the reasoning was about what +*should* be preferred and not about what shipping clients *can do*: Claude Code +reads `client_id_metadata_document_supported` and still refuses without a +`registration_endpoint`. See `2026-08-11-cimd-and-consent.md` §1. ## 4. Architecture diff --git a/specs/2026-08-11-cimd-and-consent.md b/specs/2026-08-11-cimd-and-consent.md index 5ed48be..be1dbbc 100644 --- a/specs/2026-08-11-cimd-and-consent.md +++ b/specs/2026-08-11-cimd-and-consent.md @@ -99,7 +99,12 @@ makes a client select it and then fail. ## 6. Out of scope -- RFC 7591 DCR — see §1. +- ~~RFC 7591 DCR~~ — **no longer out of scope**: implemented in this same PR + behind `--enable-dynamic-client-registration`, off by default. CIMD stayed the + preferred mechanism, but Claude Code's released version predates CIMD and + refuses a server without a `registration_endpoint`, so CIMD alone left the + flagship client unable to connect — the exact problem §1 opens with. Both + mechanisms share the consent screen and the mandatory-S256-PKCE rule. - `private_key_jwt` for CIMD clients (spec MAY). - Persisted consent grants. - Delegated (RFC 8693) tokens at `/mcp` — still deferred, see the transport spec.