Skip to content

fix(auth): accept RFC 8252 §7.1 private-use redirect URI schemes - #12

Open
DUC750 wants to merge 1 commit into
MP-Tool:mainfrom
DUC750:fix/rfc8252-private-use-redirect-schemes
Open

fix(auth): accept RFC 8252 §7.1 private-use redirect URI schemes#12
DUC750 wants to merge 1 commit into
MP-Tool:mainfrom
DUC750:fix/rfc8252-private-use-redirect-schemes

Conversation

@DUC750

@DUC750 DUC750 commented Sep 3, 2026

Copy link
Copy Markdown

Problem

parseClientRedirectUri (src/server/auth/oauth/http.ts) — and, by delegation, the auto-registration isValidRedirectUri (src/server/http/express-app.ts) — accept only https: and http:-loopback redirect URIs.

Native OAuth clients that use a private-use URI scheme as sanctioned by RFC 8252 §7.1 are therefore rejected. Concretely, the current Claude client registers and authorizes with:

redirect_uri = claude://claude.ai/mcp-auth-callback/sdk

The client completes Dynamic Client Registration fine (POST /register → 201), the sign-in page renders, the user enters valid credentials — and POST /authorize/local then returns 400 Invalid redirect URI because parseClientRedirectUri returns null for the claude: scheme. The login is unusable for such clients. The same applies to vscode://…, cursor://…, and reverse-domain schemes like com.example.app:/cb.

Reproduction (holding everything but redirect_uri constant)

redirect_uri before this PR
claude://claude.ai/mcp-auth-callback/sdk 400 Invalid redirect URI
https://app.example.com/cb passes redirect check (reaches credential step)
http://127.0.0.1:1234/cb passes redirect check
http://example.com/cb 400 Invalid redirect URI

Fix

Accept private-use URI schemes in addition to https: / http:-loopback, while keeping a denylist for schemes that can execute script or read local resources (javascript:, data:, vbscript:, file:, blob:).

The https: (any host) and http:-loopback (RFC 8252 §8.3) behaviour is unchanged.

Why this is safe

The scheme allowlist is defense-in-depth, not the authoritative open-redirect guard. The @modelcontextprotocol/sdk authorize handler only ever redirects to a redirect_uri the client previously registered — exact match, or InvalidRequestError('Unregistered redirect_uri'):

// @modelcontextprotocol/sdk .../server/auth/handlers/authorize.js
if (!client.redirect_uris.some((registered) => redirectUriMatches(requested, registered))) {
  throw new InvalidRequestError('Unregistered redirect_uri');
}

An attacker cannot register a victim client's redirect URIs, and PKCE (S256) protects the code regardless. Widening the accepted scheme set to the private-use schemes RFC 8252 explicitly defines does not weaken that guarantee; it only stops over-blocking legitimate native clients. Dangerous script-/local-resource schemes stay rejected.

Changes

  • parseClientRedirectUri: reject a denylist of dangerous schemes, keep https/http-loopback, accept other (private-use) schemes.
  • isValidRedirectUri (auto-registration): now delegates to parseClientRedirectUri, removing the duplicated scheme logic so both the callback path and auto-registration share one definition (they had drifted into two copies of the same rule).
  • Adds the repository's first vitest suite (src/server/auth/oauth/http.test.ts) covering the accept/reject matrix, including claude://… accepted and javascript:/data:/file: rejected.

Verification

Locally against this branch:

npx prettier --check <changed files>   # clean
npx eslint <changed files>             # 0 problems (incl. eslint-plugin-security)
npm run build                          # tsc strict OK
npx vitest run .../http.test.ts        # 6/6 passed

The unit suite proves the scheme matrix. It does not spin up a full browser OAuth round-trip; the motivating end-to-end case is the real claude://… value shown above.

parseClientRedirectUri (and, by delegation, the auto-registration
isValidRedirectUri) accepted only https: and http-loopback redirect URIs.
Native OAuth clients that use a private-use URI scheme (RFC 8252 §7.1) —
e.g. Claude's `claude://claude.ai/mcp-auth-callback/sdk`, VS Code's
`vscode://…`, Cursor's `cursor://…` — were rejected with HTTP 400
"Invalid redirect URI" before credential entry, making the OAuth login
unusable for them.

Accept private-use URI schemes in addition to https/http-loopback, while
keeping a denylist for schemes that can execute script or read local
resources (javascript:, data:, vbscript:, file:, blob:). The authoritative
open-redirect guard remains the SDK authorize handler's exact-match against
the client's registered redirect_uris, so widening the scheme allowlist to
the schemes RFC 8252 sanctions does not weaken that guarantee.

Consolidate the duplicated validation: isValidRedirectUri now delegates to
parseClientRedirectUri so both paths share one definition. Add the
repository's first vitest suite covering the accept/reject matrix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant