Skip to content

fix(client): form-urlencode client_secret_basic credentials (RFC 6749 §2.3.1) - #3039

Open
gdut4140 wants to merge 1 commit into
modelcontextprotocol:mainfrom
gdut4140:fix/client-secret-basic-form-encoding
Open

gdut4140 wants to merge 1 commit into
modelcontextprotocol:mainfrom
gdut4140:fix/client-secret-basic-form-encoding

Conversation

@gdut4140

Copy link
Copy Markdown

Summary

applyBasicAuth base64-encoded the client id and secret as they were. RFC 6749 §2.3.1 requires each half to be form-urlencoded (Appendix B) before they are joined with :, and a conformant token endpoint splits the decoded value on the first : and form-decodes both halves. Credentials containing reserved characters were therefore sent as different credentials, and btoa threw outright for anything outside Latin-1, aborting the token request before it left the process:

client_id / client_secret server recovered, before after
cid / a+b password a b password a+b
client id / sec:ret+with=special username client, password id:sec:ret with=special both recovered intact
cid / p€ss InvalidCharacterError from btoa, no request sent password p€ss

Credentials made only of unreserved characters are byte-identical on the wire, so nothing that worked changes.

Changes

  • packages/client/src/client/auth.ts: a small formUrlEncode helper — Appendix B is the application/x-www-form-urlencoded serializer, which is what URLSearchParams implements, including space → + and ~ → %7E — applied to both halves.
  • packages/client/test/client/authBasicEncoding.test.ts: three tests assert what a conformant server recovers; a fourth drives a real exchangeAuthorization against a recording fetch, so the header on the token request itself is covered.
  • .changeset/client-secret-basic-form-encoding.md.

Verification

  • npx vitest run test/client/authBasicEncoding.test.ts — 4 passed.
  • The same file on unmodified main — 4 failed, e.g. expected { username: 'cid', password: 'a b' } to deeply equal { username: 'cid', password: 'a+b' } and InvalidCharacterError where the test asserts it does not throw.
  • vitest run test/client/auth.test.ts test/client/authExtensions.test.ts — 314 passed.
  • Full packages/client suite — 990 passed, 12 skipped. The only failing files are the three spawn pnpm ENOENT build-dependent ones, which fail identically without this change.
  • pnpm --filter @modelcontextprotocol/client run typecheck clean, eslint src/client/auth.ts clean, prettier --check clean on the changed files.

Notes

v1.x carries the same code (src/client/auth.ts:358). Happy to send the equivalent change to that branch too if you'd like it there.

AI disclosure

Per the organization's AI contribution policy: this PR was prepared with Claude Code (an AI coding assistant) under my direction — it located the encoding deviation and drafted the code and tests. I reviewed the change and the evidence above, the reproduction, tests and suite runs were executed locally in my environment, and I can explain why the change is correct: §2.3.1 is explicit that both halves are encoded, and the tests check what a conformant server recovers rather than only the string we emit.

… §2.3.1)

RFC 6749 §2.3.1 requires the client identifier and the client secret to be
form-urlencoded (Appendix B) before they are joined with ":" and base64-encoded
into the Authorization header; a conformant token endpoint splits the decoded
value on the first ":" and form-decodes both halves. Sending them raw meant a
secret containing "+" arrived as a space, "%" broke the decoding, a ":" inside
the client identifier moved the split, and any character outside Latin-1 made
btoa throw before the request was sent.

Credentials made only of unreserved characters are byte-identical on the wire.
@gdut4140
gdut4140 requested a review from a team as a code owner October 16, 2026 03:24
@changeset-bot

changeset-bot Bot commented Oct 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b4837e8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/core-internal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@3039

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@3039

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@3039

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@3039

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@3039

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@3039

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@3039

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@3039

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@3039

commit: b4837e8

@koriyoshi2041 koriyoshi2041 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Validated exact head b4837e8a against the RFC 6749 §2.3.1 boundary. The focused auth matrix passes 318/318, client typecheck and formatting pass, and reverting only the encoding call makes all four new regressions fail (reserved + becomes a space, raw delimiters are ambiguous, and non-Latin-1 credentials throw before fetch).

The implementation also preserves the existing wire bytes for unreserved ASCII credentials while making both halves independently form-decodable. I found no correctness issue in the changed path.

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Oct 16, 2026

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

v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants