diff --git a/docs/core/mcp.md b/docs/core/mcp.md index 19be976..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 | **no** | Claude Code refuses: *"Incompatible auth server: does not support dynamic client registration"* | +| **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) @@ -131,10 +131,40 @@ 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. -### Why there is no `/register` endpoint +### Connecting a client that self-registers (CIMD) -Authorizer deliberately does **not** implement RFC 7591 dynamic client -registration, and this is unlikely to change. +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. + +### Self-registering clients: CIMD vs DCR + +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 @@ -156,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 new file mode 100644 index 0000000..be1dbbc --- /dev/null +++ b/specs/2026-08-11-cimd-and-consent.md @@ -0,0 +1,110 @@ +# 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~~ — **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.