Skip to content

Implement SEP-990 Enterprise Managed OAuth#1593

Open
sagar-okta wants to merge 9 commits intomodelcontextprotocol:mainfrom
sagar-okta:feature/sep-990
Open

Implement SEP-990 Enterprise Managed OAuth#1593
sagar-okta wants to merge 9 commits intomodelcontextprotocol:mainfrom
sagar-okta:feature/sep-990

Conversation

@sagar-okta
Copy link

@sagar-okta sagar-okta commented Feb 26, 2026

This PR implements SEP-990 Enterprise Managed Authorization using a provider-based approach with RFC 8693 Token Exchange and RFC 7523 JWT Bearer flows. This enables secure cross-app authentication for MCP clients in enterprise environments where users authenticate with an enterprise IdP.

Related: #1090

Motivation and Context

Enterprise environments often require OAuth flows where users authenticate with a centralized identity provider (IdP), and applications need to securely access protected resources on behalf of those users without storing credentials. SEP-990 addresses this by implementing:

  • Token Exchange (RFC 8693): Exchanges a user's ID token from an enterprise IdP for a JWT Authorization Grant (JAG)
  • JWT Bearer Grant (RFC 7523): Exchanges the JAG for an access token to access MCP server resources
  • OAuth Discovery (RFC 9728): Automatically discovers authorization server metadata for both IdP and MCP servers

This change is needed to support enterprise customers who need to integrate MCP clients into their existing OAuth infrastructure securely, following the same provider pattern as ClientCredentialsProvider and PrivateKeyJwtProvider.

Implementation Approach

Following PR review feedback, this implementation uses a provider-based approach instead of middleware:

Layer 2 - Utility Functions (crossAppAccess.ts):

  • requestJwtAuthorizationGrant() - RFC 8693 token exchange at IdP
  • discoverAndRequestJwtAuthGrant() - Discovery + token exchange
  • exchangeJwtAuthGrant() - RFC 7523 JWT bearer grant at MCP server

Layer 3 - Provider Class (authExtensions.ts):

  • CrossAppAccessProvider - Implements OAuthClientProvider interface
  • Uses assertion callback pattern for flexible IdP integration
  • Automatically handles OAuth discovery and token exchange flow

Core Integration (auth.ts):

  • Enhanced OAuthClientProvider interface with optional URL storage methods
  • Modified auth() to save discovered URLs for providers that need them

How Has This Been Tested?

Added comprehensive test coverage (40 tests total):

crossAppAccess.test.ts (12 tests):

  • Successful token exchange flows
  • OAuth discovery integration
  • Error handling (400, 401, 500 responses)
  • OAuth error responses (invalid_request, invalid_client, invalid_grant, etc.)
  • Token type validation
  • Request parameter encoding

authExtensions.test.ts (16 new tests):

  • End-to-end provider authentication flow
  • Assertion callback with context (authorizationServerUrl, resourceUrl, scope, fetchFn)
  • Custom fetch function handling
  • Authorization server and resource URL storage/retrieval
  • Client metadata configuration
  • Error handling for missing URLs and assertion failures

Updated client.md:

  • Removed old middleware-based documentation
  • Added new provider-based usage examples
  • Positioned in Authentication section alongside other providers

Breaking Changes

No breaking changes - This is an additive feature:

  • New CrossAppAccessProvider class (does not modify existing providers)
  • Optional methods added to OAuthClientProvider interface (backward compatible)
  • Existing auth flows remain unchanged
  • Old middleware approach completely removed (was never released)

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally (40/40 tests passing)
  • I have added appropriate error handling
  • I have added or updated documentation as needed
  • Code is properly formatted (ESLint + Prettier)
  • No unused imports or TypeScript errors

Additional Context

Implementation Details:

New Files:

  • crossAppAccess.ts (230 lines) - Layer 2 utilities for token exchange
  • crossAppAccess.test.ts (12 test cases)

Modified Files:

  • authExtensions.ts - Added CrossAppAccessProvider class (~80 lines)
  • auth.ts - Enhanced OAuthClientProvider interface with optional URL storage methods
  • middleware.ts - Removed old withCrossAppAccess middleware
  • index.ts - Added export for crossAppAccess module
  • authExtensions.test.ts - Added 16 provider tests
  • client.md - Updated documentation with provider-based examples

Key Features:

  • Assertion callback pattern allows flexible IdP integration
  • Automatic OAuth metadata discovery for both IdP and MCP servers
  • Comprehensive error handling with OAuth-specific error parsing
  • Consistent with existing provider patterns (ClientCredentialsProvider, PrivateKeyJwtProvider)
  • Supports custom fetch functions for middleware composition
  • Scope passing through the entire flow

Dependencies:

  • Uses existing qs dependency for proper OAuth parameter encoding
  • No new dependencies added

@sagar-okta sagar-okta requested a review from a team as a code owner February 26, 2026 06:15
@changeset-bot
Copy link

changeset-bot bot commented Feb 26, 2026

⚠️ No Changeset found

Latest commit: a5cc8ed

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 Feb 26, 2026

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/server

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

@modelcontextprotocol/express

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 735c7f1

pcarleton added a commit that referenced this pull request Mar 13, 2026
On top of #1593. See /tmp/sdk-xaa-fixes-pr.md for full details.

- clientSecret optional in requestJwtAuthorizationGrant
- exchangeJwtAuthGrant uses applyClientAuthentication dispatcher (client_secret_basic default)
- drop case-sensitive token_type !== 'N_A' check
- better PRM error message in authExtensions
- IdJagTokenExchangeResponseSchema Zod validation
- export applyClientAuthentication + applyBasicAuth + applyPostAuth
On top of modelcontextprotocol#1593. See /tmp/sdk-xaa-fixes-pr.md for full details.

- clientSecret optional in requestJwtAuthorizationGrant
- exchangeJwtAuthGrant uses applyClientAuthentication dispatcher (client_secret_basic default)
- drop case-sensitive token_type !== 'N_A' check
- better PRM error message in authExtensions
- IdJagTokenExchangeResponseSchema Zod validation
- export applyClientAuthentication + applyBasicAuth + applyPostAuth
Adds a handler for the auth/cross-app-access-complete-flow extension
scenario. Uses CrossAppAccessProvider with requestJwtAuthorizationGrant
in the assertion callback to perform the full SEP-990 flow:

  1. RFC 9728 PRM discovery (provider)
  2. RFC 8693 token exchange at IdP: ID token -> ID-JAG (callback)
  3. RFC 7523 JWT bearer at AS: ID-JAG -> access token (provider,
     client_secret_basic)

Context schema mirrors conformance/src/schemas/context.ts.
Conflicts:
  test/conformance/src/everythingClient.ts
    - kept both pre-registration (from modelcontextprotocol#1650) and cross-app-access
      discriminated union variants

Also: drop unused OAuthClientInformation import from crossAppAccess.ts
(lint failure from ef35f0b).
- Run prettier on crossAppAccess.ts (applyClientAuthentication call
  from ef35f0b wasn't formatted).
- Remove auth/cross-app-access-complete-flow from expected-failures
  baseline; it passes now (289/289 in CI).
There is no /crossAppAccess subpath export; everything is re-exported
from the package root. Collapse into a single import.
The XAA example in docs/client.md was the only inline (un-typechecked)
ts block in the file — every other snippet uses the source= pattern.
That's why the /crossAppAccess subpath import broke silently.

- Add auth_crossAppAccess region to clientGuide.examples.ts, add
  CrossAppAccessProvider + discoverAndRequestJwtAuthGrant to the
  shared imports region.
- Replace inline markdown with sourced fence, run sync:snippets.
- Drop two @linkcode CrossAppAccessProvider refs in crossAppAccess.ts
  that TypeDoc can't resolve cross-module; use plain backticks.
*/
authMethod?: ClientAuthMethod;
fetchFn?: FetchLike;
}): Promise<{ access_token: string; token_type: string; expires_in?: number; scope?: string }> {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: should this be OAuthTokens instead? That has 2 more fields but seems conceptually the same thing

issued_token_type: z.literal('urn:ietf:params:oauth:token-type:id-jag'),
access_token: z.string(),
token_type: z.string().optional(),
expires_in: z.number().optional(),
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: should this be z.number().coerce().optional() to match

expires_in: z.coerce.number().optional(),
?

const parseResult = OAuthErrorResponseSchema.safeParse(errorBody);
if (parseResult.success) {
const { error, error_description } = parseResult.data;
throw new Error(`Token exchange failed: ${error}${error_description ? ` - ${error_description}` : ''}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: should we use this here to make sure it's a OAuthError?

export async function parseErrorResponse(input: Response | string): Promise<OAuthError> {

throw await parseErrorResponse(response)

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