simplify diff

This commit is contained in:
Olivier Chafik 2025-06-16 23:10:27 +01:00
commit 02ce81b90c
No known key found for this signature in database
5 changed files with 20 additions and 105 deletions

View file

@ -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<string, {
params: AuthorizationParams,
client: OAuthClientInformationFull}>();
private tokens = new Map<string, AuthInfo>();
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<void> {
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<void> {
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

View file

@ -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);
}
}

View file

@ -8,7 +8,7 @@ export type AuthorizationParams = {
scopes?: string[];
codeChallenge: string;
redirectUri: string;
resource?: string;
resource?: URL;
};
/**

View file

@ -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.

View file

@ -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);
}