fix(mcp-oauth): let a path-less issuer differ from its metadata by a trailing slash - #2133
Conversation
…trailing slash RFC 8414 §3.3 asks for the metadata `issuer` to be byte-identical to the identifier the well-known URL was built from, and discovery enforced exactly that. Google publishes the two spellings inconsistently: its protected-resource documents (logging.googleapis.com/mcp, monitoring.googleapis.com/mcp, ...) name `https://accounts.google.com/`, while accounts.google.com's own metadata says `https://accounts.google.com`. Every Google MCP server therefore failed at Connect with `issuer_mismatch`. `issuerEquivalent` accepts the one difference RFC 3986 §6.2.3 already calls equivalent — an empty path with or without its slash — and nothing else: another path, host, scheme or port, a query, or a trailing slash on a non-empty path still mismatch. The document's own spelling is what discovery keeps, since that is the value `iss` responses will carry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…idation Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed e91fe42. No blocking regressions found. Both trailing-slash directions work, the metadata document's issuer spelling is retained, and callback issuer validation remains exact.
One non-blocking note: issuerEquivalent accepts additional URL normalizations beyond the documented slash-only exception; see the inline comment.
Validation: direct Node discovery and callback smoke checks and git diff --check passed. The OAuth unit suite remains unverified: its command triggered a full workspace dependency install, which I stopped before tests ran.
sent by review-bot (Codex · gpt-6-astra) · open in session
| } | ||
| if (a.pathname !== '/' || b.pathname !== '/') return false | ||
| if (a.search !== '' || b.search !== '' || a.hash !== '' || b.hash !== '') return false | ||
| return a.origin === b.origin && a.origin !== 'null' |
There was a problem hiding this comment.
[P3] Keep the exception limited to the original trailing-slash difference
new URL() normalizes before this comparison, so https://auth.example.test:443/ and https://auth.example.test/tenant/.. both compare equal to https://auth.example.test. I reproduced both against this helper. This broadens the stated exception beyond a root trailing slash. Consider retaining the root-path guard and requiring the original strings to differ by exactly one final /. Non-blocking here: these examples retain the same origin, and callback validation still compares the recorded issuer exactly.
Summary
OAuth discovery for a custom MCP provider (docs/designs/mcp-provider-oauth.md §4) fetches the authorization server's metadata for the issuer named in the protected-resource document and requires the document's
issuerto be byte-identical to it, as RFC 8414 §3.3 asks. Google spells the two differently, so every Google-hosted MCP server fails at Connect withissuer_mismatch:The same pair appears for
monitoring.googleapis.com/mcp,cloudtrace.googleapis.com/mcpandclouderrorreporting.googleapis.com/mcp. Nothing an operator enters can work around it: the authorization-server value comes from Google's document, not from the provider form.Change
issuerEquivalent(advertised, expected)replaces the!==inreadAuthServer. It accepts byte-identical values, plus exactly one concession: an issuer with an empty path may be written with or without its trailing slash. RFC 3986 §6.2.3 already makeshttps://hostandhttps://host/the same resource, so this is the interoperability reading of §3.3 rather than a relaxation of what it protects against.Still a mismatch, so a document fetched from one host can never claim to be another's: a different host, scheme or port, any path difference, a query or fragment, and a trailing slash on a non-empty path (
/tenantvs/tenant/).Discovery keeps the document's own spelling as the recorded issuer, which is the value RFC 9207
issresponses will carry. Design §4 step 3 and the test inventory in §8 are updated to describe the concession.Testing
test:unitinpackages/control-plane,src/mcp-oauth: 121 passed. The former "byte-exact trailing slash" test now asserts the concession in both directions and that the document's spelling is what discovery records; a new table test pins every other difference as a mismatch. Verified against Qai (Qargo's deployment) on 1.58.0 + this commit: Logging and Monitoring providers pass discovery and connect.🤖 Generated with Claude Code