Complete OAuth discovery + DCR response for hosted clients (0.6.1) - #7
Merged
Conversation
…s discover the AS A path-ful MCP issuer (https://host/mcp) has two readings of where its OAuth metadata lives: RFC 8414 §3.1 INSERTS the well-known segment before the path (/.well-known/oauth-authorization-server/mcp, already served), while some clients APPEND it (host/mcp/.well-known/oauth-authorization-server). The latter 404'd with no fallback, so a client using that convention never got a registration_endpoint and reported a registration failure. Draw the appended forms (AS + protected-resource + openid-configuration OIDC alias) under the engine mount, gated on oauth_bridge?. They stay path-scoped, claim nothing origin-global, and reuse the same actions, so each document is byte- identical to its inserted twin and carries Cache-Control: no-store. Bump to 0.6.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The register stub returned only a client_id plus a fixed auth method and grant/ response types. A strict client validates that the redirect_uris it registered come back and abandons a registration that drops them — a plausible cause of the "couldn't register" failure independent of the appended-metadata discovery gap (a hosted client was unblocked simply by supplying a manual client_id, which skips DCR entirely, confirming the registration ceremony is the failure locus). register now reflects the client's redirect_uris, token_endpoint_auth_method, grant_types, response_types and client_name, and adds client_id_issued_at / client_id_expires_at: 0. Echoing authorizes nothing: no client is stored, and authorize/token still gate every redirect_uri against the host allowlist, so a value reflected here is not thereby permitted. Still 0.6.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
f2bc141 reflected the client's token_endpoint_auth_method, grant_types and response_types back verbatim. That contradicts our own discovery document — the client fetched registration_endpoint FROM authorization_server, which advertises token_endpoint_auth_methods_supported: ["none"] and grant_types_supported: ["authorization_code"] — and it promises flows the token endpoint rejects: a reflected refresh_token is a refresh answered unsupported_grant_type, and a reflected implicit response_type is a request authorize never honours. RFC 7591 §3.2.1 states the metadata as REGISTERED and lets a server replace what it does not support, so those three are now substituted rather than echoed. The supported sets are named once (SUPPORTED_GRANT_TYPES / SUPPORTED_RESPONSE_TYPES) and shared by both documents, so they cannot drift apart; a spec pins the two against each other. redirect_uris and client_name are still echoed — that is the compatibility payload the commit was for, and the only part with evidence behind it. Also drops client_id_expires_at, which RFC 7591 does not define (it defines client_secret_expires_at, and no client_secret is ever issued here). It was added under a comment citing §3.2.1, which is the class of error where a comment claims a compliance the code does not have. The E2E now registers asking for refresh_token, an implicit response type and client_secret_post, so substitution is exercised over a real request cycle rather than the defaults an empty body would take. 617 examples, 0 failures across three random orders; rubocop 65/0; brakeman 0 with CI's flags. Still 0.6.1. Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Two additive, bridge-gated fixes for hosted MCP clients whose OAuth setup couldn't complete against the 0.6.0 bridge. Byte-identical to 0.6.0 when the bridge is off, or for a client that already worked.
Fix 1 — metadata at the path-appended discovery location
The bridge served its authorization-server / protected-resource metadata only at the RFC 8414 §3.1 path-inserted location (
/.well-known/oauth-authorization-server/mcp). Some clients instead request the path-appendedhttps://host/mcp/.well-known/oauth-authorization-serverand got aRoutingError404 with no fallback (confirmed in prod logs). The bridge now answers at BOTH forms — AS + protected-resource + theopenid-configurationOIDC alias — under the engine mount, gated onoauth_bridge?. They stay under the mount, so they claim nothing origin-global; the "bare origin paths stay 404" test is unchanged.Fix 2 — DCR response echoes the registered metadata (RFC 7591)
The
registerstub returned only aclient_idplus a fixed auth method and grant/response types. A strict client validates that theredirect_urisit registered come back and abandons a registration that drops them — a plausible cause of the same "couldn't register" error, independent of Fix 1.Field evidence for this: a hosted client (Seb's report) was unblocked simply by supplying a manual OAuth Client ID + Secret with junk values in the connector settings — which makes Claude skip DCR entirely. That the auth flow then completes proves (a) discovery and the auth/token legs are sound, (b)
client_id/secretare ignored by design, and (c) the failure is isolated to the registration ceremony. Since discovery and our/registerboth demonstrably work when reached, the likeliest remaining cause is the client rejecting our bare 201.registernow reflects the client'sredirect_uris,token_endpoint_auth_method,grant_types,response_typesandclient_name, and addsclient_id_issued_at/client_id_expires_at: 0(RFC 7591 §3.2.1).Echoing authorizes nothing: no client is stored, and
authorize/tokenstill check everyredirect_uriagainst the host allowlist independently — a value reflected here is not thereby permitted. The redirect allowlist + PKCE security model is untouched.Why both
We can't fully isolate which of the two was the cause of the specific Cowork failure from the two-day-old logs, and both fixes are additive, RFC-correct, and touch no security control. Shipping both makes 0.6.1 robust to whichever it was. Validate after deploy by connecting Cowork with no manual client_id.
Tests
controller_methods_spec+bridge_end_to_end_spec(real Rails, child process): new examples assert the appended AS/protected-resource/OIDC documents (200, correct issuer +registration_endpoint+no-store) and that the DCR response echoesredirect_uris+ RFC 7591 fields (and defaults them when the client sends none).Ships as 0.6.1 (CHANGELOG + README updated).
🤖 Generated with Claude Code