Skip to content

fix(mcp): rewrite multi-tenant OAuth issuer mismatches in metadata#3423

Open
pshickeydev wants to merge 1 commit into
charmbracelet:mainfrom
pshickeydev:fix/mcp-oauth-multi-tenant-issuer-validation
Open

fix(mcp): rewrite multi-tenant OAuth issuer mismatches in metadata#3423
pshickeydev wants to merge 1 commit into
charmbracelet:mainfrom
pshickeydev:fix/mcp-oauth-multi-tenant-issuer-validation

Conversation

@pshickeydev

Copy link
Copy Markdown

Fixes #3419.

The Jira MCP server (https://mcp.atlassian.com/v1/mcp/authv2) requires
OAuth, which v0.87.0 added support for. But the OAuth 2.1 implementation's
strict RFC 8414 issuer validation is incompatible with Atlassian's
multi-tenant authorization server: discovery resolves a tenant-specific URL
(https://auth.atlassian.com/VCeDs...), while the metadata response returns
a shared, top-level issuer (https://auth.atlassian.com). The mismatch fails
before authorization can complete:

calling "initialize": sending "initialize": rejected by transport: failed to
get authorization server metadata: metadata issuer
"https://auth.atlassian.com" does not match issuer URL
"https://auth.atlassian.com/VCeDsk..."

This is a common pattern for multi-tenant providers that front many tenants
with one authorization server: the tenant-specific path is used for
discovery and routing, but the canonical issuer stays at the top-level
domain.

Fix

Extends metadataFixupRoundTripper (already normalizing trailing-slash
issuers per #3396) to also rewrite the issuer when:

  • it shares the same origin (scheme + host) as the discovery URL, and
  • it is a path ancestor of the discovery URL (e.g. https://x.com is an
    ancestor of https://x.com/tenant1; https://x.com/a is not an
    ancestor of https://x.com/ab).

Both conditions must hold before the rewrite fires, so a server can't use
this to impersonate a different issuer — cross-host, cross-scheme, and
non-ancestor-path mismatches all pass through untouched and are still
rejected by the SDK's validation as before. Protected resource metadata
(which has no issuer field) is never touched.

Test plan

  • New unit tests, all passing:

    • TestHandler_MultiTenantIssuerRewrite — end-to-end handler test against
      a fake multi-tenant AS (tenant-specific discovery, shared issuer)
    • TestIssuerFromMetadataURL, TestSameOrigin, TestIsAncestorPath
      table-driven unit tests for the new helpers
    • TestMetadataFixup_RewritesSameOriginAncestor — proves the rewrite
      fires for the intended Atlassian-shaped case
    • TestMetadataFixup_SecurityBoundaries — proves it does not fire for
      cross-host, cross-scheme, non-ancestor, or descendant issuers
    • TestMetadataFixup_ProtectedResourceUntouched,
      TestMetadataFixup_TrailingSlashStillNormalized — regression guards for
      existing behavior
  • go build ./..., go vet ./... (no new findings; one pre-existing
    unrelated warning in internal/csync/maps.go), gofmt -l clean

  • go test ./... — all oauth/MCP packages pass; the only repo-wide
    failures are pre-existing environment issues (missing GCP ADC
    credentials in internal/backend, internal/server,
    internal/workspace), confirmed present and identical on the parent
    commit (e68041a6), so this change introduces no regressions

  • Manual: built the binary and ran the real OAuth flow end-to-end
    against the live Atlassian Rovo MCP server
    (https://mcp.atlassian.com/v1/mcp/authv2) with oauth: true. Debug
    logs confirmed the rewrite fired
    (Rewrote OAuth metadata issuer for multi-tenant provider), the browser
    consent flow completed, and the token was persisted and used
    successfully — where the same flow fails with the issuer-mismatch error
    above on main.

  • I have read CONTRIBUTING.md.

  • I have created a discussion that was approved by a maintainer (for new features).

Multi-tenant providers like Atlassian use a tenant-specific path for
discovery but return a shared, top-level issuer in the metadata response,
causing the SDK's strict RFC 8414 validation to reject the mismatch.
Extend metadataFixupRoundTripper to rewrite the issuer when it shares the
same origin and is a path ancestor of the discovery URL, with security
boundaries preventing cross-host, cross-scheme, and non-ancestor rewrites.

Co-Authored-By: Crush <crush@charm.land>
@charmcli

charmcli commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@pshickeydev

Copy link
Copy Markdown
Author

I have read the Contributor License Agreement (CLA) and hereby sign the CLA.

@taciturnaxolotl

Copy link
Copy Markdown
Member

there was a pr merged in right before this about an hour ago. can you check if that fixed the issue? very possible that it didnt but this sounded pretty similar

@taciturnaxolotl

Copy link
Copy Markdown
Member

ah nvm that was a slightly different issue

@taciturnaxolotl

Copy link
Copy Markdown
Member

okay did some investigating! it looks like your mcp server is out of compliance with RFC 8414 §3.3. Will investigate a bit more when I properly get back next week

The "issuer" value returned MUST be identical to the authorization server's
issuer identifier value into which the well-known URI string was inserted to
create the URL used to retrieve the metadata. If these values are not identical
the data contained in the response MUST NOT be used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP OAuth: strict issuer validation rejects Atlassian's multi-tenant auth server

3 participants