Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ program
'Run conformance tests against an authorization server implementation'
)
.requiredOption('--url <url>', 'URL of the authorization server issuer')
.option('--client-id <client>', 'Client ID')
.option('--secret <secret>', 'Client Secret')
.option('-p, --port <port>', 'redirect uri port', (value) => Number(value))
.option('-o, --output-dir <path>', 'Save results to this directory')
.option(
'--spec-version <version>',
Expand Down Expand Up @@ -491,14 +494,22 @@ program
);

const allResults: { scenario: string; checks: ConformanceCheck[] }[] = [];
const details: Record<string, unknown> = {};
for (const scenarioName of scenarios) {
console.log(`\n=== Running scenario: ${scenarioName} ===`);
try {
const result = await runAuthorizationServerConformanceTest(
validated.url,
validated,
scenarioName,
details,
outputDir
);
if (
result.checks[0].status === 'SUCCESS' &&
result.checks[0].details
) {
details[scenarioName] = result.checks[0].details;
}
allResults.push({ scenario: scenarioName, checks: result.checks });
} catch (error) {
console.error(`Failed to run scenario ${scenarioName}:`, error);
Expand Down
7 changes: 4 additions & 3 deletions src/runner/authorization-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { getClientScenarioForAuthorizationServer } from '../scenarios';
import { createResultDir } from './utils';

export async function runAuthorizationServerConformanceTest(
serverUrl: string,
option: any,
scenarioName: string,
details: Record<string, unknown>,
outputDir?: string
): Promise<{
checks: ConformanceCheck[];
Expand All @@ -28,10 +29,10 @@ export async function runAuthorizationServerConformanceTest(
const scenario = getClientScenarioForAuthorizationServer(scenarioName)!;

console.log(
`Running client scenario for authorization server '${scenarioName}' against server: ${serverUrl}`
`Running client scenario for authorization server '${scenarioName}' against server: ${option.url}`
);

const checks = await scenario.run(serverUrl);
const checks = await scenario.run(option, details);

if (resultDir) {
await fs.writeFile(
Expand Down
322 changes: 322 additions & 0 deletions src/scenarios/authorization-server/authorization-code-grant.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AuthorizationCodeGrantScenario } from './authorization-code-grant.js';
import { request } from 'undici';
import { startCallbackServer } from '../client/auth/helpers/createCallbackServer';

vi.mock('undici', () => ({
request: vi.fn()
}));

vi.mock('../client/auth/helpers/createCallbackServer', () => ({
startCallbackServer: vi.fn()
}));

const mockedRequest = vi.mocked(request);
const mockedStartCallbackServer = vi.mocked(startCallbackServer);

const SERVER_URL = 'https://example.com';
const AUTHORIZATION_ENDPOINT = `${SERVER_URL}/auth`;
const TOKEN_ENDPOINT = `${SERVER_URL}/token`;

const OPTION = {
url: SERVER_URL,
clientId: 'client',
secret: 'secret',
port: 3000
};

const METADATA = {
issuer: SERVER_URL,
authorization_endpoint: AUTHORIZATION_ENDPOINT,
token_endpoint: TOKEN_ENDPOINT
};

const DETAILS = {
'authorization-server-metadata-endpoint': {
body: METADATA
}
};

function mockCallbackServer(callbackUrl: string) {
mockedStartCallbackServer.mockReturnValue({
waitForCallback: vi.fn().mockResolvedValue(callbackUrl)
} as any);
}

function mockTokenResponse(body: Record<string, unknown>) {
mockedRequest.mockResolvedValue({
statusCode: 200,
headers: {
'content-type': 'application/json',
'cache-control': 'no-store'
},
body: {
json: async () => body
}
} as any);
}

describe('AuthorizationCodeGrantScenario', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('returns SUCCESS for valid authorization response and token response', async () => {
const scenario = new AuthorizationCodeGrantScenario();

const authorizationRequest = (scenario as any).buildAuthorizationRequest(
METADATA,
OPTION
);

const authorizationUrl = new URL(authorizationRequest);

const state = authorizationUrl.searchParams.get('state');

mockCallbackServer(
`http://localhost:3000/callback?code=abc&state=${state}&iss=${SERVER_URL}`
);

mockTokenResponse({
access_token: 'access-token',
token_type: 'Bearer'
});

const checks = await scenario.run(OPTION, DETAILS);

expect(checks).toHaveLength(1);

const check = checks[0];

expect(check.status).toBe('SUCCESS');
expect(check.errorMessage).toBeUndefined();

expect(check.details).toBeDefined();

expect((check.details as any).authorizationRequest).toContain(
AUTHORIZATION_ENDPOINT
);

expect((check.details as any).authorizationResponseUrl).toContain(
'code=abc'
);

expect((check.details as any).body.access_token).toBe('access-token');
expect((check.details as any).body.token_type).toBe('Bearer');
});

it('returns FAILURE when state parameter is invalid', async () => {
const scenario = new AuthorizationCodeGrantScenario();

mockCallbackServer('http://localhost:3000/callback?code=abc&state=invalid');

mockTokenResponse({
access_token: 'access-token',
token_type: 'Bearer'
});

const checks = await scenario.run(OPTION, DETAILS);

expect(checks).toHaveLength(1);

const check = checks[0];

expect(check.status).toBe('FAILURE');
expect(check.errorMessage).toContain('Invalid state parameter');
});

it('returns FAILURE when code parameter is missing', async () => {
const scenario = new AuthorizationCodeGrantScenario();

const authorizationRequest = (scenario as any).buildAuthorizationRequest(
METADATA,
OPTION
);

const authorizationUrl = new URL(authorizationRequest);

const state = authorizationUrl.searchParams.get('state');

mockCallbackServer(`http://localhost:3000/callback?state=${state}`);

const checks = await scenario.run(OPTION, DETAILS);

expect(checks).toHaveLength(1);

const check = checks[0];

expect(check.status).toBe('FAILURE');
expect(check.errorMessage).toContain('Invalid code parameter');
});

it('returns FAILURE when iss parameter is invalid', async () => {
const scenario = new AuthorizationCodeGrantScenario();

const authorizationRequest = (scenario as any).buildAuthorizationRequest(
METADATA,
OPTION
);

const authorizationUrl = new URL(authorizationRequest);

const state = authorizationUrl.searchParams.get('state');

mockCallbackServer(
`http://localhost:3000/callback?code=abc&state=${state}&iss=https://evil.example.com`
);

mockTokenResponse({
access_token: 'access-token',
token_type: 'Bearer'
});

const checks = await scenario.run(OPTION, DETAILS);

expect(checks).toHaveLength(1);

const check = checks[0];

expect(check.status).toBe('FAILURE');
expect(check.errorMessage).toContain('Invalid iss parameter');
});

it('returns FAILURE when token response does not include access_token', async () => {
const scenario = new AuthorizationCodeGrantScenario();

const authorizationRequest = (scenario as any).buildAuthorizationRequest(
METADATA,
OPTION
);

const authorizationUrl = new URL(authorizationRequest);

const state = authorizationUrl.searchParams.get('state');

mockCallbackServer(
`http://localhost:3000/callback?code=abc&state=${state}`
);

mockTokenResponse({
token_type: 'Bearer'
});

const checks = await scenario.run(OPTION, DETAILS);

expect(checks).toHaveLength(1);

const check = checks[0];

expect(check.status).toBe('FAILURE');
expect(check.errorMessage).toContain('Missing access_token');
});

it('returns FAILURE when token response does not include token_type', async () => {
const scenario = new AuthorizationCodeGrantScenario();

const authorizationRequest = (scenario as any).buildAuthorizationRequest(
METADATA,
OPTION
);

const authorizationUrl = new URL(authorizationRequest);

const state = authorizationUrl.searchParams.get('state');

mockCallbackServer(
`http://localhost:3000/callback?code=abc&state=${state}`
);

mockTokenResponse({
access_token: 'access-token'
});

const checks = await scenario.run(OPTION, DETAILS);

expect(checks).toHaveLength(1);

const check = checks[0];

expect(check.status).toBe('FAILURE');
expect(check.errorMessage).toContain('Missing token_type');
});

it('returns FAILURE when token response Content-Type is invalid', async () => {
const scenario = new AuthorizationCodeGrantScenario();

const authorizationRequest = (scenario as any).buildAuthorizationRequest(
METADATA,
OPTION
);

const authorizationUrl = new URL(authorizationRequest);

const state = authorizationUrl.searchParams.get('state');

mockCallbackServer(
`http://localhost:3000/callback?code=abc&state=${state}`
);

mockedRequest.mockResolvedValue({
statusCode: 200,
headers: {
'content-type': 'text/plain',
'cache-control': 'no-store'
},
body: {
json: async () => ({
access_token: 'access-token',
token_type: 'Bearer'
})
}
} as any);

const checks = await scenario.run(OPTION, DETAILS);

expect(checks).toHaveLength(1);

const check = checks[0];

expect(check.status).toBe('FAILURE');
expect(check.errorMessage).toContain('Invalid Content-Type');
});

it('returns FAILURE when token response Cache-Control is invalid', async () => {
const scenario = new AuthorizationCodeGrantScenario();

const authorizationRequest = (scenario as any).buildAuthorizationRequest(
METADATA,
OPTION
);

const authorizationUrl = new URL(authorizationRequest);

const state = authorizationUrl.searchParams.get('state');

mockCallbackServer(
`http://localhost:3000/callback?code=abc&state=${state}`
);

mockedRequest.mockResolvedValue({
statusCode: 200,
headers: {
'content-type': 'application/json',
'cache-control': 'public'
},
body: {
json: async () => ({
access_token: 'access-token',
token_type: 'Bearer'
})
}
} as any);

const checks = await scenario.run(OPTION, DETAILS);

expect(checks).toHaveLength(1);

const check = checks[0];

expect(check.status).toBe('FAILURE');
expect(check.errorMessage).toContain('Invalid Cache-Control');
});
});
Loading
Loading