Skip to content

Commit 70cc5ef

Browse files
authored
feat: check for PKCE of authorization server metadata (#212)
1 parent 8d9f80a commit 70cc5ef

2 files changed

Lines changed: 73 additions & 24 deletions

File tree

src/scenarios/authorization-server/authorization-server-metadata.test.ts

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,32 @@ vi.mock('undici', () => ({
88

99
const mockedRequest = vi.mocked(request);
1010

11-
describe('AuthorizationServerMetadataEndpointScenario (SUCCESS only)', () => {
11+
const SERVER_URL = 'https://example.com';
12+
const AUTHORIZATION_ENDPOINT = `${SERVER_URL}/auth`;
13+
const TOKEN_ENDPOINT = `${SERVER_URL}/token`;
14+
15+
const validMetadata = {
16+
issuer: SERVER_URL,
17+
authorization_endpoint: AUTHORIZATION_ENDPOINT,
18+
token_endpoint: TOKEN_ENDPOINT,
19+
response_types_supported: ['code'],
20+
code_challenge_methods_supported: ['plain', 'S256']
21+
};
22+
23+
function mockMetadataResponse(body: Record<string, unknown>) {
24+
mockedRequest.mockResolvedValue({
25+
statusCode: 200,
26+
headers: { 'content-type': 'application/json' },
27+
body: { json: async () => body }
28+
} as any);
29+
}
30+
31+
describe('AuthorizationServerMetadataEndpointScenario', () => {
1232
it('returns SUCCESS for valid authorization server metadata', async () => {
1333
const scenario = new AuthorizationServerMetadataEndpointScenario();
14-
const serverUrl = 'https://example.com';
15-
16-
mockedRequest.mockResolvedValue({
17-
statusCode: 200,
18-
headers: {
19-
'content-type': 'application/json'
20-
},
21-
body: {
22-
json: async () => ({
23-
issuer: 'https://example.com',
24-
authorization_endpoint: 'https://example.com/auth',
25-
token_endpoint: 'https://example.com/token',
26-
response_types_supported: ['code']
27-
})
28-
}
29-
} as any);
30-
31-
const checks = await scenario.run(serverUrl);
34+
mockMetadataResponse(validMetadata);
35+
36+
const checks = await scenario.run(SERVER_URL);
3237

3338
expect(checks).toHaveLength(1);
3439

@@ -37,15 +42,50 @@ describe('AuthorizationServerMetadataEndpointScenario (SUCCESS only)', () => {
3742
expect(check.errorMessage).toBeUndefined();
3843
expect(check.details).toBeDefined();
3944
expect(check.details!.contentType).toContain('application/json');
40-
expect((check.details!.body as any).issuer).toBe('https://example.com');
45+
expect((check.details!.body as any).issuer).toBe(SERVER_URL);
4146
expect((check.details!.body as any).authorization_endpoint).toBe(
42-
'https://example.com/auth'
43-
);
44-
expect((check.details!.body as any).token_endpoint).toBe(
45-
'https://example.com/token'
47+
AUTHORIZATION_ENDPOINT
4648
);
49+
expect((check.details!.body as any).token_endpoint).toBe(TOKEN_ENDPOINT);
4750
expect((check.details!.body as any).response_types_supported).toEqual([
4851
'code'
4952
]);
53+
expect(
54+
(check.details!.body as any).code_challenge_methods_supported
55+
).toEqual(['plain', 'S256']);
56+
});
57+
58+
it('returns FAILURE when code_challenge_methods_supported is missing', async () => {
59+
const scenario = new AuthorizationServerMetadataEndpointScenario();
60+
mockMetadataResponse({
61+
issuer: validMetadata.issuer,
62+
authorization_endpoint: validMetadata.authorization_endpoint,
63+
token_endpoint: validMetadata.token_endpoint,
64+
response_types_supported: validMetadata.response_types_supported
65+
});
66+
67+
const checks = await scenario.run(SERVER_URL);
68+
69+
expect(checks).toHaveLength(1);
70+
71+
const check = checks[0];
72+
expect(check.status).toBe('FAILURE');
73+
expect(check.errorMessage).toContain('code_challenge_methods_supported');
74+
});
75+
76+
it('returns FAILURE when code_challenge_methods_supported does not include S256', async () => {
77+
const scenario = new AuthorizationServerMetadataEndpointScenario();
78+
mockMetadataResponse({
79+
...validMetadata,
80+
code_challenge_methods_supported: ['plain']
81+
});
82+
83+
const checks = await scenario.run(SERVER_URL);
84+
85+
expect(checks).toHaveLength(1);
86+
87+
const check = checks[0];
88+
expect(check.status).toBe('FAILURE');
89+
expect(check.errorMessage).toContain('code_challenge_methods_supported');
5090
});
5191
});

src/scenarios/authorization-server/authorization-server-metadata.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,15 @@ export class AuthorizationServerMetadataEndpointScenario implements ClientScenar
142142
);
143143
}
144144

145+
if (
146+
!Array.isArray(body.code_challenge_methods_supported) ||
147+
!body.code_challenge_methods_supported.includes('S256')
148+
) {
149+
errors.push(
150+
'Response body does not include valid "code_challenge_methods_supported" claim'
151+
);
152+
}
153+
145154
if (body.issuer !== serverUrl) {
146155
errors.push(`Invalid issuer: ${body.issuer ?? '(missing)'}`);
147156
}

0 commit comments

Comments
 (0)