fix(oauth): CIMD and DCR accept the metadata real clients publish (claude.ai, VS Code, Zed, ChatGPT) - #240
Merged
Conversation
…ad of rejecting the client
A CIMD document is the client's self-description for every authorization
server, not an order placed with this one (RFC 7591 §2: the grants the
client "can use"). claude.ai's connector document lists
urn:ietf:params:oauth:grant-type:jwt-bearer next to authorization_code and
refresh_token; the parser rejected the whole document for it, so the
authorize request failed as an unknown client (ID2052) and claude.ai could
not sign in to any Modgud.
CIMD and DCR now register the intersection with {authorization_code,
refresh_token} and require authorization_code to survive; response_types
must include code, other values are ignored. DCR follows the same policy
(RFC 7591 §3.2.1 lets the server replace requested values; the response
already echoes the registered grants), which also closes the gap where a
refresh_token-only DCR registration was accepted.
Tests: the live claude.ai document as a parser fixture, and a full CIMD
flow (authorize, token, refresh) on a document shaped like it — red on the
old parser with exactly ID2052. Docs: CIMD/DCR accepted-fields tables, and
the stale "DCR is public-only" row.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ublish Audit of every CIMD/DCR rejection rule against RFC 7591, RFC 8252 and draft-ietf-oauth-client-id-metadata-document-02, with the live metadata of claude.ai, Claude Code, VS Code, Zed and goose (CIMD) and the registration bodies of VS Code, Zed and the MCP Inspector (DCR) as fixtures. A rule now rejects only what Modgud cannot honour; what it merely does not offer is narrowed away. - Loopback redirect registered WITH a port (VS Code's 127.0.0.1:33418, Zed's ephemeral DCR port) matched only that port; RFC 8252 §7.3 says any port MUST be accepted. The port-less twin is registered alongside, which is what OpenIddict relaxes against. Scheme, host and path still match. - One unusable redirect URI failed the whole document/registration; it is now dropped (DCR echoes what was registered), at least one must survive. Private-use schemes stay out for dynamic clients. - DCR client_name is optional (RFC 7591 §2). Missing or non-Latin-1 names are replaced by the redirect host instead of failing; over-long names are truncated. Reserved names still reject. - A malformed DCR body got the framework's empty 400; it now gets the RFC 7591 §3.2.2 error object. - A CIMD document with a UTF-8 BOM no longer fails to parse. Integration tests for the port rule are red without the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…jwks ChatGPT's connector document authenticates with private_key_jwt and a jwks_uri; Modgud rejected it outright (ADR 0008 v1: public-only), although the CIMD draft forbids only shared-secret methods. It was the last real CIMD client in the fixture set that could not connect. - Parser: token_endpoint_auth_method none, or private_key_jwt with exactly one of jwks_uri (https) / jwks. Shared secrets stay forbidden. - The synthesized client is confidential; MartenApplicationStore asks CimdClientResolver for its key set, since it has no security record. - jwks_uri is fetched through the SSRF-guarded client (64 KB), cached per Cache-Control, and refetched at once when an assertion names an unknown kid — at most once a minute. Unusable keys are skipped; private key material fails the set. - ADR 0008 amended: narrowing policy, loopback twins, private_key_jwt, and no revocation on key rotation (the assertion is re-checked on every token request). Also: OpenIddict 7 accepts only the issuer as a client assertion's aud (draft-ietf-oauth-rfc7523bis §4). PrivateKeyJwtClientAuthTests signed for the token endpoint and read the resulting ID2173 invalid_grant as "client authenticated"; they now use the issuer and check the error is about the grant, plus a test pinning the token-endpoint refusal. The admin and API reference docs told integrators to use the token endpoint as aud — fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CIMD hint still said public PKCE only, the DCR hint too although DCR has issued confidential clients for months, and the CIMD opt-in warning described the pre-#239 scope rule (declared scopes) instead of the per-scope AllowDynamicRegistrationClients opt-in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A search-and-replace turned the helper's own invalid_grant assertion into a recursive call; the stack overflow killed the test host mid-suite, and the runner still printed Passed for the tests it had finished. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.ai could not sign in to any Modgud: its CIMD document lists
urn:ietf:params:oauth:grant-type:jwt-bearernext toauthorization_codeandrefresh_token, and the parser rejected the whole document for it (ID2052, "unknown client"). It was the third CIMD field report in a row (loopback port → beta.6, missingscope→ beta.7). All three had the same root: the CIMD/DCR rules were written against documents we composed ourselves, never against the ones real clients publish.So this PR fixes the report and then audits every CIMD/DCR rejection rule against RFC 7591, RFC 8252 and draft-ietf-oauth-client-id-metadata-document-02, with the live metadata of real clients as test fixtures.
Policy: narrow, don't reject
A CIMD document describes the client for every authorization server; it is not an order placed with this one. A rule now rejects only what Modgud cannot honour at all. Values it merely does not offer are dropped. DCR follows the same policy (RFC 7591 §3.2.1: the response echoes what was registered).
jwt-bearergrantauthorization_code+refresh_tokendevice_codegrant; only port 33418 matchedprivate_key_jwtjwks_uriChanges
authorization_codeandcodemust survive.private_key_jwtfor CIMD: the draft forbids only shared secrets.private_key_jwtwith exactly one ofjwks_uri/jwkssynthesizes a confidential client;MartenApplicationStore.GetJsonWebKeySetAsyncgets the set fromCimdClientResolver(SSRF-guarded fetch, 64 KB, cached per Cache-Control, refetched at once on an unknownkid, at most once a minute). Unusable keys are skipped; private key material fails the set.client_nameis optional (RFC 7591 §2): missing or non-Latin-1 names are replaced by the redirect host instead of failing; over-long names are truncated; reserved names still reject.private_key_jwt, and no revocation on key rotation.Found on the way
aud(draft-ietf-oauth-rfc7523bis §4).PrivateKeyJwtClientAuthTestssigned for the token endpoint and read the resulting ID2173invalid_grantas "client authenticated", so they were green without an assertion ever being accepted. They now use the issuer and check the error concerns the grant; a new test pins the token-endpoint refusal.docs/admin/oauth-clients.mdanddocs/reference/oauth-api.mdtold integrators to use the token endpoint asaud; fixed.Tests
RealWorldClientMetadata(unit): live CIMD documents of claude.ai, Claude Code, VS Code, Zed, goose, ChatGPT (fetched 2026-09-21) and the DCR bodies of VS Code, Zed, MCP Inspector (from source).private_key_jwtflow: no assertion / foreign key →invalid_client, signed flow + refresh work, rotation picked up on first use, a second unknownkidwithin a minute does not refetch; malformed DCR body.executed=total, no host crash).🤖 Generated with Claude Code