From 67c1dfe09fe810efb507c60a6fd535bd1984f8ff Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 20 Nov 2025 13:25:53 +0000 Subject: [PATCH 1/4] [auth] Add metadata suite for running metadata discovery tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new 'metadata' suite that runs just the auth/metadata-* scenarios for faster iteration when testing metadata discovery specifically. Usage: node dist/index.mjs client --suite metadata --command "..." 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/index.ts | 8 +++++--- src/scenarios/client/auth/discovery-metadata.ts | 5 +++++ src/scenarios/index.ts | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1cb6b38..21145d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,8 @@ import { listScenarios, listClientScenarios, listActiveClientScenarios, - listAuthScenarios + listAuthScenarios, + listMetadataScenarios } from './scenarios'; import { ConformanceCheck } from './types'; import { ClientOptionsSchema, ServerOptionsSchema } from './schemas'; @@ -51,7 +52,8 @@ program } const suites: Record string[]> = { - auth: listAuthScenarios + auth: listAuthScenarios, + metadata: listMetadataScenarios }; const suiteName = options.suite.toLowerCase(); @@ -147,7 +149,7 @@ program console.error('Either --scenario or --suite is required'); console.error('\nAvailable client scenarios:'); listScenarios().forEach((s) => console.error(` - ${s}`)); - console.error('\nAvailable suites: auth'); + console.error('\nAvailable suites: auth, metadata'); process.exit(1); } diff --git a/src/scenarios/client/auth/discovery-metadata.ts b/src/scenarios/client/auth/discovery-metadata.ts index 56a9694..86f1d00 100644 --- a/src/scenarios/client/auth/discovery-metadata.ts +++ b/src/scenarios/client/auth/discovery-metadata.ts @@ -217,3 +217,8 @@ export const AuthMetadataVar3Scenario = createMetadataScenario( // Export all scenarios as an array for convenience export const metadataScenarios = SCENARIO_CONFIGS.map(createMetadataScenario); + +// Export function to list metadata scenario names (for suite support) +export function listMetadataScenarios(): string[] { + return metadataScenarios.map((s) => s.name); +} diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 16e01ce..3180c26 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -46,6 +46,7 @@ import { } from './server/prompts.js'; import { authScenariosList } from './client/auth/index.js'; +import { listMetadataScenarios } from './client/auth/discovery-metadata.js'; // Pending client scenarios (not yet fully tested/implemented) const pendingClientScenariosList: ClientScenario[] = [ @@ -151,3 +152,5 @@ export function listActiveClientScenarios(): string[] { export function listAuthScenarios(): string[] { return authScenariosList.map((scenario) => scenario.name); } + +export { listMetadataScenarios }; From f559fe59ee847eb82865d0f3c8fd1ec98b65bab3 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 20 Nov 2025 13:26:12 +0000 Subject: [PATCH 2/4] Don't require lefthook to be installed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set assert_lefthook_installed to false so the hooks gracefully skip if lefthook is not installed on the system. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lefthook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lefthook.yml b/lefthook.yml index 3399e5f..445ba02 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,7 +1,7 @@ # lefthook.yml # Configuration reference: https://lefthook.dev/configuration/ -assert_lefthook_installed: true +assert_lefthook_installed: false output: - meta # Print lefthook version From 8a59560188f1a7328b8f86397c54a5738f5b4309 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 20 Nov 2025 20:27:14 +0000 Subject: [PATCH 3/4] Add token endpoint auth method conformance tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three new scenarios to test that clients correctly use the appropriate authentication method based on server metadata: - auth/token-endpoint-auth-basic: Tests client_secret_basic (HTTP Basic) - auth/token-endpoint-auth-post: Tests client_secret_post - auth/token-endpoint-auth-none: Tests public client (no auth) Each scenario configures the server to only support one auth method and verifies the client uses the correct method in token requests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/scenarios/client/auth/index.ts | 10 +- .../client/auth/token-endpoint-auth.ts | 336 ++++++++++++++++++ 2 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 src/scenarios/client/auth/token-endpoint-auth.ts diff --git a/src/scenarios/client/auth/index.ts b/src/scenarios/client/auth/index.ts index f78d2e1..140c94c 100644 --- a/src/scenarios/client/auth/index.ts +++ b/src/scenarios/client/auth/index.ts @@ -10,6 +10,11 @@ import { ScopeOmittedWhenUndefinedScenario, ScopeStepUpAuthScenario } from './scope-handling.js'; +import { + ClientSecretBasicAuthScenario, + ClientSecretPostAuthScenario, + PublicClientAuthScenario +} from './token-endpoint-auth.js'; export const authScenariosList: Scenario[] = [ ...metadataScenarios, @@ -18,5 +23,8 @@ export const authScenariosList: Scenario[] = [ new ScopeFromWwwAuthenticateScenario(), new ScopeFromScopesSupportedScenario(), new ScopeOmittedWhenUndefinedScenario(), - new ScopeStepUpAuthScenario() + new ScopeStepUpAuthScenario(), + new ClientSecretBasicAuthScenario(), + new ClientSecretPostAuthScenario(), + new PublicClientAuthScenario() ]; diff --git a/src/scenarios/client/auth/token-endpoint-auth.ts b/src/scenarios/client/auth/token-endpoint-auth.ts new file mode 100644 index 0000000..872a09e --- /dev/null +++ b/src/scenarios/client/auth/token-endpoint-auth.ts @@ -0,0 +1,336 @@ +import type { Scenario, ConformanceCheck } from '../../../types.js'; +import { ScenarioUrls } from '../../../types.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 { createRequestLogger } from '../../request-logger.js'; +import express, { Request, Response } from 'express'; + +type AuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none'; + +interface AuthServerOptions { + tokenVerifier?: MockTokenVerifier; + tokenEndpointAuthMethodsSupported: string[]; + expectedAuthMethod: AuthMethod; + onTokenRequest?: (requestData: { + authorizationHeader?: string; + bodyClientSecret?: string; + timestamp: string; + }) => void; +} + +function detectAuthMethod( + authorizationHeader?: string, + bodyClientSecret?: string +): AuthMethod { + if (authorizationHeader?.startsWith('Basic ')) { + return 'client_secret_basic'; + } + if (bodyClientSecret) { + return 'client_secret_post'; + } + return 'none'; +} + +function validateBasicAuthFormat(authorizationHeader: string): { + valid: boolean; + error?: string; +} { + const encoded = authorizationHeader.substring('Basic '.length); + try { + const decoded = Buffer.from(encoded, 'base64').toString('utf-8'); + if (!decoded.includes(':')) { + return { valid: false, error: 'missing colon separator' }; + } + return { valid: true }; + } catch { + return { valid: false, error: 'base64 decoding failed' }; + } +} + +function createAuthServerForTokenAuth( + checks: ConformanceCheck[], + getAuthBaseUrl: () => string, + options: AuthServerOptions +): express.Application { + const { + tokenVerifier, + tokenEndpointAuthMethodsSupported, + expectedAuthMethod, + onTokenRequest + } = options; + + const authRoutes = { + authorization_endpoint: '/authorize', + token_endpoint: '/token', + registration_endpoint: '/register' + }; + + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + app.use( + createRequestLogger(checks, { + incomingId: 'incoming-auth-request', + outgoingId: 'outgoing-auth-response' + }) + ); + + app.get( + '/.well-known/oauth-authorization-server', + (req: Request, res: Response) => { + checks.push({ + id: 'authorization-server-metadata', + name: 'AuthorizationServerMetadata', + description: 'Client requested authorization server metadata', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [ + SpecReferences.RFC_AUTH_SERVER_METADATA_REQUEST, + SpecReferences.MCP_AUTH_DISCOVERY + ], + details: { + url: req.url, + path: req.path + } + }); + + res.json({ + issuer: getAuthBaseUrl(), + authorization_endpoint: `${getAuthBaseUrl()}${authRoutes.authorization_endpoint}`, + token_endpoint: `${getAuthBaseUrl()}${authRoutes.token_endpoint}`, + registration_endpoint: `${getAuthBaseUrl()}${authRoutes.registration_endpoint}`, + response_types_supported: ['code'], + grant_types_supported: ['authorization_code', 'refresh_token'], + code_challenge_methods_supported: ['S256'], + token_endpoint_auth_methods_supported: tokenEndpointAuthMethodsSupported + }); + } + ); + + app.get(authRoutes.authorization_endpoint, (req: Request, res: Response) => { + checks.push({ + id: 'authorization-request', + name: 'AuthorizationRequest', + description: 'Client made authorization request', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [SpecReferences.OAUTH_2_1_AUTHORIZATION_ENDPOINT], + details: { query: req.query } + }); + + const redirectUri = req.query.redirect_uri as string; + const state = req.query.state as string; + const redirectUrl = new URL(redirectUri); + redirectUrl.searchParams.set('code', 'test-auth-code'); + if (state) { + redirectUrl.searchParams.set('state', state); + } + + res.redirect(redirectUrl.toString()); + }); + + app.post(authRoutes.token_endpoint, (req: Request, res: Response) => { + const timestamp = new Date().toISOString(); + const authorizationHeader = req.headers.authorization as string | undefined; + const bodyClientSecret = req.body.client_secret; + + checks.push({ + id: 'token-request', + name: 'TokenRequest', + description: 'Client requested access token', + status: 'SUCCESS', + timestamp, + specReferences: [SpecReferences.OAUTH_2_1_TOKEN], + details: { + endpoint: '/token', + grantType: req.body.grant_type, + hasAuthorizationHeader: !!authorizationHeader, + hasBodyClientSecret: !!bodyClientSecret + } + }); + + if (onTokenRequest) { + onTokenRequest({ authorizationHeader, bodyClientSecret, timestamp }); + } + + const token = `test-token-${Date.now()}`; + if (tokenVerifier) { + tokenVerifier.registerToken(token, []); + } + + res.json({ + access_token: token, + token_type: 'Bearer', + expires_in: 3600 + }); + }); + + app.post(authRoutes.registration_endpoint, (req: Request, res: Response) => { + const clientId = `test-client-${Date.now()}`; + const clientSecret = + expectedAuthMethod === 'none' ? undefined : `test-secret-${Date.now()}`; + + checks.push({ + id: 'client-registration', + name: 'ClientRegistration', + description: 'Client registered with authorization server', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [SpecReferences.MCP_DCR], + details: { + endpoint: '/register', + clientName: req.body.client_name, + tokenEndpointAuthMethod: expectedAuthMethod + } + }); + + res.status(201).json({ + client_id: clientId, + ...(clientSecret && { client_secret: clientSecret }), + client_name: req.body.client_name || 'test-client', + redirect_uris: req.body.redirect_uris || [], + token_endpoint_auth_method: expectedAuthMethod + }); + }); + + return app; +} + +const AUTH_METHOD_NAMES: Record = { + client_secret_basic: 'HTTP Basic authentication (client_secret_basic)', + client_secret_post: 'client_secret_post', + none: 'no authentication (public client)' +}; + +class TokenEndpointAuthScenario implements Scenario { + name: string; + description: string; + private expectedAuthMethod: AuthMethod; + private authServer = new ServerLifecycle(); + private server = new ServerLifecycle(); + private checks: ConformanceCheck[] = []; + + constructor(expectedAuthMethod: AuthMethod) { + this.expectedAuthMethod = expectedAuthMethod; + this.name = `auth/token-endpoint-auth-${expectedAuthMethod === 'client_secret_basic' ? 'basic' : expectedAuthMethod === 'client_secret_post' ? 'post' : 'none'}`; + this.description = `Tests that client uses ${AUTH_METHOD_NAMES[expectedAuthMethod]} when server only supports ${expectedAuthMethod}`; + } + + async start(): Promise { + this.checks = []; + const tokenVerifier = new MockTokenVerifier(this.checks, []); + + const authApp = createAuthServerForTokenAuth( + this.checks, + this.authServer.getUrl, + { + tokenVerifier, + tokenEndpointAuthMethodsSupported: [this.expectedAuthMethod], + expectedAuthMethod: this.expectedAuthMethod, + onTokenRequest: (data) => { + const actualMethod = detectAuthMethod( + data.authorizationHeader, + data.bodyClientSecret + ); + const isCorrect = actualMethod === this.expectedAuthMethod; + + // For basic auth, also validate the format + let formatError: string | undefined; + if ( + actualMethod === 'client_secret_basic' && + data.authorizationHeader + ) { + const validation = validateBasicAuthFormat( + data.authorizationHeader + ); + if (!validation.valid) { + formatError = validation.error; + } + } + + const status = isCorrect && !formatError ? 'SUCCESS' : 'FAILURE'; + let description: string; + + if (formatError) { + description = `Client sent Basic auth header but ${formatError}`; + } else if (isCorrect) { + description = `Client correctly used ${AUTH_METHOD_NAMES[this.expectedAuthMethod]} for token endpoint`; + } else { + description = `Client used ${actualMethod} but server only supports ${this.expectedAuthMethod}`; + } + + this.checks.push({ + id: 'token-endpoint-auth-method', + name: 'Token endpoint authentication method', + description, + status, + timestamp: data.timestamp, + specReferences: [SpecReferences.OAUTH_2_1_TOKEN], + details: { + expectedAuthMethod: this.expectedAuthMethod, + actualAuthMethod: actualMethod, + hasAuthorizationHeader: !!data.authorizationHeader, + hasBodyClientSecret: !!data.bodyClientSecret, + ...(formatError && { formatError }) + } + }); + } + } + ); + await this.authServer.start(authApp); + + const app = createServer( + this.checks, + this.server.getUrl, + this.authServer.getUrl, + { + prmPath: '/.well-known/oauth-protected-resource/mcp', + requiredScopes: [], + tokenVerifier + } + ); + await this.server.start(app); + + return { serverUrl: `${this.server.getUrl()}/mcp` }; + } + + async stop() { + await this.authServer.stop(); + await this.server.stop(); + } + + getChecks(): ConformanceCheck[] { + if (!this.checks.some((c) => c.id === 'token-endpoint-auth-method')) { + this.checks.push({ + id: 'token-endpoint-auth-method', + name: 'Token endpoint authentication method', + description: 'Client did not make a token request', + status: 'FAILURE', + timestamp: new Date().toISOString(), + specReferences: [SpecReferences.OAUTH_2_1_TOKEN] + }); + } + return this.checks; + } +} + +export class ClientSecretBasicAuthScenario extends TokenEndpointAuthScenario { + constructor() { + super('client_secret_basic'); + } +} + +export class ClientSecretPostAuthScenario extends TokenEndpointAuthScenario { + constructor() { + super('client_secret_post'); + } +} + +export class PublicClientAuthScenario extends TokenEndpointAuthScenario { + constructor() { + super('none'); + } +} From 0d0c631de59cd7619772ac75f97bd571edfd640c Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 20 Nov 2025 20:31:31 +0000 Subject: [PATCH 4/4] Refactor token endpoint auth tests to use shared createAuthServer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend createAuthServer helper with: - tokenEndpointAuthMethodsSupported option for metadata - onTokenRequest callback now receives full Request object - onRegistrationRequest callback for custom client credentials This eliminates the duplicate auth server implementation in token-endpoint-auth.ts and reduces code by ~140 lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../client/auth/helpers/createAuthServer.ts | 57 ++-- .../client/auth/token-endpoint-auth.ts | 272 ++++-------------- 2 files changed, 94 insertions(+), 235 deletions(-) diff --git a/src/scenarios/client/auth/helpers/createAuthServer.ts b/src/scenarios/client/auth/helpers/createAuthServer.ts index 7828990..b7b40b1 100644 --- a/src/scenarios/client/auth/helpers/createAuthServer.ts +++ b/src/scenarios/client/auth/helpers/createAuthServer.ts @@ -10,16 +10,21 @@ export interface AuthServerOptions { loggingEnabled?: boolean; routePrefix?: string; scopesSupported?: string[]; + tokenEndpointAuthMethodsSupported?: string[]; tokenVerifier?: MockTokenVerifier; - onTokenRequest?: (requestData: { - scope?: string; - grantType: string; - timestamp: string; - }) => { token: string; scopes: string[] }; + onTokenRequest?: ( + req: Request, + timestamp: string + ) => { token: string; scopes: string[] } | void; onAuthorizationRequest?: (requestData: { scope?: string; timestamp: string; }) => void; + onRegistrationRequest?: (req: Request) => { + clientId: string; + clientSecret?: string; + tokenEndpointAuthMethod?: string; + }; } export function createAuthServer( @@ -33,9 +38,11 @@ export function createAuthServer( loggingEnabled = true, routePrefix = '', scopesSupported, + tokenEndpointAuthMethodsSupported = ['none'], tokenVerifier, onTokenRequest, - onAuthorizationRequest + onAuthorizationRequest, + onRegistrationRequest } = options; // Track scopes from the most recent authorization request @@ -85,7 +92,7 @@ export function createAuthServer( response_types_supported: ['code'], grant_types_supported: ['authorization_code', 'refresh_token'], code_challenge_methods_supported: ['S256'], - token_endpoint_auth_methods_supported: ['none'] + token_endpoint_auth_methods_supported: tokenEndpointAuthMethodsSupported }; // Add scopes_supported if provided @@ -141,7 +148,6 @@ export function createAuthServer( app.post(authRoutes.token_endpoint, (req: Request, res: Response) => { const timestamp = new Date().toISOString(); - const requestedScope = req.body.scope; checks.push({ id: 'token-request', @@ -160,13 +166,11 @@ export function createAuthServer( let scopes: string[] = lastAuthorizationScopes; if (onTokenRequest) { - const result = onTokenRequest({ - scope: requestedScope, - grantType: req.body.grant_type, - timestamp - }); - token = result.token; - scopes = result.scopes; + const result = onTokenRequest(req, timestamp); + if (result) { + token = result.token; + scopes = result.scopes; + } } // Register token with verifier if provided @@ -183,6 +187,17 @@ export function createAuthServer( }); app.post(authRoutes.registration_endpoint, (req: Request, res: Response) => { + let clientId = 'test-client-id'; + let clientSecret: string | undefined = 'test-client-secret'; + let tokenEndpointAuthMethod: string | undefined; + + if (onRegistrationRequest) { + const result = onRegistrationRequest(req); + clientId = result.clientId; + clientSecret = result.clientSecret; + tokenEndpointAuthMethod = result.tokenEndpointAuthMethod; + } + checks.push({ id: 'client-registration', name: 'ClientRegistration', @@ -192,15 +207,19 @@ export function createAuthServer( specReferences: [SpecReferences.MCP_DCR], details: { endpoint: '/register', - clientName: req.body.client_name + clientName: req.body.client_name, + ...(tokenEndpointAuthMethod && { tokenEndpointAuthMethod }) } }); res.status(201).json({ - client_id: 'test-client-id', - client_secret: 'test-client-secret', + client_id: clientId, + ...(clientSecret && { client_secret: clientSecret }), client_name: req.body.client_name || 'test-client', - redirect_uris: req.body.redirect_uris || [] + redirect_uris: req.body.redirect_uris || [], + ...(tokenEndpointAuthMethod && { + token_endpoint_auth_method: tokenEndpointAuthMethod + }) }); }); diff --git a/src/scenarios/client/auth/token-endpoint-auth.ts b/src/scenarios/client/auth/token-endpoint-auth.ts index 872a09e..976f69b 100644 --- a/src/scenarios/client/auth/token-endpoint-auth.ts +++ b/src/scenarios/client/auth/token-endpoint-auth.ts @@ -1,25 +1,13 @@ 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 { createRequestLogger } from '../../request-logger.js'; -import express, { Request, Response } from 'express'; type AuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none'; -interface AuthServerOptions { - tokenVerifier?: MockTokenVerifier; - tokenEndpointAuthMethodsSupported: string[]; - expectedAuthMethod: AuthMethod; - onTokenRequest?: (requestData: { - authorizationHeader?: string; - bodyClientSecret?: string; - timestamp: string; - }) => void; -} - function detectAuthMethod( authorizationHeader?: string, bodyClientSecret?: string @@ -49,156 +37,6 @@ function validateBasicAuthFormat(authorizationHeader: string): { } } -function createAuthServerForTokenAuth( - checks: ConformanceCheck[], - getAuthBaseUrl: () => string, - options: AuthServerOptions -): express.Application { - const { - tokenVerifier, - tokenEndpointAuthMethodsSupported, - expectedAuthMethod, - onTokenRequest - } = options; - - const authRoutes = { - authorization_endpoint: '/authorize', - token_endpoint: '/token', - registration_endpoint: '/register' - }; - - const app = express(); - app.use(express.json()); - app.use(express.urlencoded({ extended: true })); - - app.use( - createRequestLogger(checks, { - incomingId: 'incoming-auth-request', - outgoingId: 'outgoing-auth-response' - }) - ); - - app.get( - '/.well-known/oauth-authorization-server', - (req: Request, res: Response) => { - checks.push({ - id: 'authorization-server-metadata', - name: 'AuthorizationServerMetadata', - description: 'Client requested authorization server metadata', - status: 'SUCCESS', - timestamp: new Date().toISOString(), - specReferences: [ - SpecReferences.RFC_AUTH_SERVER_METADATA_REQUEST, - SpecReferences.MCP_AUTH_DISCOVERY - ], - details: { - url: req.url, - path: req.path - } - }); - - res.json({ - issuer: getAuthBaseUrl(), - authorization_endpoint: `${getAuthBaseUrl()}${authRoutes.authorization_endpoint}`, - token_endpoint: `${getAuthBaseUrl()}${authRoutes.token_endpoint}`, - registration_endpoint: `${getAuthBaseUrl()}${authRoutes.registration_endpoint}`, - response_types_supported: ['code'], - grant_types_supported: ['authorization_code', 'refresh_token'], - code_challenge_methods_supported: ['S256'], - token_endpoint_auth_methods_supported: tokenEndpointAuthMethodsSupported - }); - } - ); - - app.get(authRoutes.authorization_endpoint, (req: Request, res: Response) => { - checks.push({ - id: 'authorization-request', - name: 'AuthorizationRequest', - description: 'Client made authorization request', - status: 'SUCCESS', - timestamp: new Date().toISOString(), - specReferences: [SpecReferences.OAUTH_2_1_AUTHORIZATION_ENDPOINT], - details: { query: req.query } - }); - - const redirectUri = req.query.redirect_uri as string; - const state = req.query.state as string; - const redirectUrl = new URL(redirectUri); - redirectUrl.searchParams.set('code', 'test-auth-code'); - if (state) { - redirectUrl.searchParams.set('state', state); - } - - res.redirect(redirectUrl.toString()); - }); - - app.post(authRoutes.token_endpoint, (req: Request, res: Response) => { - const timestamp = new Date().toISOString(); - const authorizationHeader = req.headers.authorization as string | undefined; - const bodyClientSecret = req.body.client_secret; - - checks.push({ - id: 'token-request', - name: 'TokenRequest', - description: 'Client requested access token', - status: 'SUCCESS', - timestamp, - specReferences: [SpecReferences.OAUTH_2_1_TOKEN], - details: { - endpoint: '/token', - grantType: req.body.grant_type, - hasAuthorizationHeader: !!authorizationHeader, - hasBodyClientSecret: !!bodyClientSecret - } - }); - - if (onTokenRequest) { - onTokenRequest({ authorizationHeader, bodyClientSecret, timestamp }); - } - - const token = `test-token-${Date.now()}`; - if (tokenVerifier) { - tokenVerifier.registerToken(token, []); - } - - res.json({ - access_token: token, - token_type: 'Bearer', - expires_in: 3600 - }); - }); - - app.post(authRoutes.registration_endpoint, (req: Request, res: Response) => { - const clientId = `test-client-${Date.now()}`; - const clientSecret = - expectedAuthMethod === 'none' ? undefined : `test-secret-${Date.now()}`; - - checks.push({ - id: 'client-registration', - name: 'ClientRegistration', - description: 'Client registered with authorization server', - status: 'SUCCESS', - timestamp: new Date().toISOString(), - specReferences: [SpecReferences.MCP_DCR], - details: { - endpoint: '/register', - clientName: req.body.client_name, - tokenEndpointAuthMethod: expectedAuthMethod - } - }); - - res.status(201).json({ - client_id: clientId, - ...(clientSecret && { client_secret: clientSecret }), - client_name: req.body.client_name || 'test-client', - redirect_uris: req.body.redirect_uris || [], - token_endpoint_auth_method: expectedAuthMethod - }); - }); - - return app; -} - const AUTH_METHOD_NAMES: Record = { client_secret_basic: 'HTTP Basic authentication (client_secret_basic)', client_secret_post: 'client_secret_post', @@ -223,63 +61,65 @@ class TokenEndpointAuthScenario implements Scenario { this.checks = []; const tokenVerifier = new MockTokenVerifier(this.checks, []); - const authApp = createAuthServerForTokenAuth( - this.checks, - this.authServer.getUrl, - { - tokenVerifier, - tokenEndpointAuthMethodsSupported: [this.expectedAuthMethod], - expectedAuthMethod: this.expectedAuthMethod, - onTokenRequest: (data) => { - const actualMethod = detectAuthMethod( - data.authorizationHeader, - data.bodyClientSecret - ); - const isCorrect = actualMethod === this.expectedAuthMethod; - - // For basic auth, also validate the format - let formatError: string | undefined; - if ( - actualMethod === 'client_secret_basic' && - data.authorizationHeader - ) { - const validation = validateBasicAuthFormat( - data.authorizationHeader - ); - if (!validation.valid) { - formatError = validation.error; - } + const authApp = createAuthServer(this.checks, this.authServer.getUrl, { + tokenVerifier, + tokenEndpointAuthMethodsSupported: [this.expectedAuthMethod], + onTokenRequest: (req, timestamp) => { + const authorizationHeader = req.headers.authorization as + | string + | undefined; + const bodyClientSecret = req.body.client_secret; + const actualMethod = detectAuthMethod( + authorizationHeader, + bodyClientSecret + ); + const isCorrect = actualMethod === this.expectedAuthMethod; + + // For basic auth, also validate the format + let formatError: string | undefined; + if (actualMethod === 'client_secret_basic' && authorizationHeader) { + const validation = validateBasicAuthFormat(authorizationHeader); + if (!validation.valid) { + formatError = validation.error; } + } - const status = isCorrect && !formatError ? 'SUCCESS' : 'FAILURE'; - let description: string; - - if (formatError) { - description = `Client sent Basic auth header but ${formatError}`; - } else if (isCorrect) { - description = `Client correctly used ${AUTH_METHOD_NAMES[this.expectedAuthMethod]} for token endpoint`; - } else { - description = `Client used ${actualMethod} but server only supports ${this.expectedAuthMethod}`; - } + const status = isCorrect && !formatError ? 'SUCCESS' : 'FAILURE'; + let description: string; - this.checks.push({ - id: 'token-endpoint-auth-method', - name: 'Token endpoint authentication method', - description, - status, - timestamp: data.timestamp, - specReferences: [SpecReferences.OAUTH_2_1_TOKEN], - details: { - expectedAuthMethod: this.expectedAuthMethod, - actualAuthMethod: actualMethod, - hasAuthorizationHeader: !!data.authorizationHeader, - hasBodyClientSecret: !!data.bodyClientSecret, - ...(formatError && { formatError }) - } - }); + if (formatError) { + description = `Client sent Basic auth header but ${formatError}`; + } else if (isCorrect) { + description = `Client correctly used ${AUTH_METHOD_NAMES[this.expectedAuthMethod]} for token endpoint`; + } else { + description = `Client used ${actualMethod} but server only supports ${this.expectedAuthMethod}`; } - } - ); + + this.checks.push({ + id: 'token-endpoint-auth-method', + name: 'Token endpoint authentication method', + description, + status, + timestamp, + specReferences: [SpecReferences.OAUTH_2_1_TOKEN], + details: { + expectedAuthMethod: this.expectedAuthMethod, + actualAuthMethod: actualMethod, + hasAuthorizationHeader: !!authorizationHeader, + hasBodyClientSecret: !!bodyClientSecret, + ...(formatError && { formatError }) + } + }); + }, + onRegistrationRequest: () => ({ + clientId: `test-client-${Date.now()}`, + clientSecret: + this.expectedAuthMethod === 'none' + ? undefined + : `test-secret-${Date.now()}`, + tokenEndpointAuthMethod: this.expectedAuthMethod + }) + }); await this.authServer.start(authApp); const app = createServer(