Skip to content

fix: include scope parameter in OAuth authorization code token exchange#1669

Open
Vadaski wants to merge 1 commit intomodelcontextprotocol:mainfrom
Vadaski:fix/941-oauth-scope-in-token-exchange
Open

fix: include scope parameter in OAuth authorization code token exchange#1669
Vadaski wants to merge 1 commit intomodelcontextprotocol:mainfrom
Vadaski:fix/941-oauth-scope-in-token-exchange

Conversation

@Vadaski
Copy link

@Vadaski Vadaski commented Mar 12, 2026

Problem

When exchanging an authorization code for tokens, the SDK never included a scope parameter in the token request. This breaks providers (e.g. Azure AD) that require the scope to be re-stated in the token exchange, and prevents callers from controlling which scope is requested.

Closes #941

Root Cause

prepareAuthorizationCodeRequest() had no scope parameter; and fetchToken() / auth() had no way to thread scope through to the token endpoint.

Changes

prepareAuthorizationCodeRequest()

Added an optional scope?: string parameter. When provided it is appended to the URLSearchParams; when omitted the function behaves exactly as before (no scope field, fully backwards compatible).

fetchToken() — new scope option

The function now accepts an explicit scope option.

Code path Scope behaviour
authorization_code via built-in prepareAuthorizationCodeRequest Only the explicitly supplied scope is forwarded. clientMetadata.scope is never injected. This prevents invalid_scope from servers that narrowed the grant during authorization (RFC 6749 §4.1.3).
Custom prepareTokenRequest() (client_credentials, jwt-bearer, …) Falls back to provider.clientMetadata.scope as before, because these flows start a fresh request rather than redeeming a pre-authorized grant.

auth()tokenExchangeScope

auth() now computes a separate tokenExchangeScope = scope (the raw scope from the WWW-Authenticate challenge, if any). It passes only that explicit server-provided value into fetchToken(), so neither PRM scopes_supported nor clientMetadata.scope are re-injected at the token-exchange step.

resolvedScope (the full fallback chain: WWW-Authenticate → PRM → clientMetadata) continues to be used for DCR and the authorization redirect, as before.

Test Coverage

  • prepareAuthorizationCodeRequest: includes scope when provided; omits scope when not provided.
  • fetchToken: explicit scope reaches the token body; no scope injection from clientMetadata when called without explicit scope.
  • Existing exchangeAuthorization / full auth() tests continue to pass unchanged.

Compatibility

No breaking changes. All existing call sites that do not pass scope to prepareAuthorizationCodeRequest or fetchToken behave identically to before.

…ontextprotocol#941)

- `prepareAuthorizationCodeRequest()` now accepts an optional `scope`
  parameter and appends it to the token request body when provided,
  keeping the call backwards compatible.

- `fetchToken()` gains a `scope` option.  For the `authorization_code`
  path it forwards *only* the explicitly supplied scope so that a server
  which narrowed the granted scope during authorization does not receive
  a broader re-assertion (RFC 6749 §4.1.3).  For the custom
  `prepareTokenRequest()` path (client_credentials, jwt-bearer, …) it
  retains the previous behaviour of falling back to
  `provider.clientMetadata.scope`, because those flows initiate a fresh
  token request rather than redeeming a pre-authorized grant.

- `auth()` computes `tokenExchangeScope` as the explicit scope from the
  WWW-Authenticate challenge only; it does not inject PRM scopes or
  clientMetadata.scope into the token exchange, preventing
  invalid_scope errors from providers (e.g. Azure AD) that narrow the
  granted scope at the authorization endpoint.

Closes modelcontextprotocol#941

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Vadaski Vadaski requested a review from a team as a code owner March 12, 2026 07:12
@changeset-bot
Copy link

changeset-bot bot commented Mar 12, 2026

⚠️ No Changeset found

Latest commit: 98dd73a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 12, 2026

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/server

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

@modelcontextprotocol/express

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 163e3dd

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.

OAuth: scope parameter missing from token exchange requests

1 participant