Add client credentials conformance tests (JWT and Basic auth) (#55)

* Add client credentials conformance scenarios with dynamic keypair generation

Implements SEP-1046 client credentials conformance tests:
- auth/client-credentials-jwt: Tests private_key_jwt authentication
- auth/client-credentials-basic: Tests client_secret_basic authentication

Key changes:
- Generate EC P-256 keypair dynamically at test start (no hardcoded keys)
- Pass credentials to client via MCP_CONFORMANCE_CONTEXT environment variable
- Add context field to ScenarioUrls interface for scenario-specific data
- Update client runner to pass context as env var to spawned client process

The MCP_CONFORMANCE_CONTEXT env var contains a JSON object with:
- client_id: The expected client identifier
- private_key_pem: PEM-encoded private key (for JWT scenarios)
- client_secret: Client secret (for basic auth scenarios)
- signing_algorithm: JWT signing algorithm (defaults to ES256)

* Fix client-credentials scenarios for conformance testing

- Generate EC keypair with extractable: true so private key can be
  exported and passed to clients via MCP_CONFORMANCE_CONTEXT
- Fix client_secret_basic scenario to use authorizationHeader param
  instead of non-existent headers object

* Fix jose type import - use CryptoKey instead of KeyLike

* Add OAuth 2.1 section 4.2 spec reference to client credentials checks

Add OAUTH_2_1_CLIENT_CREDENTIALS spec reference pointing to OAuth 2.1
draft section 4.2 (Client Credentials Grant) and include it in all
client_credentials conformance checks for both JWT and basic auth flows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add JWT iss claim verification for client credentials

Per RFC 7523, verify that the JWT issuer (iss) claim matches the
expected client_id, in addition to the existing sub claim check.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Carleton 2025-11-25 20:07:05 +00:00 • committed by GitHub
commit b96a29a962
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -15,7 +15,8 @@ export interface ClientExecutionResult {
async function executeClient(
command: string,
serverUrl: string,
timeout: number = 30000
timeout: number = 30000,
context?: Record<string, unknown>
): Promise<ClientExecutionResult> {
const commandParts = command.split(' ');
const executable = commandParts[0];
@ -25,30 +26,37 @@ async function executeClient(
let stderr = '';
let timedOut = false;
// Build environment with optional context
const env = { ...process.env };
if (context) {
env.MCP_CONFORMANCE_CONTEXT = JSON.stringify(context);
}
return new Promise((resolve) => {
const process = spawn(executable, args, {
const childProcess = spawn(executable, args, {
shell: true,
stdio: 'pipe'
stdio: 'pipe',
env
});
const timeoutHandle = setTimeout(() => {
timedOut = true;
process.kill();
childProcess.kill();
}, timeout);
if (process.stdout) {
process.stdout.on('data', (data) => {
if (childProcess.stdout) {
childProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
}
if (process.stderr) {
process.stderr.on('data', (data) => {
if (childProcess.stderr) {
childProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
}
process.on('close', (code) => {
childProcess.on('close', (code) => {
clearTimeout(timeoutHandle);
resolve({
exitCode: code || 0,
@ -58,7 +66,7 @@ async function executeClient(
});
});
process.on('error', (error) => {
childProcess.on('error', (error) => {
clearTimeout(timeoutHandle);
resolve({
exitCode: -1,
@ -90,12 +98,16 @@ export async function runConformanceTest(
const urls = await scenario.start();
console.error(`Executing client: ${clientCommand} ${urls.serverUrl}`);
if (urls.context) {
console.error(`With context: ${JSON.stringify(urls.context)}`);
}
try {
const clientOutput = await executeClient(
clientCommand,
urls.serverUrl,
timeout
timeout,
urls.context
);
// Print stdout/stderr if client exited with nonzero code