Default to S256 code challenge if not specified in authorization server metadata (#992)

This commit is contained in:
Luca Chang 2025-10-07 07:29:44 -07:00 • committed by GitHub
commit 59cb50463a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -786,30 +786,6 @@ describe('OAuth Authorization', () => {
expect(calls[1][0].toString()).toBe('https://auth.example.com/.well-known/oauth-authorization-server');
});
it('throws error when OIDC provider does not support S256 PKCE', async () => {
// OAuth discovery fails
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404
});
// OpenID Connect discovery succeeds but without S256 support
const invalidOpenIdMetadata = {
...validOpenIdMetadata,
code_challenge_methods_supported: ['plain'] // Missing S256
};
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => invalidOpenIdMetadata
});
await expect(discoverAuthorizationServerMetadata('https://auth.example.com')).rejects.toThrow(
'does not support S256 code challenge method required by MCP specification'
);
});
it('continues on 4xx errors', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
@ -913,6 +889,17 @@ describe('OAuth Authorization', () => {
code_challenge_methods_supported: ['S256']
};
const validOpenIdMetadata = {
issuer: 'https://auth.example.com',
authorization_endpoint: 'https://auth.example.com/auth',
token_endpoint: 'https://auth.example.com/token',
jwks_uri: 'https://auth.example.com/jwks',
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
response_types_supported: ['code'],
code_challenge_methods_supported: ['S256']
};
const validClientInfo = {
client_id: 'client123',
client_secret: 'secret123',
@ -986,9 +973,9 @@ describe('OAuth Authorization', () => {
expect(authorizationUrl.searchParams.get('prompt')).toBe('consent');
});
it('uses metadata authorization_endpoint when provided', async () => {
it.each([validMetadata, validOpenIdMetadata])('uses metadata authorization_endpoint when provided', async baseMetadata => {
const { authorizationUrl } = await startAuthorization('https://auth.example.com', {
metadata: validMetadata,
metadata: baseMetadata,
clientInformation: validClientInfo,
redirectUrl: 'http://localhost:3000/callback'
});
@ -996,9 +983,9 @@ describe('OAuth Authorization', () => {
expect(authorizationUrl.toString()).toMatch(/^https:\/\/auth\.example\.com\/auth\?/);
});
it('validates response type support', async () => {
it.each([validMetadata, validOpenIdMetadata])('validates response type support', async baseMetadata => {
const metadata = {
...validMetadata,
...baseMetadata,
response_types_supported: ['token'] // Does not support 'code'
};
@ -1011,21 +998,44 @@ describe('OAuth Authorization', () => {
).rejects.toThrow(/does not support response type/);
});
it('validates PKCE support', async () => {
const metadata = {
...validMetadata,
response_types_supported: ['code'],
code_challenge_methods_supported: ['plain'] // Does not support 'S256'
};
// https://github.com/modelcontextprotocol/typescript-sdk/issues/832
it.each([validMetadata, validOpenIdMetadata])(
'assumes supported code challenge methods includes S256 if absent',
async baseMetadata => {
const metadata = {
...baseMetadata,
response_types_supported: ['code'],
code_challenge_methods_supported: undefined
};
await expect(
startAuthorization('https://auth.example.com', {
const { authorizationUrl } = await startAuthorization('https://auth.example.com', {
metadata,
clientInformation: validClientInfo,
redirectUrl: 'http://localhost:3000/callback'
})
).rejects.toThrow(/does not support code challenge method/);
});
});
expect(authorizationUrl.toString()).toMatch(/^https:\/\/auth\.example\.com\/auth\?.+&code_challenge_method=S256/);
}
);
it.each([validMetadata, validOpenIdMetadata])(
'validates supported code challenge methods includes S256 if present',
async baseMetadata => {
const metadata = {
...baseMetadata,
response_types_supported: ['code'],
code_challenge_methods_supported: ['plain'] // Does not support 'S256'
};
await expect(
startAuthorization('https://auth.example.com', {
metadata,
clientInformation: validClientInfo,
redirectUrl: 'http://localhost:3000/callback'
})
).rejects.toThrow(/does not support code challenge method/);
}
);
});
describe('exchangeAuthorization', () => {