diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 44516130..2cd9a2d1 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -938,8 +938,7 @@ describe("OAuth Authorization", () => { // Call the auth function with a resource that has a fragment const result = await auth(mockProvider, { - serverUrl: "https://resource.example.com", - resource: new URL("https://api.example.com/mcp-server#fragment"), + serverUrl: "https://api.example.com/mcp-server#fragment", }); expect(result).toBe("REDIRECT"); @@ -987,8 +986,7 @@ describe("OAuth Authorization", () => { // Call auth without authorization code (should trigger redirect) const result = await auth(mockProvider, { - serverUrl: "https://resource.example.com", - resource: new URL("https://api.example.com/mcp-server"), + serverUrl: "https://api.example.com/mcp-server", }); expect(result).toBe("REDIRECT"); @@ -1048,9 +1046,8 @@ describe("OAuth Authorization", () => { // Call auth with authorization code const result = await auth(mockProvider, { - serverUrl: "https://resource.example.com", + serverUrl: "https://api.example.com/mcp-server", authorizationCode: "auth-code-123", - resource: new URL("https://api.example.com/mcp-server"), }); expect(result).toBe("AUTHORIZED"); @@ -1111,8 +1108,7 @@ describe("OAuth Authorization", () => { // Call auth with existing tokens (should trigger refresh) const result = await auth(mockProvider, { - serverUrl: "https://resource.example.com", - resource: new URL("https://api.example.com/mcp-server"), + serverUrl: "https://api.example.com/mcp-server", }); expect(result).toBe("AUTHORIZED"); @@ -1160,8 +1156,7 @@ describe("OAuth Authorization", () => { // Call auth with empty resource parameter const result = await auth(mockProvider, { - serverUrl: "https://resource.example.com", - resource: undefined, + serverUrl: "https://api.example.com/mcp-server", }); expect(result).toBe("REDIRECT"); @@ -1203,8 +1198,7 @@ describe("OAuth Authorization", () => { // Call auth with resource containing multiple # symbols const result = await auth(mockProvider, { - serverUrl: "https://resource.example.com", - resource: new URL("https://api.example.com/mcp-server#fragment#another"), + serverUrl: "https://api.example.com/mcp-server#fragment#another", }); expect(result).toBe("REDIRECT"); @@ -1248,8 +1242,7 @@ describe("OAuth Authorization", () => { // This tests the security fix that prevents token confusion between // multiple MCP servers on the same domain const result1 = await auth(mockProvider, { - serverUrl: "https://api.example.com", - resource: new URL("https://api.example.com/mcp-server-1/v1"), + serverUrl: "https://api.example.com/mcp-server-1/v1", }); expect(result1).toBe("REDIRECT"); @@ -1263,8 +1256,7 @@ describe("OAuth Authorization", () => { // Test with different path on same domain const result2 = await auth(mockProvider, { - serverUrl: "https://api.example.com", - resource: new URL("https://api.example.com/mcp-server-2/v1"), + serverUrl: "https://api.example.com/mcp-server-2/v1", }); expect(result2).toBe("REDIRECT"); @@ -1308,8 +1300,7 @@ describe("OAuth Authorization", () => { // Call auth with resource containing query parameters const result = await auth(mockProvider, { - serverUrl: "https://resource.example.com", - resource: new URL("https://api.example.com/mcp-server?param=value&another=test"), + serverUrl: "https://api.example.com/mcp-server?param=value&another=test", }); expect(result).toBe("REDIRECT"); diff --git a/src/client/auth.ts b/src/client/auth.ts index e465ea3b..681cde99 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -94,19 +94,13 @@ export async function auth( authorizationCode, scope, resourceMetadataUrl, - resource }: { serverUrl: string | URL; authorizationCode?: string; scope?: string; - resourceMetadataUrl?: URL; - resource?: URL }): Promise { + resourceMetadataUrl?: URL }): Promise { - // Remove fragment from resource parameter if provided - let canonicalResource: URL | undefined; - if (resource) { - canonicalResource = resourceUrlFromServerUrl(new URL(resource)); - } + const resource = resourceUrlFromServerUrl(typeof serverUrl === "string" ? new URL(serverUrl) : serverUrl); let authorizationServerUrl = serverUrl; try { @@ -151,7 +145,7 @@ export async function auth( authorizationCode, codeVerifier, redirectUri: provider.redirectUrl, - resource: canonicalResource, + resource, }); await provider.saveTokens(tokens); @@ -168,7 +162,7 @@ export async function auth( metadata, clientInformation, refreshToken: tokens.refresh_token, - resource: canonicalResource, + resource, }); await provider.saveTokens(newTokens); @@ -187,7 +181,7 @@ export async function auth( state, redirectUrl: provider.redirectUrl, scope: scope || provider.clientMetadata.scope, - resource: canonicalResource, + resource, }); await provider.saveCodeVerifier(codeVerifier); diff --git a/src/client/sse.ts b/src/client/sse.ts index 41f21de6..7a500c6b 100644 --- a/src/client/sse.ts +++ b/src/client/sse.ts @@ -90,7 +90,6 @@ export class SSEClientTransport implements Transport { result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl, - resource: resourceUrlFromServerUrl(new URL(this._url)) }); } catch (error) { this.onerror?.(error as Error); @@ -210,7 +209,6 @@ export class SSEClientTransport implements Transport { serverUrl: this._url, authorizationCode, resourceMetadataUrl: this._resourceMetadataUrl, - resource: resourceUrlFromServerUrl(new URL(this._url)) }); if (result !== "AUTHORIZED") { throw new UnauthorizedError("Failed to authorize"); @@ -249,7 +247,6 @@ export class SSEClientTransport implements Transport { const result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl, - resource: resourceUrlFromServerUrl(new URL(this._url)) }); if (result !== "AUTHORIZED") { throw new UnauthorizedError(); diff --git a/src/client/streamableHttp.ts b/src/client/streamableHttp.ts index 3534fb45..85a0ad10 100644 --- a/src/client/streamableHttp.ts +++ b/src/client/streamableHttp.ts @@ -153,7 +153,6 @@ export class StreamableHTTPClientTransport implements Transport { result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl, - resource: resourceUrlFromServerUrl(new URL(this._url)) }); } catch (error) { this.onerror?.(error as Error); @@ -371,7 +370,6 @@ export class StreamableHTTPClientTransport implements Transport { serverUrl: this._url, authorizationCode, resourceMetadataUrl: this._resourceMetadataUrl, - resource: resourceUrlFromServerUrl(new URL(this._url)) }); if (result !== "AUTHORIZED") { throw new UnauthorizedError("Failed to authorize"); @@ -423,7 +421,6 @@ export class StreamableHTTPClientTransport implements Transport { const result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl, - resource: resourceUrlFromServerUrl(new URL(this._url)) }); if (result !== "AUTHORIZED") { throw new UnauthorizedError(); diff --git a/src/examples/server/demoInMemoryOAuthProvider.ts b/src/examples/server/demoInMemoryOAuthProvider.ts index 316d1f8a..d6a64398 100644 --- a/src/examples/server/demoInMemoryOAuthProvider.ts +++ b/src/examples/server/demoInMemoryOAuthProvider.ts @@ -21,6 +21,14 @@ export class DemoInMemoryClientsStore implements OAuthRegisteredClientsStore { } } +/** + * 🚨 DEMO ONLY - NOT FOR PRODUCTION + * + * This example demonstrates MCP OAuth flow but lacks some of the features required for production use, + * for example: + * - Persistent token storage + * - Rate limiting + */ export class DemoInMemoryAuthProvider implements OAuthServerProvider { clientsStore = new DemoInMemoryClientsStore(); private codes = new Map { - throw new Error('Refresh tokens not implemented for example demo'); + throw new Error('Not implemented for example demo'); } async verifyAccessToken(token: string): Promise { diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 65b6263e..da5e740a 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -282,12 +282,7 @@ if (useOAuth) { const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}`); const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`); - // Configure the demo auth provider to validate resources match this server - const demoProviderConfig = { - serverUrl: mcpServerUrl.href, - validateResourceMatchesServer: false // Set to true to enable strict validation - }; - const oauthMetadata: OAuthMetadata = setupAuthServer(authServerUrl, demoProviderConfig); + const oauthMetadata: OAuthMetadata = setupAuthServer(authServerUrl, mcpServerUrl); const tokenVerifier = { verifyAccessToken: async (token: string) => { diff --git a/src/server/auth/handlers/authorize.ts b/src/server/auth/handlers/authorize.ts index 17c88b45..0a6283a8 100644 --- a/src/server/auth/handlers/authorize.ts +++ b/src/server/auth/handlers/authorize.ts @@ -119,9 +119,6 @@ export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: A const { scope, code_challenge, resource } = parseResult.data; state = parseResult.data.state; - // Pass through the resource parameter to the provider - // The provider can decide how to validate it - // Validate scopes let requestedScopes: string[] = []; if (scope !== undefined) { diff --git a/src/server/auth/handlers/token.ts b/src/server/auth/handlers/token.ts index 3ffd4cf2..1d97805b 100644 --- a/src/server/auth/handlers/token.ts +++ b/src/server/auth/handlers/token.ts @@ -93,9 +93,6 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand const { code, code_verifier, redirect_uri, resource } = parseResult.data; - // Pass through the resource parameter to the provider - // The provider can decide how to validate it - const skipLocalPkceValidation = provider.skipLocalPkceValidation; // Perform local PKCE validation unless explicitly skipped @@ -127,9 +124,6 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand const { refresh_token, scope, resource } = parseResult.data; - // Pass through the resource parameter to the provider - // The provider can decide how to validate it - const scopes = scope?.split(" "); const tokens = await provider.exchangeRefreshToken(client, refresh_token, scopes, resource ? new URL(resource) : undefined); res.status(200).json(tokens);