Skip to content

fix(mcp-oauth): let a path-less issuer differ from its metadata by a trailing slash - #2133

Merged
zfy0701 merged 2 commits into
agentconnect-md:mainfrom
joerideturck:fix/mcp-oauth-issuer-trailing-slash
Sep 17, 2026
Merged

zfy0701 merged 2 commits into
agentconnect-md:mainfrom
joerideturck:fix/mcp-oauth-issuer-trailing-slash

Conversation

@joerideturck

Copy link
Copy Markdown
Contributor

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 issuer to 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 with issuer_mismatch:

GET https://logging.googleapis.com/.well-known/oauth-protected-resource/mcp
{"resource":"https://logging.googleapis.com/mcp","authorization_servers":["https://accounts.google.com/"], ...}

GET https://accounts.google.com/.well-known/oauth-authorization-server
{"issuer":"https://accounts.google.com", ...}

The same pair appears for monitoring.googleapis.com/mcp, cloudtrace.googleapis.com/mcp and clouderrorreporting.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 !== in readAuthServer. 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 makes https://host and https://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 (/tenant vs /tenant/).

Discovery keeps the document's own spelling as the recorded issuer, which is the value RFC 9207 iss responses will carry. Design §4 step 3 and the test inventory in §8 are updated to describe the concession.

Testing

test:unit in packages/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

joerideturck and others added 2 commits September 17, 2026 08:58
…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>

@agentconnect-md-test agentconnect-md-test Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@zfy0701
zfy0701 merged commit f7266ca into agentconnect-md:main Sep 17, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants