diff --git a/src/examples/server/demoInMemoryOAuthProvider.ts b/src/examples/server/demoInMemoryOAuthProvider.ts index 3672c3e0..5de0fb90 100644 --- a/src/examples/server/demoInMemoryOAuthProvider.ts +++ b/src/examples/server/demoInMemoryOAuthProvider.ts @@ -5,7 +5,6 @@ import { OAuthClientInformationFull, OAuthMetadata, OAuthTokens } from '../../sh import express, { Request, Response } from "express"; import { AuthInfo } from '../../server/auth/types.js'; import { createOAuthMetadata, mcpAuthRouter } from '../../server/auth/router.js'; -import { InvalidTargetError, InvalidRequestError } from '../../server/auth/errors.js'; import { resourceUrlFromServerUrl } from '../../shared/auth-utils.js'; @@ -22,52 +21,21 @@ export class DemoInMemoryClientsStore implements OAuthRegisteredClientsStore { } } -/** - * Configuration options for the demo OAuth provider - */ -export interface DemoOAuthProviderConfig { - /** - * The canonical URL of this MCP server. When provided, the provider will validate - * that the resource parameter in OAuth requests matches this URL. - * - * This should be the full URL that clients use to connect to this server, - * without any fragment component (e.g., "https://api.example.com/mcp"). - * - * Required when validateResourceMatchesServer is true. - */ - serverUrl?: string; - - /** - * If true, validates that the resource parameter matches the configured serverUrl. - * - * When enabled: - * - serverUrl must be configured (throws error if not) - * - resource parameter is required on all requests - * - resource must exactly match serverUrl (after fragment removal) - * - requests without resource parameter will be rejected with invalid_request error - * - requests with non-matching resource will be rejected with invalid_target error - * - * When disabled: - * - warnings are logged when resource parameter is missing (for migration tracking) - * - * @default false - */ - validateResourceMatchesServer?: boolean; -} - export class DemoInMemoryAuthProvider implements OAuthServerProvider { clientsStore = new DemoInMemoryClientsStore(); private codes = new Map(); private tokens = new Map(); - private config?: DemoOAuthProviderConfig; + private validateResource?: (resource?: URL) => boolean; - constructor(config?: DemoOAuthProviderConfig) { - if (config?.validateResourceMatchesServer && !config?.serverUrl) { - throw new Error("serverUrl must be configured when validateResourceMatchesServer is true"); + constructor(mcpServerUrl?: URL) { + if (mcpServerUrl) { + const expectedResource = resourceUrlFromServerUrl(mcpServerUrl); + this.validateResource = (resource?: URL) => { + return !resource || resource.toString() !== expectedResource.toString(); + }; } - this.config = config; } async authorize( @@ -75,8 +43,6 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider { params: AuthorizationParams, res: Response ): Promise { - await this.validateResource(params.resource); - const code = randomUUID(); const searchParams = new URLSearchParams({ @@ -126,7 +92,9 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider { throw new Error(`Authorization code was not issued to this client, ${codeData.client.client_id} != ${client.client_id}`); } - await this.validateResource(codeData.params.resource); + if (this.validateResource && !this.validateResource(codeData.params.resource)) { + throw new Error('Invalid resource'); + } this.codes.delete(authorizationCode); const token = randomUUID(); @@ -164,7 +132,6 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider { if (!tokenData || !tokenData.expiresAt || tokenData.expiresAt < Date.now()) { throw new Error('Invalid or expired token'); } - await this.validateResource(tokenData.resource); return { token, @@ -175,29 +142,6 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider { }; } - /** - * Validates that the client is allowed to access the requested resource. - * In a real implementation, this would check against a database or configuration. - */ - private async validateResource(resource?: string): Promise { - if (this.config?.validateResourceMatchesServer) { - if (!resource) { - throw new InvalidRequestError("Resource parameter is required when server URL validation is enabled"); - } - - const canonicalServerUrl = resourceUrlFromServerUrl(this.config.serverUrl!); - - if (resource !== canonicalServerUrl) { - throw new InvalidTargetError( - `Resource parameter '${resource}' does not match this server's URL '${canonicalServerUrl}'` - ); - } - } else if (!resource) { - // Always log warning if resource is missing (unless validation is enabled) - console.warn(`Token refresh request is missing the resource parameter. Consider migrating to RFC 8707.`); - } - } - /** * Get token details including resource information (for demo introspection endpoint) */ @@ -207,13 +151,13 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider { } -export const setupAuthServer = (authServerUrl: URL, config?: DemoOAuthProviderConfig): OAuthMetadata => { +export const setupAuthServer = (authServerUrl: URL, mcpServerUrl: URL): OAuthMetadata => { // Create separate auth server app // NOTE: This is a separate app on a separate port to illustrate // how to separate an OAuth Authorization Server from a Resource // server in the SDK. The SDK is not intended to be provide a standalone // authorization server. - const provider = new DemoInMemoryAuthProvider(config); + const provider = new DemoInMemoryAuthProvider(mcpServerUrl); const authApp = express(); authApp.use(express.json()); // For introspection requests diff --git a/src/server/auth/errors.ts b/src/server/auth/errors.ts index 5c001bcd..428199ce 100644 --- a/src/server/auth/errors.ts +++ b/src/server/auth/errors.ts @@ -189,13 +189,3 @@ export class InsufficientScopeError extends OAuthError { super("insufficient_scope", message, errorUri); } } - -/** - * Invalid target error - The requested resource is invalid, unknown, or malformed. - * (RFC 8707 - Resource Indicators for OAuth 2.0) - */ -export class InvalidTargetError extends OAuthError { - constructor(message: string, errorUri?: string) { - super("invalid_target", message, errorUri); - } -} diff --git a/src/server/auth/provider.ts b/src/server/auth/provider.ts index 25698416..93a56a09 100644 --- a/src/server/auth/provider.ts +++ b/src/server/auth/provider.ts @@ -8,7 +8,7 @@ export type AuthorizationParams = { scopes?: string[]; codeChallenge: string; redirectUri: string; - resource?: string; + resource?: URL; }; /** diff --git a/src/server/auth/types.ts b/src/server/auth/types.ts index bf1a257b..0189e9ed 100644 --- a/src/server/auth/types.ts +++ b/src/server/auth/types.ts @@ -26,7 +26,7 @@ export interface AuthInfo { * The RFC 8707 resource server identifier for which this token is valid. * If set, this MUST match the MCP server's resource identifier (minus hash fragment). */ - resource?: string; + resource?: URL; /** * Additional data associated with the token. diff --git a/src/shared/auth-utils.ts b/src/shared/auth-utils.ts index e69d821d..086d812f 100644 --- a/src/shared/auth-utils.ts +++ b/src/shared/auth-utils.ts @@ -1,5 +1,5 @@ /** - * Utilities for handling OAuth resource URIs according to RFC 8707. + * Utilities for handling OAuth resource URIs. */ /** @@ -7,27 +7,8 @@ * RFC 8707 section 2 states that resource URIs "MUST NOT include a fragment component". * Keeps everything else unchanged (scheme, domain, port, path, query). */ -export function resourceUrlFromServerUrl(url: string): string { - const hashIndex = url.indexOf('#'); - return hashIndex === -1 ? url : url.substring(0, hashIndex); +export function resourceUrlFromServerUrl(url: URL): URL { + const resourceURL = new URL(url.href); + resourceURL.hash = ''; // Remove fragment + return resourceURL; } - -/** - * Validates a resource URI according to RFC 8707 requirements. - * @param resourceUri The resource URI to validate - * @throws Error if the URI contains a fragment - */ -export function validateResourceUri(resourceUri: string): void { - if (resourceUri.includes('#')) { - throw new Error(`Invalid resource URI: ${resourceUri} - must not contain a fragment`); - } -} - -/** - * Extracts resource URI from server URL by removing fragment. - * @param serverUrl The server URL to extract from - * @returns The resource URI without fragment - */ -export function extractResourceUri(serverUrl: string | URL): string { - return resourceUrlFromServerUrl(typeof serverUrl === 'string' ? serverUrl : serverUrl.href); -} \ No newline at end of file