Skip to content

fix(client): accumulate scopes (union) on step-up authorization challenges (SEP-2350)#2265

Open
mattzcarey wants to merge 1 commit into
mainfrom
fix/sep-2350-scope-union-step-up
Open

fix(client): accumulate scopes (union) on step-up authorization challenges (SEP-2350)#2265
mattzcarey wants to merge 1 commit into
mainfrom
fix/sep-2350-scope-union-step-up

Conversation

@mattzcarey

Copy link
Copy Markdown
Contributor

Implements SEP-2350 (tracking issue #2200): client-side scope accumulation in step-up authorization.

Spec

The draft authorization spec (basic/authorization/index.mdx, "Scope Challenge Handling" note and "Step-Up Authorization Flow" step 2) makes scope accumulation a client-side responsibility:

Clients are responsible for scope accumulation: when re-authorizing in response to a scope challenge, clients SHOULD request the union of previously requested/granted scopes and the newly challenged scopes, so that per-operation challenges don't drop previously granted permissions.

Servers stay stateless and emit only per-operation scopes in WWW-Authenticate: Bearer error="insufficient_scope", scope="..." challenges.

Before / after

Before: the 403 insufficient_scope retry path in StreamableHTTPClientTransport did this._scope = scope — the challenged scopes replaced the requested scope, so the re-authorization dropped everything previously granted. A client holding read write that hit a challenge for admin would re-authorize with only admin.

After: the client requests the union of (1) the scope on the stored tokens (previously granted), (2) the previously requested scope, and (3) the newly challenged scope. The same client now re-authorizes with read write admin. The union is order-preserving and exact-string-deduped — scopes are treated as opaque strings per the spec, so no hierarchy-aware deduplication (repo does not absorb repo:read).

The helper is exported as unionScopes(...scopeStrings: Array<string | undefined>): string | undefined from @modelcontextprotocol/client.

Retry limiting is unchanged: the existing _lastUpscopingHeader guard still prevents infinite upscoping loops.

Decision note for reviewers: 401 path left as-is

The 401 path (handleOAuthUnauthorized / the this._scope = scope assignments in the 401 branches of streamableHttp.ts and sse.ts) is intentionally unchanged. The spec's union language is specifically about re-authorization during step-up after an insufficient_scope challenge; a 401 carries initial-auth semantics (no valid grant to preserve), and applying the union there is debatable. Flagging explicitly in case reviewers want union-on-401 too — happy to follow up.

Relatedly, sse.ts has no 403 insufficient_scope handler at all (only 401 paths), so there was nothing to mirror there.

Validation

  • pnpm build:all
  • pnpm typecheck:all
  • pnpm lint:all
  • pnpm --filter @modelcontextprotocol/client test — 378/378 ✅ (new: 3 step-up integration tests in streamableHttp.test.ts covering union, dedup-on-repeated-scope, and no-prior-scope; 8 unionScopes unit tests in auth.test.ts)
  • Changeset: patch for @modelcontextprotocol/client

Downstream impact: cloudflare/agents needs no changes — its DurableObjectOAuthClientProvider persists token scope, so the union picks previously granted scopes up automatically.

A follow-up PR implementing SEP-2468 (iss validation) touches the same transport retry region and will be rebased over this.

Closes #2200

…enges (SEP-2350)

Per the draft authorization spec's step-up authorization flow, scope
accumulation is a client-side responsibility: when re-authorizing after a
403 insufficient_scope challenge, the client SHOULD request the union of
previously requested/granted scopes and the newly challenged scopes.

Previously the StreamableHTTP transport replaced the requested scope with
the challenged scopes only, dropping previously granted permissions.

- Add exported unionScopes() helper (opaque-string, order-preserving,
  deduped union of space-delimited scope strings)
- Union stored token scope + previously requested scope + challenged
  scope in the 403 insufficient_scope retry path
- Tests for the step-up union, dedup, and no-prior-scope cases plus
  unionScopes unit tests

Closes #2200
@mattzcarey mattzcarey requested a review from a team as a code owner June 9, 2026 20:34
@changeset-bot

changeset-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7bb9636

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

This PR includes changesets to release 1 package
Name Type
@modelcontextprotocol/client 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 Jun 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@2265

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/codemod@2265

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@2265

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server-legacy@2265

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@2265

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/fastify@2265

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@2265

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@2265

commit: 7bb9636

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.

Implement SEP-2350: Clarify client-side scope accumulation in step-up authorization

1 participant