feat: check for PKCE of authorization server metadata (#212)

This commit is contained in:
Takashi Norimatsu 2026-04-14 01:56:48 +09:00 • committed by GitHub
commit 70cc5efbad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 24 deletions

View file

@ -8,27 +8,32 @@ vi.mock('undici', () => ({
const mockedRequest = vi.mocked(request);
describe('AuthorizationServerMetadataEndpointScenario (SUCCESS only)', () => {
const SERVER_URL = 'https://example.com';
const AUTHORIZATION_ENDPOINT = `${SERVER_URL}/auth`;
const TOKEN_ENDPOINT = `${SERVER_URL}/token`;
const validMetadata = {
issuer: SERVER_URL,
authorization_endpoint: AUTHORIZATION_ENDPOINT,
token_endpoint: TOKEN_ENDPOINT,
response_types_supported: ['code'],
code_challenge_methods_supported: ['plain', 'S256']
};
function mockMetadataResponse(body: Record<string, unknown>) {
mockedRequest.mockResolvedValue({
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: { json: async () => body }
} as any);
}
describe('AuthorizationServerMetadataEndpointScenario', () => {
it('returns SUCCESS for valid authorization server metadata', async () => {
const scenario = new AuthorizationServerMetadataEndpointScenario();
const serverUrl = 'https://example.com';
mockMetadataResponse(validMetadata);
mockedRequest.mockResolvedValue({
statusCode: 200,
headers: {
'content-type': 'application/json'
},
body: {
json: async () => ({
issuer: 'https://example.com',
authorization_endpoint: 'https://example.com/auth',
token_endpoint: 'https://example.com/token',
response_types_supported: ['code']
})
}
} as any);
const checks = await scenario.run(serverUrl);
const checks = await scenario.run(SERVER_URL);
expect(checks).toHaveLength(1);
@ -37,15 +42,50 @@ describe('AuthorizationServerMetadataEndpointScenario (SUCCESS only)', () => {
expect(check.errorMessage).toBeUndefined();
expect(check.details).toBeDefined();
expect(check.details!.contentType).toContain('application/json');
expect((check.details!.body as any).issuer).toBe('https://example.com');
expect((check.details!.body as any).issuer).toBe(SERVER_URL);
expect((check.details!.body as any).authorization_endpoint).toBe(
'https://example.com/auth'
);
expect((check.details!.body as any).token_endpoint).toBe(
'https://example.com/token'
AUTHORIZATION_ENDPOINT
);
expect((check.details!.body as any).token_endpoint).toBe(TOKEN_ENDPOINT);
expect((check.details!.body as any).response_types_supported).toEqual([
'code'
]);
expect(
(check.details!.body as any).code_challenge_methods_supported
).toEqual(['plain', 'S256']);
});
it('returns FAILURE when code_challenge_methods_supported is missing', async () => {
const scenario = new AuthorizationServerMetadataEndpointScenario();
mockMetadataResponse({
issuer: validMetadata.issuer,
authorization_endpoint: validMetadata.authorization_endpoint,
token_endpoint: validMetadata.token_endpoint,
response_types_supported: validMetadata.response_types_supported
});
const checks = await scenario.run(SERVER_URL);
expect(checks).toHaveLength(1);
const check = checks[0];
expect(check.status).toBe('FAILURE');
expect(check.errorMessage).toContain('code_challenge_methods_supported');
});
it('returns FAILURE when code_challenge_methods_supported does not include S256', async () => {
const scenario = new AuthorizationServerMetadataEndpointScenario();
mockMetadataResponse({
...validMetadata,
code_challenge_methods_supported: ['plain']
});
const checks = await scenario.run(SERVER_URL);
expect(checks).toHaveLength(1);
const check = checks[0];
expect(check.status).toBe('FAILURE');
expect(check.errorMessage).toContain('code_challenge_methods_supported');
});
});

View file

@ -142,6 +142,15 @@ export class AuthorizationServerMetadataEndpointScenario implements ClientScenar
);
}
if (
!Array.isArray(body.code_challenge_methods_supported) ||
!body.code_challenge_methods_supported.includes('S256')
) {
errors.push(
'Response body does not include valid "code_challenge_methods_supported" claim'
);
}
if (body.issuer !== serverUrl) {
errors.push(`Invalid issuer: ${body.issuer ?? '(missing)'}`);
}