Skip to content

[release/1.x] Validate OAuth PKCE support and metadata issuer - #1949

Open
halter73 wants to merge 5 commits into
release/1.xfrom
halter73-backport-pkce-metadata-enforcement
Open

halter73 wants to merge 5 commits into
release/1.xfrom
halter73-backport-pkce-metadata-enforcement

Conversation

@halter73

@halter73 halter73 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Backport #1700 (7369a26) to release/1.x, with an explicitly approved 1.x security-default adaptation for metadata-free legacy servers. Also backport only the RFC 8414 authorization-server metadata-issuer checks from #1605 (4aade88), preserving its intentional legacy exemption.

The first commit preserves #1700's production change and four regression tests, with cherry-pick attribution. Two test-file conflicts were resolved without importing unrelated scope-selection/offline-access changes. The second commit adds the opt-in compatibility switch described below. The third adds the source-equivalent metadata-issuer checks, related tests/documentation, and the approved test-only conformance package update needed for compliant issuer fixtures. A follow-up changes only the build/test workflow's Node pin from 20 to 22 for the conformance bundle's runtime prerequisite.

  • Require discovered authorization server metadata to advertise S256 in code_challenge_methods_supported; no longer synthesize that field for discovered documents.
  • Continue trying alternative well-known endpoints when a document fails PKCE validation, logging the specific failure. A later eligible document can still succeed.
  • Never replace a discovered document that fails PKCE validation with synthetic legacy defaults, regardless of the switch.
  • When protected resource metadata is available, require the metadata issuer to exactly match the selected authorization server identifier using OriginalString and StringComparison.Ordinal. Missing/mismatched issuers immediately stop discovery before authorization or token exchange; they are not swallowed, retried against another endpoint, or replaced by synthetic defaults.
  • Add no response-aware callback APIs, state-validation backport, or RFC 9207 authorization-response issuer-validation behavior. No main-branch change or unrelated ExceptionDispatchInfo refactor is included.

This supersedes the proposed state-validation backport #1948 with a focused 1.x approach, rather than importing the callback architecture from #1605/#1726.

Deliberate 1.x security-default adaptation

Original #1700 retains the MCP 2025-03-26 default-endpoint fallback when protected resource metadata is unavailable and authorization server metadata discovery finds no usable document. That fallback synthesizes S256, which does not establish PKCE support. Because the existing 1.x callback API returns only a code and cannot provide response state for SDK validation, this backport makes that fallback disabled by default.

ModelContextProtocol.Authentication.AllowLegacyOAuthMetadataFallback is a default-off, process-wide AppContext switch. When absent or false, the client rejects that metadata-free path before requesting an authorization code or exchanging tokens. A host can explicitly restore only the original eligible fallback at startup:

AppContext.SetSwitch(
    "ModelContextProtocol.Authentication.AllowLegacyOAuthMetadataFallback",
    true);

The switch retains the original resourceUri is null guard, /authorize, /token, and /register defaults, and unchanged S256 challenge/verifier behavior. It cannot bypass discovered metadata that omits S256, and the client logs a warning whenever it actually uses this unverified fallback.

Enabling the switch is not verification of PKCE support and does not add state validation. Hosts must independently establish that a trusted deployment correctly enforces PKCE, including downgrade defenses. This is not a per-server allowlist: it affects all OAuth clients in the process. Do not enable it in a process that also connects to untrusted authorization servers or toggle it around individual requests.

The API remarks, transport guide, and protected-client sample documentation explain configuration and security requirements. The conformance client opts in only for its trusted auth/2025-03-26-oauth-endpoint-fallback fixture; other scenarios retain the secure default.

Security rationale

RFC 9700 §2.1 explicitly states that clients which have ensured authorization-server PKCE support "MAY rely on the CSRF protection provided by PKCE". §4.7.1 describes PKCE as an alternative to state for CSRF protection and requires clients to establish server support before relying on it. This backport strengthens that prerequisite for existing code-only callbacks without requiring a new callback API or SDK response-state validation.

This verifies advertised support, not the server's actual verifier or downgrade implementation. Authorization servers must correctly enforce PKCE, including the §4.8.2 downgrade defenses. This is an alternative for the CSRF protection requirement, not a claim that state validation is universally unnecessary or that this approach is security-equivalent to every state-validation design.

Metadata issuer validation is included; response issuer validation remains deferred

RFC 8414 §3.3 requires the metadata issuer to be identical to the issuer identifier used for discovery. This PR includes #1605's metadata-only checks without importing its response-aware callback API. Comparisons do not normalize URI case, trailing slashes, or percent-encoding, so previously accepted equivalent-looking issuer strings may now be rejected. The source's McpException rethrow path is retained: metadata validation failures stop discovery immediately instead of being logged and silently retried.

As in #1605, these issuer checks are exempt when resourceUri is null in the MCP 2025-03-26 legacy path: discovery uses the MCP server's origin rather than an authorization server identifier obtained from protected resource metadata, so it need not equal the canonical issuer. PKCE requirements and the default-off fallback switch remain unchanged.

RFC 9207 authorization-response iss validation addresses authorization-server mix-up, a separate threat; PKCE is not a substitute for issuer validation. That response check still requires response-aware callback plumbing and remains deferred for servicing scope. Including metadata checks is not a claim that response-issuer validation is unnecessary or that 1.x clients are immune to mix-up attacks.

Test fixture dependency update

The test-only @modelcontextprotocol/conformance pin is updated from 0.1.13 to 0.1.15, the earliest stable published release correcting the tenant issuer fixtures. In 0.1.13, auth/metadata-var2 and auth/metadata-var3 advertise an authorization server identifier ending in /tenant1 but return a root metadata issuer, which the new SDK checks correctly reject. modelcontextprotocol/conformance#152, commit 50c3cf1, fixes the server-side issuer to include routePrefix while preserving the tested discovery paths.

This is not an SDK/client validation bypass or a rewritten fixture. The manifest changes one pin; npm updates its lock entry and adds 20 transitive package entries for the new Octokit dependency chain, leaving existing other package versions unchanged. The newer harness also includes scenario specification-version annotations and a shared auth fixture fix that creates a fresh stateless MCP server per request. The existing selected scenarios and full suite pass without unrelated SDK behavior fixes, further upgrades, skipped scenarios, or weakened assertions.

CI runtime prerequisite: The 0.1.15 conformance bundle statically imports fs.globSync. The original Node 20 CI pin cannot load it and fails with SyntaxError: The requested module 'fs' does not provide an export named 'globSync' before the conformance tests start; this is not an issuer-validation regression. Earlier local validation used Node 24.20.0, where that import works, and therefore missed the CI runtime mismatch. The follow-up changes only node-version: '20' to '22' in .github/workflows/ci-build-test.yml, matching main. The pinned setup-node action, other workflows, npm/dependency pins, SDK behavior, and test expectations are unchanged.

Release note

Compatibility tightening: OAuth authentication now requires discovered authorization server metadata to advertise S256 PKCE support, even if the server implements PKCE without advertising it. When protected resource metadata is available, authorization server metadata must also provide an issuer exactly matching the selected authorization server identifier, without normalizing case, trailing slashes, or percent-encoding. Metadata validation failures immediately stop discovery before authorization or token exchange. The original 2025-03-26 metadata-issuer exemption is preserved when protected resource metadata is unavailable; RFC 9207 authorization-response issuer validation is not added.

Metadata-free legacy authentication is also rejected by default. Trusted 2025-03-26 deployments with independently established PKCE support can explicitly restore the historical no-metadata fallback using the process-wide ModelContextProtocol.Authentication.AllowLegacyOAuthMetadataFallback AppContext switch; enabling it emits a warning and never bypasses PKCE validation of discovered metadata.

Validation

  • CI runtime follow-up: solution Debug and Release builds passed with zero warnings/errors. All conformance client/server selections (--filter 'FullyQualifiedName~Conformance') ran under a checksum-verified, process-local Node 22.23.4, not the shared host's Node 24: 72 passed, 2 existing Windows skips on each of .NET 8, 9, and 10 in both configurations (432 passed, 12 skips across the six runs). The fs.globSync module import and conformance CLI startup also succeeded. The portable runtime was removed afterward; shared machine settings and the host Node version were not changed.
  • Earlier OAuth and client conformance selection at the issuer-validation commit, using Node 24.20.0: 77 passed on each of .NET 8, 9, and 10.
  • Earlier complete non-manual solution suite at that commit, using Node 24.20.0: 8,594 passed, 262 skipped, zero failures across .NET 8/9/10 and .NET Framework 4.7.2. The subsequent CI-runtime commit changes only the one workflow pin.
  • Issuer coverage includes matching root/path issuers, missing issuers, mismatched issuers, exact-string case/trailing-slash/percent-encoding rejection, no discovery retry, code-only callback rejection before invocation, no authorization/token activity on rejection, and preserved legacy acceptance with a missing or different canonical issuer.
  • Existing coverage remains for absent/false/true switch states, eligible legacy fallback only when enabled, rejection of discovered missing/plain-only PKCE support even when enabled, alternative discovery success, preservation of the protected-resource-metadata guard, warning emission only when fallback is used, and no authorization/token requests on rejected paths. Global AppContext data is isolated in a nonparallel collection and restored, including the absent state; switch values are not cached.

Local process tests used a test-process-only current-directory PATH entry for Windows executable lookup. npm installed the approved pinned test-dependency update without lifecycle scripts. No machine-wide settings or repository test behavior were weakened.

PranavSenthilnathan and others added 2 commits October 21, 2026 18:48
(cherry picked from commit 7369a26)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep the release/1.x OAuth client fail-closed when PKCE support cannot be verified from authorization server metadata. Retain the historical no-metadata fallback only behind an explicit process-wide compatibility switch, with a warning and deployment requirements.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adapt the metadata-only checks and fail-fast McpException handling from #1605 (4aade88), preserving its 2025-03-26 legacy exemption. Add exact-string matching and pre-authorization rejection coverage without introducing response-aware callbacks or RFC 9207 response validation.

Update the test-only conformance pin from 0.1.13 to 0.1.15 for corrected tenant issuer fixtures (modelcontextprotocol/conformance#152; 50c3cf1a430274ebdaaddeb95ef2c786a6bca290). Keep the PKCE requirements and fallback switch unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@halter73 halter73 changed the title [release/1.x] Require OAuth PKCE metadata by default [release/1.x] Validate OAuth PKCE support and metadata issuer Oct 22, 2026
The pinned conformance 0.1.15 bundle imports fs.globSync, which is unavailable in Node 20. Align the build/test workflow with the Node 22 runtime on main without changing the setup-node action, dependency pins, SDK behavior, or test expectations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread package-lock.json Outdated

@tarekgh tarekgh 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.

I left a minor comment, LGTM, otherwise!

Undo the incidental worktree-directory name captured by npm during the conformance dependency update. Preserve every dependency entry.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

3 participants