plain-oauthserver: accept Client ID Metadata Documents (SEP-991) - #108
Draft
davegaeddert wants to merge 1 commit into
Draft
plain-oauthserver: accept Client ID Metadata Documents (SEP-991)#108davegaeddert wants to merge 1 commit into
davegaeddert wants to merge 1 commit into
Conversation
A client_id may now be an HTTPS URL to a hosted metadata document, which is what Claude's custom connector uses by default (its "recommended" option) and what Claude Code uses since v2.1.81. A DCR-only server made people pick the non-default option in Claude's dialog. - New `cimd.py`: URL validation per draft-ietf-oauth-client-id-metadata- document-02, an SSRF-guarded fetch (public-address-only DNS, connection pinned to the checked address with the hostname as SNI, no redirects, 5 KB cap, 5 s deadline), document validation (public clients only), and a resolver that stores the document as an `OAuthApplication` row keyed by the URL with a Cache-Control-derived TTL clamped to [5m, 24h] and a 7-day stale-if-error grace. - `/authorize` resolves URL client_ids; token and revocation endpoints never fetch. Metadata advertises `client_id_metadata_document_supported`. - Consent screen shows the metadata host, the redirect host, and a warning for loopback-only clients, per the MCP security guidance. - Settings: `OAUTH_SERVER_ALLOW_CLIENT_ID_METADATA_DOCUMENTS` (default on) and `OAUTH_SERVER_CLIENT_ID_METADATA_ALLOWED_HOSTS` (default any public host). - Migration adds `metadata_fetched_at` / `metadata_expires_at`. - `_is_allowed_redirect_uri` moved from views to models so cimd.py can share it. New dependency on httpx. - Tests: internal vectors for the URL, address, TTL, fetch, and document rules using Claude's frozen documents; public end-to-end flows through the endpoints. Conformance doctor probes the CIMD path and the README documents the MCP conformance CLI invocation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1WfjEHGYHdbH5Yd9diDBg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Claude's Add custom connector dialog now marks Client ID Metadata Documents (CIMD, MCP SEP-991) as the recommended OAuth-client option, and Claude Code uses CIMD since v2.1.81. MCP 2026-07-28 deprecates DCR. A DCR-only
plain.oauthserverforces people to pick the non-default option in Claude's dialog.Claude selects CIMD only when the AS metadata advertises both
client_id_metadata_document_supported: trueand"none"intoken_endpoint_auth_methods_supported; otherwise it silently falls back to DCR.What
cimd.py(new): URL validation perdraft-ietf-oauth-client-id-metadata-document-02; an SSRF-guarded fetch (every DNS answer must be globally routable, incl. seeing through IPv4-mapped / 6to4 / NAT64; the connection is pinned to the checked address with the hostname as SNI andHost, so DNS rebinding can't swap it; no redirects; 5 KB cap; 5 s deadline; JSON content type required); document validation (public clients only —private_key_jwtand anyclient_secret*rejected); and a resolver that stores the document as anOAuthApplicationrow keyed by the URL.Cache-Control(s-maxage/max-age) clamped to [5 min, 24 h], 1 h default. A failed refetch serves the stored copy for 7 days (claude.ai's metadata endpoints have had outages that took down live-fetch-only servers)./authorizeresolves URLclient_ids. Token and revocation endpoints never fetch — the row exists from authorize, the client is proven by PKCE / the refresh token, andredirect_uriis bound to the code. All fetches happen outsidetransaction.atomic().client_id_metadata_document_supported: true(gated by the new setting). DCR stays on by default.client_nameis self-asserted), the redirect host, and a warning for loopback-only clients — per the MCP security-considerations page.OAUTH_SERVER_ALLOW_CLIENT_ID_METADATA_DOCUMENTS(defaultTrue),OAUTH_SERVER_CLIENT_ID_METADATA_ALLOWED_HOSTS(defaultNone= any public host).0002: nullablemetadata_fetched_at/metadata_expires_atonOAuthApplication._is_allowed_redirect_urimoved fromviews.pytomodels.pysocimd.pycan share it. New dependency:httpx.client_id, so revocation must be scoped by(user, application)), plain-mcp README handshake wording, conformance README, anddoctor.pynow probes the CIMD path.Testing
./scripts/test plain-oauthserver: 165 passed. Newtests/internal/test_cimd_internals.py(URL / address / TTL / fetch-via-httpx.MockTransport/ document vectors, using Claude's and ChatGPT's frozen live documents) andtests/public/test_cimd.py(end-to-end through the endpoints: Claude full flow with no/register, Claude Code's port-less loopback with an ephemeral port, refetch-on-expiry, stale-if-error, grace expiry, rejections, allowlist, setting off, registered clients unaffected)../scripts/type-check plain-oauthserverclean; ruff clean.npx @modelcontextprotocol/conformance authorization --url <app> --client-id https://claude.ai/oauth/claude-code-client-metadata -p 3000(works with no hosted document because our loopback match is port-agnostic), then Claude's connector dialog on the recommended option.Plan and research:
plain/oauthserver/cimd.mdin the futures repo.🤖 Generated with Claude Code
https://claude.ai/code/session_01C1WfjEHGYHdbH5Yd9diDBg
Generated by Claude Code