Skip to content

Commit 67acf87

Browse files
committed
feat: add positive tests for the Authorization Code Grant
1 parent acc70de commit 67acf87

11 files changed

Lines changed: 733 additions & 23 deletions

src/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,14 @@ program
464464
'Run conformance tests against an authorization server implementation'
465465
)
466466
.requiredOption('--url <url>', 'URL of the authorization server issuer')
467+
.requiredOption('--client-id <client>', 'Client ID')
468+
.requiredOption('--secret <secret>', 'Client Secret')
469+
.option(
470+
'-p, --port <port>',
471+
'redirect uri port',
472+
(value) => Number(value),
473+
3000
474+
)
467475
.option('-o, --output-dir <path>', 'Save results to this directory')
468476
.option(
469477
'--spec-version <version>',
@@ -492,14 +500,22 @@ program
492500
);
493501

494502
const allResults: { scenario: string; checks: ConformanceCheck[] }[] = [];
503+
const details: Record<string, unknown> = {};
495504
for (const scenarioName of scenarios) {
496505
console.log(`\n=== Running scenario: ${scenarioName} ===`);
497506
try {
498507
const result = await runAuthorizationServerConformanceTest(
499-
validated.url,
508+
validated,
500509
scenarioName,
510+
details,
501511
outputDir
502512
);
513+
if (
514+
result.checks[0].status === 'SUCCESS' &&
515+
result.checks[0].details
516+
) {
517+
details[scenarioName] = result.checks[0].details;
518+
}
503519
allResults.push({ scenario: scenarioName, checks: result.checks });
504520
} catch (error) {
505521
console.error(`Failed to run scenario ${scenarioName}:`, error);

src/runner/authorization-server.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import path from 'path';
33
import { ConformanceCheck } from '../types';
44
import { getClientScenarioForAuthorizationServer } from '../scenarios';
55
import { createResultDir } from './utils';
6+
import { AuthorizationServerOptions } from '../schemas';
67

78
export async function runAuthorizationServerConformanceTest(
8-
serverUrl: string,
9+
option: AuthorizationServerOptions,
910
scenarioName: string,
11+
details: Record<string, unknown>,
1012
outputDir?: string
1113
): Promise<{
1214
checks: ConformanceCheck[];
@@ -28,10 +30,10 @@ export async function runAuthorizationServerConformanceTest(
2830
const scenario = getClientScenarioForAuthorizationServer(scenarioName)!;
2931

3032
console.log(
31-
`Running client scenario for authorization server '${scenarioName}' against server: ${serverUrl}`
33+
`Running client scenario for authorization server '${scenarioName}' against server: ${option.url}`
3234
);
3335

34-
const checks = await scenario.run(serverUrl);
36+
const checks = await scenario.run(option, details);
3537

3638
if (resultDir) {
3739
await fs.writeFile(
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import express from 'express';
2+
3+
export interface CallbackServer {
4+
waitForCallback: (timeoutMs: number) => Promise<string>;
5+
}
6+
7+
export function startCallbackServer(port: number): CallbackServer {
8+
const app = express();
9+
10+
let resolveFn: (url: string) => void;
11+
12+
const promise = new Promise<string>((resolve) => {
13+
resolveFn = resolve;
14+
});
15+
16+
const server = app.listen(port, '127.0.0.1', () => {
17+
console.log(`Callback server started: http://localhost:${port}`);
18+
});
19+
20+
app.use((req, res) => {
21+
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
22+
res.send('OK. You can close this page.');
23+
24+
server.close();
25+
resolveFn(fullUrl);
26+
});
27+
28+
return {
29+
waitForCallback: (timeoutMs: number) =>
30+
Promise.race([
31+
promise,
32+
new Promise<string>((_, reject) =>
33+
setTimeout(() => {
34+
server.close();
35+
reject(new Error('Timeout: No callback received'));
36+
}, timeoutMs)
37+
)
38+
])
39+
};
40+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { SpecReference } from '../../../types';
2+
3+
export const SpecReferences: { [key: string]: SpecReference } = {
4+
MCP_AUTH_DISCOVERY: {
5+
id: 'MCP-Authorization-metadata-discovery',
6+
url: 'https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-server-metadata-discovery'
7+
},
8+
OAUTH_2_1_AUTHORIZATION_CODE_GRANT: {
9+
id: 'OAUTH-2.1-authorization-code-grant',
10+
url: 'https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-13.html#section-4.1'
11+
}
12+
};

0 commit comments

Comments
 (0)