[auth] Implement CIMD conformance test (#37)
* add cimd test and negative test * Improve auth-test-no-cimd.ts documentation for clarity Address review feedback: expanded comments to better explain what CIMD is, why this client is non-compliant, and what behavior it's testing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove .js extensions from relative imports tsx doesn't require .js extensions for TypeScript imports. Keeps .js for @modelcontextprotocol/sdk imports as those are external packages. Also improves auth-test-no-cimd.ts documentation for clarity. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Skip auth/basic-cimd test until SDK support lands Waiting on typescript-sdk PR #1127 for CIMD support. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix trailing whitespace in auth-test-no-cimd.ts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f087e44bdb
commit
d303013e5d
34 changed files with 301 additions and 110 deletions
|
|
@ -7,10 +7,10 @@ import {
|
||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from '@modelcontextprotocol/sdk/client/auth.js';
|
} from '@modelcontextprotocol/sdk/client/auth.js';
|
||||||
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
|
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
|
import { withOAuthRetry } from './helpers/withOAuthRetry';
|
||||||
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
|
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
|
||||||
import { runAsCli } from './helpers/cliRunner.js';
|
import { runAsCli } from './helpers/cliRunner';
|
||||||
import { logger } from './helpers/logger.js';
|
import { logger } from './helpers/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broken client that always uses root-based PRM discovery.
|
* Broken client that always uses root-based PRM discovery.
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import {
|
||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from '@modelcontextprotocol/sdk/client/auth.js';
|
} from '@modelcontextprotocol/sdk/client/auth.js';
|
||||||
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
|
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
|
import { withOAuthRetry } from './helpers/withOAuthRetry';
|
||||||
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
|
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
|
||||||
import { runAsCli } from './helpers/cliRunner.js';
|
import { runAsCli } from './helpers/cliRunner';
|
||||||
import { logger } from './helpers/logger.js';
|
import { logger } from './helpers/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broken client that only responds to 401, not 403.
|
* Broken client that only responds to 401, not 403.
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import {
|
||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from '@modelcontextprotocol/sdk/client/auth.js';
|
} from '@modelcontextprotocol/sdk/client/auth.js';
|
||||||
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
|
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
|
import { withOAuthRetry } from './helpers/withOAuthRetry';
|
||||||
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
|
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
|
||||||
import { runAsCli } from './helpers/cliRunner.js';
|
import { runAsCli } from './helpers/cliRunner';
|
||||||
import { logger } from './helpers/logger.js';
|
import { logger } from './helpers/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broken client that ignores the scope from WWW-Authenticate header.
|
* Broken client that ignores the scope from WWW-Authenticate header.
|
||||||
|
|
|
||||||
51
examples/clients/typescript/auth-test-no-cimd.ts
Normal file
51
examples/clients/typescript/auth-test-no-cimd.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||||
|
import { withOAuthRetry } from './helpers/withOAuthRetry';
|
||||||
|
import { runAsCli } from './helpers/cliRunner';
|
||||||
|
import { logger } from './helpers/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-compliant client that doesn't use CIMD (Client ID Metadata Document).
|
||||||
|
*
|
||||||
|
* This client intentionally omits the clientMetadataUrl parameter when the server
|
||||||
|
* advertises client_id_metadata_document_supported=true. A compliant client should
|
||||||
|
* use CIMD when the server supports it, but this client falls back to DCR (Dynamic
|
||||||
|
* Client Registration) instead.
|
||||||
|
*
|
||||||
|
* Used to test that conformance checks detect clients that don't properly
|
||||||
|
* implement CIMD support.
|
||||||
|
*/
|
||||||
|
export async function runClient(serverUrl: string): Promise<void> {
|
||||||
|
const client = new Client(
|
||||||
|
{ name: 'test-auth-client-no-cimd', version: '1.0.0' },
|
||||||
|
{ capabilities: {} }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Non-compliant: omitting clientMetadataUrl causes fallback to DCR
|
||||||
|
// A compliant client would pass a clientMetadataUrl here when the server
|
||||||
|
// advertises client_id_metadata_document_supported=true
|
||||||
|
const oauthFetch = withOAuthRetry(
|
||||||
|
'test-auth-client-no-cimd',
|
||||||
|
new URL(serverUrl)
|
||||||
|
)(fetch);
|
||||||
|
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
||||||
|
fetch: oauthFetch
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect(transport);
|
||||||
|
logger.debug('Connected to MCP server (without CIMD)');
|
||||||
|
|
||||||
|
await client.listTools();
|
||||||
|
logger.debug('Successfully listed tools');
|
||||||
|
|
||||||
|
await client.callTool({ name: 'test-tool', arguments: {} });
|
||||||
|
logger.debug('Successfully called tool');
|
||||||
|
|
||||||
|
await transport.close();
|
||||||
|
logger.debug('Connection closed successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
runAsCli(runClient, import.meta.url, 'auth-test-no-cimd <server-url>');
|
||||||
|
|
@ -8,10 +8,10 @@ import {
|
||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from '@modelcontextprotocol/sdk/client/auth.js';
|
} from '@modelcontextprotocol/sdk/client/auth.js';
|
||||||
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
|
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
|
import { withOAuthRetry } from './helpers/withOAuthRetry';
|
||||||
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
|
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
|
||||||
import { runAsCli } from './helpers/cliRunner.js';
|
import { runAsCli } from './helpers/cliRunner';
|
||||||
import { logger } from './helpers/logger.js';
|
import { logger } from './helpers/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broken client that only requests a subset of scopes.
|
* Broken client that only requests a subset of scopes.
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,17 @@
|
||||||
|
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||||
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
|
import { withOAuthRetry, handle401 } from './helpers/withOAuthRetry';
|
||||||
import { runAsCli } from './helpers/cliRunner.js';
|
import { runAsCli } from './helpers/cliRunner';
|
||||||
import { logger } from './helpers/logger.js';
|
import { logger } from './helpers/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed client metadata URL for CIMD conformance tests.
|
||||||
|
* When server supports client_id_metadata_document_supported, this URL
|
||||||
|
* will be used as the client_id instead of doing dynamic registration.
|
||||||
|
*/
|
||||||
|
const CIMD_CLIENT_METADATA_URL =
|
||||||
|
'https://conformance-test.local/client-metadata.json';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Well-behaved auth client that follows all OAuth protocols correctly.
|
* Well-behaved auth client that follows all OAuth protocols correctly.
|
||||||
|
|
@ -17,7 +25,9 @@ export async function runClient(serverUrl: string): Promise<void> {
|
||||||
|
|
||||||
const oauthFetch = withOAuthRetry(
|
const oauthFetch = withOAuthRetry(
|
||||||
'test-auth-client',
|
'test-auth-client',
|
||||||
new URL(serverUrl)
|
new URL(serverUrl),
|
||||||
|
handle401,
|
||||||
|
CIMD_CLIENT_METADATA_URL
|
||||||
)(fetch);
|
)(fetch);
|
||||||
|
|
||||||
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,11 @@ export class ConformanceOAuthProvider implements OAuthClientProvider {
|
||||||
return this._clientMetadata;
|
return this._clientMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get clientMetadataUrl(): string | undefined {
|
||||||
|
return this._clientMetadataUrl?.toString();
|
||||||
|
}
|
||||||
|
|
||||||
clientInformation(): OAuthClientInformation | undefined {
|
clientInformation(): OAuthClientInformation | undefined {
|
||||||
if (this._clientMetadataUrl) {
|
|
||||||
console.log('Using client ID metadata URL');
|
|
||||||
return {
|
|
||||||
client_id: this._clientMetadataUrl.toString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return this._clientInformation;
|
return this._clientInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,14 +60,16 @@ export const handle401 = async (
|
||||||
export const withOAuthRetry = (
|
export const withOAuthRetry = (
|
||||||
clientName: string,
|
clientName: string,
|
||||||
baseUrl?: string | URL,
|
baseUrl?: string | URL,
|
||||||
handle401Fn: typeof handle401 = handle401
|
handle401Fn: typeof handle401 = handle401,
|
||||||
|
clientMetadataUrl?: string
|
||||||
): Middleware => {
|
): Middleware => {
|
||||||
const provider = new ConformanceOAuthProvider(
|
const provider = new ConformanceOAuthProvider(
|
||||||
'http://localhost:3000/callback',
|
'http://localhost:3000/callback',
|
||||||
{
|
{
|
||||||
client_name: clientName,
|
client_name: clientName,
|
||||||
redirect_uris: ['http://localhost:3000/callback']
|
redirect_uris: ['http://localhost:3000/callback']
|
||||||
}
|
},
|
||||||
|
clientMetadataUrl
|
||||||
);
|
);
|
||||||
return (next: FetchLike) => {
|
return (next: FetchLike) => {
|
||||||
return async (
|
return async (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ConformanceCheck, CheckStatus } from '../types.js';
|
import { ConformanceCheck, CheckStatus } from '../types';
|
||||||
|
|
||||||
export function createServerInfoCheck(serverInfo: {
|
export function createServerInfoCheck(serverInfo: {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Namespaced exports for client checks
|
// Namespaced exports for client checks
|
||||||
import * as client from './client.js';
|
import * as client from './client';
|
||||||
|
|
||||||
export const clientChecks = client;
|
export const clientChecks = client;
|
||||||
|
|
|
||||||
99
src/scenarios/client/auth/basic-cimd.ts
Normal file
99
src/scenarios/client/auth/basic-cimd.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import type { Scenario, ConformanceCheck } from '../../../types';
|
||||||
|
import { ScenarioUrls } from '../../../types';
|
||||||
|
import { createAuthServer } from './helpers/createAuthServer';
|
||||||
|
import { createServer } from './helpers/createServer';
|
||||||
|
import { ServerLifecycle } from './helpers/serverLifecycle';
|
||||||
|
import { SpecReferences } from './spec-references';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed client metadata URL that clients should use for CIMD tests.
|
||||||
|
* This URL doesn't need to resolve - the server will accept it as-is
|
||||||
|
* and use hardcoded metadata.
|
||||||
|
*/
|
||||||
|
export const CIMD_CLIENT_METADATA_URL =
|
||||||
|
'https://conformance-test.local/client-metadata.json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scenario: Client ID Metadata Documents (SEP-991/URL-based client IDs)
|
||||||
|
*
|
||||||
|
* Tests that when a server advertises client_id_metadata_document_supported=true,
|
||||||
|
* clients SHOULD use a URL as their client_id instead of using dynamic client
|
||||||
|
* registration.
|
||||||
|
*/
|
||||||
|
export class AuthBasicCIMDScenario implements Scenario {
|
||||||
|
name = 'auth/basic-cimd';
|
||||||
|
description =
|
||||||
|
'Tests OAuth flow with Client ID Metadata Documents (SEP-991/URL-based client IDs). Server advertises client_id_metadata_document_supported=true and client should use URL as client_id instead of DCR.';
|
||||||
|
private authServer = new ServerLifecycle();
|
||||||
|
private server = new ServerLifecycle();
|
||||||
|
private checks: ConformanceCheck[] = [];
|
||||||
|
|
||||||
|
async start(): Promise<ScenarioUrls> {
|
||||||
|
this.checks = [];
|
||||||
|
|
||||||
|
const authApp = createAuthServer(this.checks, this.authServer.getUrl, {
|
||||||
|
clientIdMetadataDocumentSupported: true,
|
||||||
|
onAuthorizationRequest: (data) => {
|
||||||
|
// Check if client used URL-based client ID
|
||||||
|
const usedUrlClientId = data.clientId === CIMD_CLIENT_METADATA_URL;
|
||||||
|
this.checks.push({
|
||||||
|
id: 'cimd-client-id-used',
|
||||||
|
name: 'Client ID Metadata Document Usage',
|
||||||
|
description: usedUrlClientId
|
||||||
|
? 'Client correctly used URL-based client ID when server supports client_id_metadata_document_supported'
|
||||||
|
: 'Client SHOULD use URL-based client ID when server advertises client_id_metadata_document_supported=true',
|
||||||
|
status: usedUrlClientId ? 'SUCCESS' : 'WARNING',
|
||||||
|
timestamp: data.timestamp,
|
||||||
|
specReferences: [
|
||||||
|
SpecReferences.MCP_CLIENT_ID_METADATA_DOCUMENTS,
|
||||||
|
SpecReferences.IETF_CIMD
|
||||||
|
],
|
||||||
|
details: {
|
||||||
|
expectedClientId: CIMD_CLIENT_METADATA_URL,
|
||||||
|
actualClientId: data.clientId || 'none'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.authServer.start(authApp);
|
||||||
|
|
||||||
|
const app = createServer(
|
||||||
|
this.checks,
|
||||||
|
this.server.getUrl,
|
||||||
|
this.authServer.getUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.server.start(app);
|
||||||
|
|
||||||
|
return { serverUrl: `${this.server.getUrl()}/mcp` };
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
await this.authServer.stop();
|
||||||
|
await this.server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
getChecks(): ConformanceCheck[] {
|
||||||
|
// Ensure we have the CIMD check - if not, the client didn't make an auth request
|
||||||
|
const hasCimdCheck = this.checks.some(
|
||||||
|
(c) => c.id === 'cimd-client-id-used'
|
||||||
|
);
|
||||||
|
if (!hasCimdCheck) {
|
||||||
|
this.checks.push({
|
||||||
|
id: 'cimd-client-id-used',
|
||||||
|
name: 'Client ID Metadata Document Usage',
|
||||||
|
description:
|
||||||
|
'Client did not make an authorization request to test CIMD support',
|
||||||
|
status: 'FAILURE',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
specReferences: [
|
||||||
|
SpecReferences.MCP_CLIENT_ID_METADATA_DOCUMENTS,
|
||||||
|
SpecReferences.IETF_CIMD
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.checks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,12 @@
|
||||||
* generated from them.
|
* generated from them.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Scenario, ConformanceCheck } from '../../../types.js';
|
import type { Scenario, ConformanceCheck } from '../../../types';
|
||||||
import { ScenarioUrls } from '../../../types.js';
|
import { ScenarioUrls } from '../../../types';
|
||||||
import { createAuthServer } from './helpers/createAuthServer.js';
|
import { createAuthServer } from './helpers/createAuthServer';
|
||||||
import { createServer } from './helpers/createServer.js';
|
import { createServer } from './helpers/createServer';
|
||||||
import { ServerLifecycle } from './helpers/serverLifecycle.js';
|
import { ServerLifecycle } from './helpers/serverLifecycle';
|
||||||
import { SpecReferences } from './spec-references.js';
|
import { SpecReferences } from './spec-references';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import type { ConformanceCheck } from '../../../../types.js';
|
import type { ConformanceCheck } from '../../../../types';
|
||||||
import { createRequestLogger } from '../../../request-logger.js';
|
import { createRequestLogger } from '../../../request-logger';
|
||||||
import { SpecReferences } from '../spec-references.js';
|
import { SpecReferences } from '../spec-references';
|
||||||
import { MockTokenVerifier } from './mockTokenVerifier.js';
|
import { MockTokenVerifier } from './mockTokenVerifier';
|
||||||
|
|
||||||
export interface AuthServerOptions {
|
export interface AuthServerOptions {
|
||||||
metadataPath?: string;
|
metadataPath?: string;
|
||||||
|
|
@ -10,6 +10,7 @@ export interface AuthServerOptions {
|
||||||
loggingEnabled?: boolean;
|
loggingEnabled?: boolean;
|
||||||
routePrefix?: string;
|
routePrefix?: string;
|
||||||
scopesSupported?: string[];
|
scopesSupported?: string[];
|
||||||
|
clientIdMetadataDocumentSupported?: boolean;
|
||||||
tokenVerifier?: MockTokenVerifier;
|
tokenVerifier?: MockTokenVerifier;
|
||||||
onTokenRequest?: (requestData: {
|
onTokenRequest?: (requestData: {
|
||||||
scope?: string;
|
scope?: string;
|
||||||
|
|
@ -17,6 +18,7 @@ export interface AuthServerOptions {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}) => { token: string; scopes: string[] };
|
}) => { token: string; scopes: string[] };
|
||||||
onAuthorizationRequest?: (requestData: {
|
onAuthorizationRequest?: (requestData: {
|
||||||
|
clientId?: string;
|
||||||
scope?: string;
|
scope?: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
@ -33,6 +35,7 @@ export function createAuthServer(
|
||||||
loggingEnabled = true,
|
loggingEnabled = true,
|
||||||
routePrefix = '',
|
routePrefix = '',
|
||||||
scopesSupported,
|
scopesSupported,
|
||||||
|
clientIdMetadataDocumentSupported,
|
||||||
tokenVerifier,
|
tokenVerifier,
|
||||||
onTokenRequest,
|
onTokenRequest,
|
||||||
onAuthorizationRequest
|
onAuthorizationRequest
|
||||||
|
|
@ -93,6 +96,12 @@ export function createAuthServer(
|
||||||
metadata.scopes_supported = scopesSupported;
|
metadata.scopes_supported = scopesSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add client_id_metadata_document_supported if provided
|
||||||
|
if (clientIdMetadataDocumentSupported !== undefined) {
|
||||||
|
metadata.client_id_metadata_document_supported =
|
||||||
|
clientIdMetadataDocumentSupported;
|
||||||
|
}
|
||||||
|
|
||||||
// Add OpenID Configuration specific fields
|
// Add OpenID Configuration specific fields
|
||||||
if (isOpenIdConfiguration) {
|
if (isOpenIdConfiguration) {
|
||||||
metadata.jwks_uri = `${getAuthBaseUrl()}/.well-known/jwks.json`;
|
metadata.jwks_uri = `${getAuthBaseUrl()}/.well-known/jwks.json`;
|
||||||
|
|
@ -123,6 +132,7 @@ export function createAuthServer(
|
||||||
|
|
||||||
if (onAuthorizationRequest) {
|
if (onAuthorizationRequest) {
|
||||||
onAuthorizationRequest({
|
onAuthorizationRequest({
|
||||||
|
clientId: req.query.client_id as string | undefined,
|
||||||
scope: scopeParam,
|
scope: scopeParam,
|
||||||
timestamp
|
timestamp
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@ import {
|
||||||
} from '@modelcontextprotocol/sdk/types.js';
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
|
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
|
||||||
import express, { Request, Response, NextFunction } from 'express';
|
import express, { Request, Response, NextFunction } from 'express';
|
||||||
import type { ConformanceCheck } from '../../../../types.js';
|
import type { ConformanceCheck } from '../../../../types';
|
||||||
import { createRequestLogger } from '../../../request-logger.js';
|
import { createRequestLogger } from '../../../request-logger';
|
||||||
import { MockTokenVerifier } from './mockTokenVerifier.js';
|
import { MockTokenVerifier } from './mockTokenVerifier';
|
||||||
import { SpecReferences } from '../spec-references.js';
|
import { SpecReferences } from '../spec-references';
|
||||||
|
|
||||||
export interface ServerOptions {
|
export interface ServerOptions {
|
||||||
prmPath?: string | null;
|
prmPath?: string | null;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js';
|
import { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js';
|
||||||
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
||||||
import type { ConformanceCheck } from '../../../../types.js';
|
import type { ConformanceCheck } from '../../../../types';
|
||||||
import { SpecReferences } from '../spec-references.js';
|
import { SpecReferences } from '../spec-references';
|
||||||
|
|
||||||
export class MockTokenVerifier implements OAuthTokenVerifier {
|
export class MockTokenVerifier implements OAuthTokenVerifier {
|
||||||
private tokenScopes: Map<string, string[]> = new Map();
|
private tokenScopes: Map<string, string[]> = new Map();
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { authScenariosList } from './index.js';
|
import { authScenariosList } from './index';
|
||||||
import {
|
import {
|
||||||
runClientAgainstScenario,
|
runClientAgainstScenario,
|
||||||
InlineClientRunner
|
InlineClientRunner
|
||||||
} from './test_helpers/testClient.js';
|
} from './test_helpers/testClient';
|
||||||
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test.js';
|
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test';
|
||||||
import { runClient as badPrmClient } from '../../../../examples/clients/typescript/auth-test-bad-prm.js';
|
import { runClient as badPrmClient } from '../../../../examples/clients/typescript/auth-test-bad-prm';
|
||||||
import { runClient as ignoreScopeClient } from '../../../../examples/clients/typescript/auth-test-ignore-scope.js';
|
import { runClient as noCimdClient } from '../../../../examples/clients/typescript/auth-test-no-cimd';
|
||||||
import { runClient as partialScopesClient } from '../../../../examples/clients/typescript/auth-test-partial-scopes.js';
|
import { runClient as ignoreScopeClient } from '../../../../examples/clients/typescript/auth-test-ignore-scope';
|
||||||
import { runClient as ignore403Client } from '../../../../examples/clients/typescript/auth-test-ignore-403.js';
|
import { runClient as partialScopesClient } from '../../../../examples/clients/typescript/auth-test-partial-scopes';
|
||||||
import { setLogLevel } from '../../../../examples/clients/typescript/helpers/logger.js';
|
import { runClient as ignore403Client } from '../../../../examples/clients/typescript/auth-test-ignore-403';
|
||||||
|
import { setLogLevel } from '../../../../examples/clients/typescript/helpers/logger';
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
setLogLevel('error');
|
setLogLevel('error');
|
||||||
|
|
@ -22,7 +23,10 @@ const skipScenarios = new Set<string>([
|
||||||
// Waiting on typescript-sdk support for using scopes_supported from PRM
|
// Waiting on typescript-sdk support for using scopes_supported from PRM
|
||||||
// to request scopes.
|
// to request scopes.
|
||||||
// https://github.com/modelcontextprotocol/typescript-sdk/pull/1133
|
// https://github.com/modelcontextprotocol/typescript-sdk/pull/1133
|
||||||
'auth/scope-from-scopes-supported'
|
'auth/scope-from-scopes-supported',
|
||||||
|
// Waiting on typescript-sdk support for CIMD
|
||||||
|
// https://github.com/modelcontextprotocol/typescript-sdk/pull/1127
|
||||||
|
'auth/basic-cimd'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
describe('Client Auth Scenarios', () => {
|
describe('Client Auth Scenarios', () => {
|
||||||
|
|
@ -76,4 +80,11 @@ describe('Negative tests', () => {
|
||||||
'scope-step-up-escalation'
|
'scope-step-up-escalation'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('client uses DCR instead of CIMD when server supports it', async () => {
|
||||||
|
const runner = new InlineClientRunner(noCimdClient);
|
||||||
|
await runClientAgainstScenario(runner, 'auth/basic-cimd', [
|
||||||
|
'cimd-client-id-used'
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
import { Scenario } from '../../../types';
|
import { Scenario } from '../../../types';
|
||||||
import { metadataScenarios } from './discovery-metadata.js';
|
import { metadataScenarios } from './discovery-metadata';
|
||||||
|
import { AuthBasicCIMDScenario } from './basic-cimd';
|
||||||
import {
|
import {
|
||||||
Auth20250326OAuthMetadataBackcompatScenario,
|
Auth20250326OAuthMetadataBackcompatScenario,
|
||||||
Auth20250326OEndpointFallbackScenario
|
Auth20250326OEndpointFallbackScenario
|
||||||
} from './march-spec-backcompat.js';
|
} from './march-spec-backcompat';
|
||||||
import {
|
import {
|
||||||
ScopeFromWwwAuthenticateScenario,
|
ScopeFromWwwAuthenticateScenario,
|
||||||
ScopeFromScopesSupportedScenario,
|
ScopeFromScopesSupportedScenario,
|
||||||
ScopeOmittedWhenUndefinedScenario,
|
ScopeOmittedWhenUndefinedScenario,
|
||||||
ScopeStepUpAuthScenario
|
ScopeStepUpAuthScenario
|
||||||
} from './scope-handling.js';
|
} from './scope-handling';
|
||||||
|
|
||||||
export const authScenariosList: Scenario[] = [
|
export const authScenariosList: Scenario[] = [
|
||||||
...metadataScenarios,
|
...metadataScenarios,
|
||||||
|
new AuthBasicCIMDScenario(),
|
||||||
new Auth20250326OAuthMetadataBackcompatScenario(),
|
new Auth20250326OAuthMetadataBackcompatScenario(),
|
||||||
new Auth20250326OEndpointFallbackScenario(),
|
new Auth20250326OEndpointFallbackScenario(),
|
||||||
new ScopeFromWwwAuthenticateScenario(),
|
new ScopeFromWwwAuthenticateScenario(),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import type { Scenario, ConformanceCheck } from '../../../types.js';
|
import type { Scenario, ConformanceCheck } from '../../../types';
|
||||||
import { ScenarioUrls } from '../../../types.js';
|
import { ScenarioUrls } from '../../../types';
|
||||||
import { createAuthServer } from './helpers/createAuthServer.js';
|
import { createAuthServer } from './helpers/createAuthServer';
|
||||||
import { createServer } from './helpers/createServer.js';
|
import { createServer } from './helpers/createServer';
|
||||||
import { ServerLifecycle } from './helpers/serverLifecycle.js';
|
import { ServerLifecycle } from './helpers/serverLifecycle';
|
||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import { SpecReferences } from './spec-references.js';
|
import { SpecReferences } from './spec-references';
|
||||||
|
|
||||||
export class Auth20250326OAuthMetadataBackcompatScenario implements Scenario {
|
export class Auth20250326OAuthMetadataBackcompatScenario implements Scenario {
|
||||||
name = 'auth/2025-03-26-oauth-metadata-backcompat';
|
name = 'auth/2025-03-26-oauth-metadata-backcompat';
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import type { Scenario, ConformanceCheck } from '../../../types.js';
|
import type { Scenario, ConformanceCheck } from '../../../types';
|
||||||
import { ScenarioUrls } from '../../../types.js';
|
import { ScenarioUrls } from '../../../types';
|
||||||
import { createAuthServer } from './helpers/createAuthServer.js';
|
import { createAuthServer } from './helpers/createAuthServer';
|
||||||
import { createServer } from './helpers/createServer.js';
|
import { createServer } from './helpers/createServer';
|
||||||
import { ServerLifecycle } from './helpers/serverLifecycle.js';
|
import { ServerLifecycle } from './helpers/serverLifecycle';
|
||||||
import { SpecReferences } from './spec-references.js';
|
import { SpecReferences } from './spec-references';
|
||||||
import { MockTokenVerifier } from './helpers/mockTokenVerifier.js';
|
import { MockTokenVerifier } from './helpers/mockTokenVerifier';
|
||||||
import type { Request, Response, NextFunction } from 'express';
|
import type { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -52,5 +52,13 @@ export const SpecReferences: { [key: string]: SpecReference } = {
|
||||||
MCP_AUTH_ERROR_HANDLING: {
|
MCP_AUTH_ERROR_HANDLING: {
|
||||||
id: 'MCP-Auth-error-handling',
|
id: 'MCP-Auth-error-handling',
|
||||||
url: 'https://modelcontextprotocol.io/specification/draft/basic/authorization#error-handling'
|
url: 'https://modelcontextprotocol.io/specification/draft/basic/authorization#error-handling'
|
||||||
|
},
|
||||||
|
MCP_CLIENT_ID_METADATA_DOCUMENTS: {
|
||||||
|
id: 'MCP-Client-ID-Metadata-Documents',
|
||||||
|
url: 'https://modelcontextprotocol.io/specification/draft/basic/authorization#client-id-metadata-documents'
|
||||||
|
},
|
||||||
|
IETF_CIMD: {
|
||||||
|
id: 'IETF-OAuth-Client-ID-Metadata-Document',
|
||||||
|
url: 'https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue