Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 89 additions & 13 deletions docs/core/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<url>/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)
Expand All @@ -131,10 +131,40 @@ nothing useful and permission checks resolve to `service_account:<client_id>`. 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
Expand All @@ -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 |
Expand Down
6 changes: 4 additions & 2 deletions docs/core/oauth2-oidc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

---

Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions docs/core/sso-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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 |
Expand Down
9 changes: 7 additions & 2 deletions specs/2026-08-10-mcp-http-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
110 changes: 110 additions & 0 deletions specs/2026-08-11-cimd-and-consent.md
Original file line number Diff line number Diff line change
@@ -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.