[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:
Paul Carleton 2025-11-20 21:00:13 +00:00 • committed by GitHub
commit d303013e5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 301 additions and 110 deletions

View 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;
}
}

View file

@ -6,12 +6,12 @@
* generated from them.
*/
import type { Scenario, ConformanceCheck } from '../../../types.js';
import { ScenarioUrls } from '../../../types.js';
import { createAuthServer } from './helpers/createAuthServer.js';
import { createServer } from './helpers/createServer.js';
import { ServerLifecycle } from './helpers/serverLifecycle.js';
import { SpecReferences } from './spec-references.js';
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';
import { Request, Response } from 'express';
/**

View file

@ -1,8 +1,8 @@
import express, { Request, Response } from 'express';
import type { ConformanceCheck } from '../../../../types.js';
import { createRequestLogger } from '../../../request-logger.js';
import { SpecReferences } from '../spec-references.js';
import { MockTokenVerifier } from './mockTokenVerifier.js';
import type { ConformanceCheck } from '../../../../types';
import { createRequestLogger } from '../../../request-logger';
import { SpecReferences } from '../spec-references';
import { MockTokenVerifier } from './mockTokenVerifier';
export interface AuthServerOptions {
metadataPath?: string;
@ -10,6 +10,7 @@ export interface AuthServerOptions {
loggingEnabled?: boolean;
routePrefix?: string;
scopesSupported?: string[];
clientIdMetadataDocumentSupported?: boolean;
tokenVerifier?: MockTokenVerifier;
onTokenRequest?: (requestData: {
scope?: string;
@ -17,6 +18,7 @@ export interface AuthServerOptions {
timestamp: string;
}) => { token: string; scopes: string[] };
onAuthorizationRequest?: (requestData: {
clientId?: string;
scope?: string;
timestamp: string;
}) => void;
@ -33,6 +35,7 @@ export function createAuthServer(
loggingEnabled = true,
routePrefix = '',
scopesSupported,
clientIdMetadataDocumentSupported,
tokenVerifier,
onTokenRequest,
onAuthorizationRequest
@ -93,6 +96,12 @@ export function createAuthServer(
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
if (isOpenIdConfiguration) {
metadata.jwks_uri = `${getAuthBaseUrl()}/.well-known/jwks.json`;
@ -123,6 +132,7 @@ export function createAuthServer(
if (onAuthorizationRequest) {
onAuthorizationRequest({
clientId: req.query.client_id as string | undefined,
scope: scopeParam,
timestamp
});

View file

@ -9,10 +9,10 @@ import {
} from '@modelcontextprotocol/sdk/types.js';
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
import express, { Request, Response, NextFunction } from 'express';
import type { ConformanceCheck } from '../../../../types.js';
import { createRequestLogger } from '../../../request-logger.js';
import { MockTokenVerifier } from './mockTokenVerifier.js';
import { SpecReferences } from '../spec-references.js';
import type { ConformanceCheck } from '../../../../types';
import { createRequestLogger } from '../../../request-logger';
import { MockTokenVerifier } from './mockTokenVerifier';
import { SpecReferences } from '../spec-references';
export interface ServerOptions {
prmPath?: string | null;

View file

@ -1,7 +1,7 @@
import { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js';
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
import type { ConformanceCheck } from '../../../../types.js';
import { SpecReferences } from '../spec-references.js';
import type { ConformanceCheck } from '../../../../types';
import { SpecReferences } from '../spec-references';
export class MockTokenVerifier implements OAuthTokenVerifier {
private tokenScopes: Map<string, string[]> = new Map();

View file

@ -1,14 +1,15 @@
import { authScenariosList } from './index.js';
import { authScenariosList } from './index';
import {
runClientAgainstScenario,
InlineClientRunner
} from './test_helpers/testClient.js';
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test.js';
import { runClient as badPrmClient } from '../../../../examples/clients/typescript/auth-test-bad-prm.js';
import { runClient as ignoreScopeClient } from '../../../../examples/clients/typescript/auth-test-ignore-scope.js';
import { runClient as partialScopesClient } from '../../../../examples/clients/typescript/auth-test-partial-scopes.js';
import { runClient as ignore403Client } from '../../../../examples/clients/typescript/auth-test-ignore-403.js';
import { setLogLevel } from '../../../../examples/clients/typescript/helpers/logger.js';
} from './test_helpers/testClient';
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test';
import { runClient as badPrmClient } from '../../../../examples/clients/typescript/auth-test-bad-prm';
import { runClient as noCimdClient } from '../../../../examples/clients/typescript/auth-test-no-cimd';
import { runClient as ignoreScopeClient } from '../../../../examples/clients/typescript/auth-test-ignore-scope';
import { runClient as partialScopesClient } from '../../../../examples/clients/typescript/auth-test-partial-scopes';
import { runClient as ignore403Client } from '../../../../examples/clients/typescript/auth-test-ignore-403';
import { setLogLevel } from '../../../../examples/clients/typescript/helpers/logger';
beforeAll(() => {
setLogLevel('error');
@ -22,7 +23,10 @@ const skipScenarios = new Set<string>([
// Waiting on typescript-sdk support for using scopes_supported from PRM
// to request scopes.
// 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', () => {
@ -76,4 +80,11 @@ describe('Negative tests', () => {
'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'
]);
});
});

View file

@ -1,18 +1,20 @@
import { Scenario } from '../../../types';
import { metadataScenarios } from './discovery-metadata.js';
import { metadataScenarios } from './discovery-metadata';
import { AuthBasicCIMDScenario } from './basic-cimd';
import {
Auth20250326OAuthMetadataBackcompatScenario,
Auth20250326OEndpointFallbackScenario
} from './march-spec-backcompat.js';
} from './march-spec-backcompat';
import {
ScopeFromWwwAuthenticateScenario,
ScopeFromScopesSupportedScenario,
ScopeOmittedWhenUndefinedScenario,
ScopeStepUpAuthScenario
} from './scope-handling.js';
} from './scope-handling';
export const authScenariosList: Scenario[] = [
...metadataScenarios,
new AuthBasicCIMDScenario(),
new Auth20250326OAuthMetadataBackcompatScenario(),
new Auth20250326OEndpointFallbackScenario(),
new ScopeFromWwwAuthenticateScenario(),

View file

@ -1,10 +1,10 @@
import type { Scenario, ConformanceCheck } from '../../../types.js';
import { ScenarioUrls } from '../../../types.js';
import { createAuthServer } from './helpers/createAuthServer.js';
import { createServer } from './helpers/createServer.js';
import { ServerLifecycle } from './helpers/serverLifecycle.js';
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 express, { Request, Response } from 'express';
import { SpecReferences } from './spec-references.js';
import { SpecReferences } from './spec-references';
export class Auth20250326OAuthMetadataBackcompatScenario implements Scenario {
name = 'auth/2025-03-26-oauth-metadata-backcompat';

View file

@ -1,10 +1,10 @@
import type { Scenario, ConformanceCheck } from '../../../types.js';
import { ScenarioUrls } from '../../../types.js';
import { createAuthServer } from './helpers/createAuthServer.js';
import { createServer } from './helpers/createServer.js';
import { ServerLifecycle } from './helpers/serverLifecycle.js';
import { SpecReferences } from './spec-references.js';
import { MockTokenVerifier } from './helpers/mockTokenVerifier.js';
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';
import { MockTokenVerifier } from './helpers/mockTokenVerifier';
import type { Request, Response, NextFunction } from 'express';
/**

View file

@ -52,5 +52,13 @@ export const SpecReferences: { [key: string]: SpecReference } = {
MCP_AUTH_ERROR_HANDLING: {
id: 'MCP-Auth-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'
}
};

View file

@ -1,4 +1,4 @@
import { getScenario } from '../../../index.js';
import { getScenario } from '../../../index';
import { spawn } from 'child_process';
const CLIENT_TIMEOUT = 10000; // 10 seconds for client to complete

View file

@ -11,10 +11,10 @@ import {
ListToolsRequestSchema,
ElicitResultSchema
} from '@modelcontextprotocol/sdk/types.js';
import type { Scenario, ConformanceCheck } from '../../types.js';
import type { Scenario, ConformanceCheck } from '../../types';
import express, { Request, Response } from 'express';
import { ScenarioUrls } from '../../types.js';
import { createRequestLogger } from '../request-logger.js';
import { ScenarioUrls } from '../../types';
import { createRequestLogger } from '../request-logger';
import { randomUUID } from 'crypto';
function isInitializeRequest(body: unknown): boolean {

View file

@ -1,6 +1,6 @@
import http from 'http';
import { Scenario, ScenarioUrls, ConformanceCheck } from '../../types.js';
import { clientChecks } from '../../checks/index.js';
import { Scenario, ScenarioUrls, ConformanceCheck } from '../../types';
import { clientChecks } from '../../checks/index';
export class InitializeScenario implements Scenario {
name = 'initialize';

View file

@ -4,10 +4,10 @@ import {
CallToolRequestSchema,
ListToolsRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import type { Scenario, ConformanceCheck } from '../../types.js';
import type { Scenario, ConformanceCheck } from '../../types';
import express, { Request, Response } from 'express';
import { ScenarioUrls } from '../../types.js';
import { createRequestLogger } from '../request-logger.js';
import { ScenarioUrls } from '../../types';
import { createRequestLogger } from '../request-logger';
function createServer(checks: ConformanceCheck[]): express.Application {
const server = new Server(

View file

@ -1,15 +1,15 @@
import { Scenario, ClientScenario } from '../types.js';
import { InitializeScenario } from './client/initialize.js';
import { ToolsCallScenario } from './client/tools_call.js';
import { ElicitationClientDefaultsScenario } from './client/elicitation-defaults.js';
import { Scenario, ClientScenario } from '../types';
import { InitializeScenario } from './client/initialize';
import { ToolsCallScenario } from './client/tools_call';
import { ElicitationClientDefaultsScenario } from './client/elicitation-defaults';
// Import all new server test scenarios
import { ServerInitializeScenario } from './server/lifecycle.js';
import { ServerInitializeScenario } from './server/lifecycle';
import {
LoggingSetLevelScenario,
CompletionCompleteScenario
} from './server/utils.js';
} from './server/utils';
import {
ToolsListScenario,
@ -23,12 +23,12 @@ import {
ToolsCallElicitationScenario,
ToolsCallAudioScenario,
ToolsCallEmbeddedResourceScenario
} from './server/tools.js';
} from './server/tools';
import { JsonSchema2020_12Scenario } from './server/json-schema-2020-12.js';
import { JsonSchema2020_12Scenario } from './server/json-schema-2020-12';
import { ElicitationDefaultsScenario } from './server/elicitation-defaults.js';
import { ElicitationEnumsScenario } from './server/elicitation-enums.js';
import { ElicitationDefaultsScenario } from './server/elicitation-defaults';
import { ElicitationEnumsScenario } from './server/elicitation-enums';
import {
ResourcesListScenario,
@ -37,7 +37,7 @@ import {
ResourcesTemplateReadScenario,
ResourcesSubscribeScenario,
ResourcesUnsubscribeScenario
} from './server/resources.js';
} from './server/resources';
import {
PromptsListScenario,
@ -45,10 +45,10 @@ import {
PromptsGetWithArgsScenario,
PromptsGetEmbeddedResourceScenario,
PromptsGetWithImageScenario
} from './server/prompts.js';
} from './server/prompts';
import { authScenariosList } from './client/auth/index.js';
import { listMetadataScenarios } from './client/auth/discovery-metadata.js';
import { authScenariosList } from './client/auth/index';
import { listMetadataScenarios } from './client/auth/discovery-metadata';
// Pending client scenarios (not yet fully tested/implemented)
const pendingClientScenariosList: ClientScenario[] = [

View file

@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from 'express';
import { ConformanceCheck } from '../types.js';
import { ConformanceCheck } from '../types';
export interface LoggerOptions {
incomingId: string;

View file

@ -1,5 +1,5 @@
import { spawn, ChildProcess } from 'child_process';
import { getClientScenario, listActiveClientScenarios } from '../index.js';
import { getClientScenario, listActiveClientScenarios } from '../index';
import path from 'path';
describe('Server Scenarios', () => {

View file

@ -2,8 +2,8 @@
* SEP-1034: Elicitation default values test scenarios for MCP servers
*/
import { ClientScenario, ConformanceCheck } from '../../types.js';
import { connectToServer } from './client-helper.js';
import { ClientScenario, ConformanceCheck } from '../../types';
import { connectToServer } from './client-helper';
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
export class ElicitationDefaultsScenario implements ClientScenario {

View file

@ -2,8 +2,8 @@
* SEP-1330: Elicitation enum schema improvements test scenarios for MCP servers
*/
import { ClientScenario, ConformanceCheck } from '../../types.js';
import { connectToServer } from './client-helper.js';
import { ClientScenario, ConformanceCheck } from '../../types';
import { connectToServer } from './client-helper';
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
export class ElicitationEnumsScenario implements ClientScenario {

View file

@ -2,8 +2,8 @@
* Lifecycle test scenarios for MCP servers
*/
import { ClientScenario, ConformanceCheck } from '../../types.js';
import { connectToServer } from './client-helper.js';
import { ClientScenario, ConformanceCheck } from '../../types';
import { connectToServer } from './client-helper';
export class ServerInitializeScenario implements ClientScenario {
name = 'server-initialize';

Some files were not shown because too many files have changed in this diff Show more