Implement SEP-990 Enterprise Managed OAuth#1328
Implement SEP-990 Enterprise Managed OAuth#1328sagar-okta wants to merge 8 commits intomodelcontextprotocol:mainfrom
Conversation
|
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
|
Hi @sagar-okta, I owe you a review on this but won't be able to get to it until Jan 23rd while I wrap up conformance tests for SDK tiering. To make progress in the meantime, a conformance test for this feature would be really helpful to ensure the implementations are compatible across SDKs. Cross-linking: modelcontextprotocol/python-sdk#1721 Thanks for your patience! |
82931fd to
a783bde
Compare
pcarleton
left a comment
There was a problem hiding this comment.
I think this would be better implemented as an OAuthProvider similar to the ClientCredentials providers.
it would look something like this:
class CrossAppAccessProvider implements OAuthClientProvider {
// Standard no-op stubs like ClientCredentialsProvider
get redirectUrl(): undefined { return undefined; }
// ... tokens(), saveTokens(), etc.
get clientMetadata(): OAuthClientMetadata {
return {
client_name: 'cross-app-access-client',
redirect_uris: [],
grant_types: ['urn:ietf:params:oauth:grant-type:jwt-bearer'],
token_endpoint_auth_method: 'client_secret_basic'
};
}
async prepareTokenRequest(scope?: string): Promise<URLSearchParams> {
// Step 1: Exchange ID token for JAG at the IDP
const jag = await this.fetchAuthorizationGrant(scope);
// Step 2 params: JWT bearer grant (executed by withOAuth infra)
const params = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jag
});
if (scope) params.set('scope', scope);
return params;
}
private async fetchAuthorizationGrant(scope?: string): Promise<string> {
// Token exchange (RFC 8693) against IDP token endpoint
// Uses URLSearchParams, proper error handling, Zod parsing
// ...
}
}
Additionally, with the changes proposed in https://github.com/modelcontextprotocol/typescript-sdk/pull/1527/changes you should be able to grab the other discovery metadata you need from the provider, or alternatively provide them to the provider and have them used elsewhere in the flow.
There was a problem hiding this comment.
please use OAuthErrorCode from packages/core/src/auth/errors.ts and edit there as needed
There was a problem hiding this comment.
similar here, please add these to core if they're not already there.
There was a problem hiding this comment.
Moved the enum to auth.ts inside core
There was a problem hiding this comment.
these should also be able to use the types from core.
There was a problem hiding this comment.
Though a similar type was found in core we kept the existing one since we are using enum for error type. Moved this as well to auth.ts under core.
pcarleton
left a comment
There was a problem hiding this comment.
I think this would be better implemented as an OAuthProvider similar to the ClientCredentials providers.
it would look something like this:
class CrossAppAccessProvider implements OAuthClientProvider {
// Standard no-op stubs like ClientCredentialsProvider
get redirectUrl(): undefined { return undefined; }
// ... tokens(), saveTokens(), etc.
get clientMetadata(): OAuthClientMetadata {
return {
client_name: 'cross-app-access-client',
redirect_uris: [],
grant_types: ['urn:ietf:params:oauth:grant-type:jwt-bearer'],
token_endpoint_auth_method: 'client_secret_basic'
};
}
async prepareTokenRequest(scope?: string): Promise<URLSearchParams> {
// Step 1: Exchange ID token for JAG at the IDP
const jag = await this.fetchAuthorizationGrant(scope);
// Step 2 params: JWT bearer grant (executed by withOAuth infra)
const params = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jag
});
if (scope) params.set('scope', scope);
return params;
}
private async fetchAuthorizationGrant(scope?: string): Promise<string> {
// Token exchange (RFC 8693) against IDP token endpoint
// Uses URLSearchParams, proper error handling, Zod parsing
// ...
}
}
Additionally, with the changes proposed in https://github.com/modelcontextprotocol/typescript-sdk/pull/1527/changes you should be able to grab the other discovery metadata you need from the provider, or alternatively provide them to the provider and have them used elsewhere in the flow.
|
Hi @pcarleton |
Replaces the standalone middleware approach from PR #1328 with: - CrossAppAccessProvider in authExtensions.ts: plugs into withOAuth for token caching, 401 retry, client_secret_basic, and Zod validation - assertion callback: receives orchestrator context (AS URL, resource URL, scope, fetchFn), returns JAG string. Decouples IDP interaction from provider - requestJwtAuthorizationGrant in crossAppAccess.ts: standalone Layer 2 utility for RFC 8693 token exchange (ID token → JAG) - saveResourceUrl on OAuthClientProvider: orchestrator saves resource URL so providers can use it in prepareTokenRequest Removes: withCrossAppAccess middleware, xaaUtil.ts (597 lines), qs dependency Adds: conformance test scenario (auth/cross-app-access-complete-flow) - passes 9/9
46e32f9 to
adbfabd
Compare
This PR implements SEP-990 which adds support for Enterprise Managed OAuth using RFC 8693 Token Exchange and RFC 7523 JWT Bearer flows. This enables secure machine-to-machine authentication for MCP clients in enterprise environments without requiring user interaction.
Related: #1090
Motivation and Context
Enterprise environments often require more secure OAuth flows that don't involve user interaction for machine-to-machine communication. SEP-990 addresses this by implementing:
Token Exchange (RFC 8693): Allows exchanging an ID token from an enterprise IDP for an authorization grant
JWT Bearer Grant (RFC 7523): Enables exchanging the authorization grant for an access token to access MCP resources
This change is needed to support enterprise customers who need to integrate MCP clients into their existing OAuth infrastructure securely.
How Has This Been Tested?
Added comprehensive unit tests in xaa-util.test.ts (994 new test cases) covering:
Successful token exchange flows
Authorization grant request failures (400, 401, 500 errors)
Access token exchange failures
OAuth error handling (invalid_request, invalid_client, invalid_grant, etc.)
Edge cases and validation (empty responses, malformed JSON, special characters encoding)
Token type validation
Request body encoding
Added middleware tests in middleware.test.ts (55 additional test cases)
Added documentation in client.md
Breaking Changes
No breaking changes - This is an additive feature that introduces new functionality without modifying existing APIs.
Types of changes
Checklist
Additional context
Implementation Details:
New utility module xaa-util.ts (593 lines) implementing the core token exchange logic
New middleware functions in middleware.ts for integrating XAA into the auth flow
Added qs dependency for proper URL encoding of OAuth request parameters
Comprehensive error handling for various OAuth error responses
Support for OAuth metadata discovery for both IDP and MCP authorization servers