Skip to content

Commit c847294

Browse files
authored
Merge branch 'main' into non_file_roots
2 parents 0dcd092 + 59cb504 commit c847294

File tree

3 files changed

+63
-58
lines changed

3 files changed

+63
-58
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"examples:simple-server:w": "tsx --watch src/examples/server/simpleStreamableHttp.ts --oauth",
5757
"prepack": "npm run build:esm && npm run build:cjs",
5858
"lint": "eslint src/ && prettier --check .",
59+
"lint:fix": "eslint src/ --fix && prettier --write .",
5960
"test": "npm run fetch:spec-types && jest",
6061
"start": "npm run server",
6162
"server": "tsx watch --clear-screen=false src/cli.ts server",

src/client/auth.test.ts

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -786,30 +786,6 @@ describe('OAuth Authorization', () => {
786786
expect(calls[1][0].toString()).toBe('https://auth.example.com/.well-known/oauth-authorization-server');
787787
});
788788

789-
it('throws error when OIDC provider does not support S256 PKCE', async () => {
790-
// OAuth discovery fails
791-
mockFetch.mockResolvedValueOnce({
792-
ok: false,
793-
status: 404
794-
});
795-
796-
// OpenID Connect discovery succeeds but without S256 support
797-
const invalidOpenIdMetadata = {
798-
...validOpenIdMetadata,
799-
code_challenge_methods_supported: ['plain'] // Missing S256
800-
};
801-
802-
mockFetch.mockResolvedValueOnce({
803-
ok: true,
804-
status: 200,
805-
json: async () => invalidOpenIdMetadata
806-
});
807-
808-
await expect(discoverAuthorizationServerMetadata('https://auth.example.com')).rejects.toThrow(
809-
'does not support S256 code challenge method required by MCP specification'
810-
);
811-
});
812-
813789
it('continues on 4xx errors', async () => {
814790
mockFetch.mockResolvedValueOnce({
815791
ok: false,
@@ -913,6 +889,17 @@ describe('OAuth Authorization', () => {
913889
code_challenge_methods_supported: ['S256']
914890
};
915891

892+
const validOpenIdMetadata = {
893+
issuer: 'https://auth.example.com',
894+
authorization_endpoint: 'https://auth.example.com/auth',
895+
token_endpoint: 'https://auth.example.com/token',
896+
jwks_uri: 'https://auth.example.com/jwks',
897+
subject_types_supported: ['public'],
898+
id_token_signing_alg_values_supported: ['RS256'],
899+
response_types_supported: ['code'],
900+
code_challenge_methods_supported: ['S256']
901+
};
902+
916903
const validClientInfo = {
917904
client_id: 'client123',
918905
client_secret: 'secret123',
@@ -986,19 +973,19 @@ describe('OAuth Authorization', () => {
986973
expect(authorizationUrl.searchParams.get('prompt')).toBe('consent');
987974
});
988975

989-
it('uses metadata authorization_endpoint when provided', async () => {
976+
it.each([validMetadata, validOpenIdMetadata])('uses metadata authorization_endpoint when provided', async baseMetadata => {
990977
const { authorizationUrl } = await startAuthorization('https://auth.example.com', {
991-
metadata: validMetadata,
978+
metadata: baseMetadata,
992979
clientInformation: validClientInfo,
993980
redirectUrl: 'http://localhost:3000/callback'
994981
});
995982

996983
expect(authorizationUrl.toString()).toMatch(/^https:\/\/auth\.example\.com\/auth\?/);
997984
});
998985

999-
it('validates response type support', async () => {
986+
it.each([validMetadata, validOpenIdMetadata])('validates response type support', async baseMetadata => {
1000987
const metadata = {
1001-
...validMetadata,
988+
...baseMetadata,
1002989
response_types_supported: ['token'] // Does not support 'code'
1003990
};
1004991

@@ -1011,21 +998,44 @@ describe('OAuth Authorization', () => {
1011998
).rejects.toThrow(/does not support response type/);
1012999
});
10131000

1014-
it('validates PKCE support', async () => {
1015-
const metadata = {
1016-
...validMetadata,
1017-
response_types_supported: ['code'],
1018-
code_challenge_methods_supported: ['plain'] // Does not support 'S256'
1019-
};
1001+
// https://github.com/modelcontextprotocol/typescript-sdk/issues/832
1002+
it.each([validMetadata, validOpenIdMetadata])(
1003+
'assumes supported code challenge methods includes S256 if absent',
1004+
async baseMetadata => {
1005+
const metadata = {
1006+
...baseMetadata,
1007+
response_types_supported: ['code'],
1008+
code_challenge_methods_supported: undefined
1009+
};
10201010

1021-
await expect(
1022-
startAuthorization('https://auth.example.com', {
1011+
const { authorizationUrl } = await startAuthorization('https://auth.example.com', {
10231012
metadata,
10241013
clientInformation: validClientInfo,
10251014
redirectUrl: 'http://localhost:3000/callback'
1026-
})
1027-
).rejects.toThrow(/does not support code challenge method/);
1028-
});
1015+
});
1016+
1017+
expect(authorizationUrl.toString()).toMatch(/^https:\/\/auth\.example\.com\/auth\?.+&code_challenge_method=S256/);
1018+
}
1019+
);
1020+
1021+
it.each([validMetadata, validOpenIdMetadata])(
1022+
'validates supported code challenge methods includes S256 if present',
1023+
async baseMetadata => {
1024+
const metadata = {
1025+
...baseMetadata,
1026+
response_types_supported: ['code'],
1027+
code_challenge_methods_supported: ['plain'] // Does not support 'S256'
1028+
};
1029+
1030+
await expect(
1031+
startAuthorization('https://auth.example.com', {
1032+
metadata,
1033+
clientInformation: validClientInfo,
1034+
redirectUrl: 'http://localhost:3000/callback'
1035+
})
1036+
).rejects.toThrow(/does not support code challenge method/);
1037+
}
1038+
);
10291039
});
10301040

10311041
describe('exchangeAuthorization', () => {

src/client/auth.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ export class UnauthorizedError extends Error {
149149

150150
type ClientAuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none';
151151

152+
const AUTHORIZATION_CODE_RESPONSE_TYPE = 'code';
153+
const AUTHORIZATION_CODE_CHALLENGE_METHOD = 'S256';
154+
152155
/**
153156
* Determines the best client authentication method to use based on server support and client configuration.
154157
*
@@ -766,16 +769,7 @@ export async function discoverAuthorizationServerMetadata(
766769
if (type === 'oauth') {
767770
return OAuthMetadataSchema.parse(await response.json());
768771
} else {
769-
const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
770-
771-
// MCP spec requires OIDC providers to support S256 PKCE
772-
if (!metadata.code_challenge_methods_supported?.includes('S256')) {
773-
throw new Error(
774-
`Incompatible OIDC provider at ${endpointUrl}: does not support S256 code challenge method required by MCP specification`
775-
);
776-
}
777-
778-
return metadata;
772+
return OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
779773
}
780774
}
781775

@@ -803,19 +797,19 @@ export async function startAuthorization(
803797
resource?: URL;
804798
}
805799
): Promise<{ authorizationUrl: URL; codeVerifier: string }> {
806-
const responseType = 'code';
807-
const codeChallengeMethod = 'S256';
808-
809800
let authorizationUrl: URL;
810801
if (metadata) {
811802
authorizationUrl = new URL(metadata.authorization_endpoint);
812803

813-
if (!metadata.response_types_supported.includes(responseType)) {
814-
throw new Error(`Incompatible auth server: does not support response type ${responseType}`);
804+
if (!metadata.response_types_supported.includes(AUTHORIZATION_CODE_RESPONSE_TYPE)) {
805+
throw new Error(`Incompatible auth server: does not support response type ${AUTHORIZATION_CODE_RESPONSE_TYPE}`);
815806
}
816807

817-
if (!metadata.code_challenge_methods_supported || !metadata.code_challenge_methods_supported.includes(codeChallengeMethod)) {
818-
throw new Error(`Incompatible auth server: does not support code challenge method ${codeChallengeMethod}`);
808+
if (
809+
metadata.code_challenge_methods_supported &&
810+
!metadata.code_challenge_methods_supported.includes(AUTHORIZATION_CODE_CHALLENGE_METHOD)
811+
) {
812+
throw new Error(`Incompatible auth server: does not support code challenge method ${AUTHORIZATION_CODE_CHALLENGE_METHOD}`);
819813
}
820814
} else {
821815
authorizationUrl = new URL('/authorize', authorizationServerUrl);
@@ -826,10 +820,10 @@ export async function startAuthorization(
826820
const codeVerifier = challenge.code_verifier;
827821
const codeChallenge = challenge.code_challenge;
828822

829-
authorizationUrl.searchParams.set('response_type', responseType);
823+
authorizationUrl.searchParams.set('response_type', AUTHORIZATION_CODE_RESPONSE_TYPE);
830824
authorizationUrl.searchParams.set('client_id', clientInformation.client_id);
831825
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
832-
authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
826+
authorizationUrl.searchParams.set('code_challenge_method', AUTHORIZATION_CODE_CHALLENGE_METHOD);
833827
authorizationUrl.searchParams.set('redirect_uri', String(redirectUrl));
834828

835829
if (state) {

0 commit comments

Comments
 (0)