豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 4cbcec0

Browse files
pcarletonbhosmer-antmattzcarey
authored
[v1.x backport] Default to client_secret_basic when server omits token_endpoint_auth_methods_supported (#1611)
Co-authored-by: Basil Hosmer <basil@anthropic.com> Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
1 parent c9b58d1 commit 4cbcec0

File tree

3 files changed

+80
-26
lines changed

3 files changed

+80
-26
lines changed

src/client/auth.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,21 +263,25 @@ const AUTHORIZATION_CODE_CHALLENGE_METHOD = 'S256';
263263
export function selectClientAuthMethod(clientInformation: OAuthClientInformationMixed, supportedMethods: string[]): ClientAuthMethod {
264264
const hasClientSecret = clientInformation.client_secret !== undefined;
265265

266-
// If server doesn't specify supported methods, use RFC 6749 defaults
267-
if (supportedMethods.length === 0) {
268-
return hasClientSecret ? 'client_secret_post' : 'none';
269-
}
270-
271-
// Prefer the method returned by the server during client registration if valid and supported
266+
// Prefer the method returned by the server during client registration, if valid.
267+
// When server metadata is present we also require the method to be listed as supported;
268+
// when supportedMethods is empty (metadata omitted the field) the DCR hint stands alone.
272269
if (
273270
'token_endpoint_auth_method' in clientInformation &&
274271
clientInformation.token_endpoint_auth_method &&
275272
isClientAuthMethod(clientInformation.token_endpoint_auth_method) &&
276-
supportedMethods.includes(clientInformation.token_endpoint_auth_method)
273+
(supportedMethods.length === 0 || supportedMethods.includes(clientInformation.token_endpoint_auth_method))
277274
) {
278275
return clientInformation.token_endpoint_auth_method;
279276
}
280277

278+
// If server metadata omits token_endpoint_auth_methods_supported, RFC 8414 §2 says the
279+
// default is client_secret_basic. RFC 6749 §2.3.1 also requires servers to support HTTP
280+
// Basic authentication for clients with a secret, making it the safest default.
281+
if (supportedMethods.length === 0) {
282+
return hasClientSecret ? 'client_secret_basic' : 'none';
283+
}
284+
281285
// Try methods in priority order (most secure first)
282286
if (hasClientSecret && supportedMethods.includes('client_secret_basic')) {
283287
return 'client_secret_basic';

test/client/auth.test.ts

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,27 @@ describe('OAuth Authorization', () => {
12971297
const authMethod = selectClientAuthMethod(clientInfo, supportedMethods);
12981298
expect(authMethod).toBe('none');
12991299
});
1300+
it('defaults to client_secret_basic when server omits token_endpoint_auth_methods_supported (RFC 8414 §2)', () => {
1301+
// RFC 8414 §2: if omitted, the default is client_secret_basic.
1302+
// RFC 6749 §2.3.1: servers MUST support HTTP Basic for clients with a secret.
1303+
const clientInfo = { client_id: 'test-client-id', client_secret: 'test-client-secret' };
1304+
const authMethod = selectClientAuthMethod(clientInfo, []);
1305+
expect(authMethod).toBe('client_secret_basic');
1306+
});
1307+
it('defaults to none for public clients when server omits token_endpoint_auth_methods_supported', () => {
1308+
const clientInfo = { client_id: 'test-client-id' };
1309+
const authMethod = selectClientAuthMethod(clientInfo, []);
1310+
expect(authMethod).toBe('none');
1311+
});
1312+
it('honors DCR-returned token_endpoint_auth_method even when server metadata omits supported methods', () => {
1313+
const clientInfo = {
1314+
client_id: 'test-client-id',
1315+
client_secret: 'test-client-secret',
1316+
token_endpoint_auth_method: 'client_secret_post'
1317+
};
1318+
const authMethod = selectClientAuthMethod(clientInfo, []);
1319+
expect(authMethod).toBe('client_secret_post');
1320+
});
13001321
});
13011322

13021323
describe('startAuthorization', () => {
@@ -1513,8 +1534,10 @@ describe('OAuth Authorization', () => {
15131534
expect(body.get('grant_type')).toBe('authorization_code');
15141535
expect(body.get('code')).toBe('code123');
15151536
expect(body.get('code_verifier')).toBe('verifier123');
1516-
expect(body.get('client_id')).toBe('client123');
1517-
expect(body.get('client_secret')).toBe('secret123');
1537+
// Default auth method is client_secret_basic when no metadata provided (RFC 8414 §2)
1538+
expect(body.get('client_id')).toBeNull();
1539+
expect(body.get('client_secret')).toBeNull();
1540+
expect(options.headers.get('Authorization')).toBe('Basic ' + btoa('client123:secret123'));
15181541
expect(body.get('redirect_uri')).toBe('http://localhost:3000/callback');
15191542
expect(body.get('resource')).toBe('https://api.example.com/mcp-server');
15201543
});
@@ -1552,8 +1575,10 @@ describe('OAuth Authorization', () => {
15521575
expect(body.get('grant_type')).toBe('authorization_code');
15531576
expect(body.get('code')).toBe('code123');
15541577
expect(body.get('code_verifier')).toBe('verifier123');
1555-
expect(body.get('client_id')).toBe('client123');
1556-
expect(body.get('client_secret')).toBe('secret123');
1578+
// Default auth method is client_secret_basic when no metadata provided (RFC 8414 §2)
1579+
expect(body.get('client_id')).toBeNull();
1580+
expect(body.get('client_secret')).toBeNull();
1581+
expect(options.headers.get('Authorization')).toBe('Basic ' + btoa('client123:secret123'));
15571582
expect(body.get('redirect_uri')).toBe('http://localhost:3000/callback');
15581583
expect(body.get('resource')).toBe('https://api.example.com/mcp-server');
15591584
});
@@ -1675,8 +1700,10 @@ describe('OAuth Authorization', () => {
16751700
expect(body.get('grant_type')).toBe('authorization_code');
16761701
expect(body.get('code')).toBe('code123');
16771702
expect(body.get('code_verifier')).toBe('verifier123');
1678-
expect(body.get('client_id')).toBe('client123');
1679-
expect(body.get('client_secret')).toBe('secret123');
1703+
// Default auth method is client_secret_basic when no metadata provided (RFC 8414 §2)
1704+
expect(body.get('client_id')).toBeNull();
1705+
expect(body.get('client_secret')).toBeNull();
1706+
expect((options.headers as Headers).get('Authorization')).toBe('Basic ' + btoa('client123:secret123'));
16801707
expect(body.get('redirect_uri')).toBe('http://localhost:3000/callback');
16811708
expect(body.get('resource')).toBe('https://api.example.com/mcp-server');
16821709
});
@@ -1735,8 +1762,10 @@ describe('OAuth Authorization', () => {
17351762
const body = mockFetch.mock.calls[0][1].body as URLSearchParams;
17361763
expect(body.get('grant_type')).toBe('refresh_token');
17371764
expect(body.get('refresh_token')).toBe('refresh123');
1738-
expect(body.get('client_id')).toBe('client123');
1739-
expect(body.get('client_secret')).toBe('secret123');
1765+
// Default auth method is client_secret_basic when no metadata provided (RFC 8414 §2)
1766+
expect(body.get('client_id')).toBeNull();
1767+
expect(body.get('client_secret')).toBeNull();
1768+
expect(headers.get('Authorization')).toBe('Basic ' + btoa('client123:secret123'));
17401769
expect(body.get('resource')).toBe('https://api.example.com/mcp-server');
17411770
});
17421771

@@ -3133,7 +3162,7 @@ describe('OAuth Authorization', () => {
31333162
expect(body.get('client_secret')).toBeNull();
31343163
});
31353164

3136-
it('defaults to client_secret_post when no auth methods specified', async () => {
3165+
it('defaults to client_secret_basic when no auth methods specified (RFC 8414 §2)', async () => {
31373166
mockFetch.mockResolvedValueOnce({
31383167
ok: true,
31393168
status: 200,
@@ -3150,13 +3179,15 @@ describe('OAuth Authorization', () => {
31503179
expect(tokens).toEqual(validTokens);
31513180
const request = mockFetch.mock.calls[0][1];
31523181

3153-
// Check headers
3154-
expect(request.headers.get('Content-Type')).toBe('application/x-www-form-urlencoded');
3155-
expect(request.headers.get('Authorization')).toBeNull();
3182+
// RFC 8414 §2: when token_endpoint_auth_methods_supported is omitted,
3183+
// the default is client_secret_basic (HTTP Basic auth, not body params)
3184+
const authHeader = request.headers.get('Authorization');
3185+
const expected = 'Basic ' + btoa('client123:secret123');
3186+
expect(authHeader).toBe(expected);
31563187

31573188
const body = request.body as URLSearchParams;
3158-
expect(body.get('client_id')).toBe('client123');
3159-
expect(body.get('client_secret')).toBe('secret123');
3189+
expect(body.get('client_id')).toBeNull();
3190+
expect(body.get('client_secret')).toBeNull();
31603191
});
31613192
});
31623193

test/client/sse.test.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@ import { Mock, Mocked, MockedFunction, MockInstance } from 'vitest';
88
import { listenOnRandomPort } from '../helpers/http.js';
99
import { AddressInfo } from 'node:net';
1010

11+
/**
12+
* Parses HTTP Basic auth from a request's Authorization header.
13+
* Returns the decoded client_id and client_secret, or undefined if the header is absent or malformed.
14+
* client_secret_basic is the default client auth method when server metadata omits
15+
* token_endpoint_auth_methods_supported (RFC 8414 §2).
16+
*/
17+
function parseBasicAuth(req: IncomingMessage): { clientId: string; clientSecret: string } | undefined {
18+
const auth = req.headers.authorization;
19+
if (!auth || !auth.startsWith('Basic ')) return undefined;
20+
const decoded = Buffer.from(auth.slice(6), 'base64').toString('utf8');
21+
const sep = decoded.indexOf(':');
22+
if (sep === -1) return undefined;
23+
return { clientId: decoded.slice(0, sep), clientSecret: decoded.slice(sep + 1) };
24+
}
25+
1126
describe('SSEClientTransport', () => {
1227
let resourceServer: Server;
1328
let authServer: Server;
@@ -668,11 +683,12 @@ describe('SSEClientTransport', () => {
668683
});
669684
req.on('end', () => {
670685
const params = new URLSearchParams(body);
686+
const basicAuth = parseBasicAuth(req);
671687
if (
672688
params.get('grant_type') === 'refresh_token' &&
673689
params.get('refresh_token') === 'refresh-token' &&
674-
params.get('client_id') === 'test-client-id' &&
675-
params.get('client_secret') === 'test-client-secret'
690+
basicAuth?.clientId === 'test-client-id' &&
691+
basicAuth?.clientSecret === 'test-client-secret'
676692
) {
677693
res.writeHead(200, { 'Content-Type': 'application/json' });
678694
res.end(
@@ -796,11 +812,12 @@ describe('SSEClientTransport', () => {
796812
});
797813
req.on('end', () => {
798814
const params = new URLSearchParams(body);
815+
const basicAuth = parseBasicAuth(req);
799816
if (
800817
params.get('grant_type') === 'refresh_token' &&
801818
params.get('refresh_token') === 'refresh-token' &&
802-
params.get('client_id') === 'test-client-id' &&
803-
params.get('client_secret') === 'test-client-secret'
819+
basicAuth?.clientId === 'test-client-id' &&
820+
basicAuth?.clientSecret === 'test-client-secret'
804821
) {
805822
res.writeHead(200, { 'Content-Type': 'application/json' });
806823
res.end(
@@ -1230,10 +1247,12 @@ describe('SSEClientTransport', () => {
12301247
});
12311248
req.on('end', () => {
12321249
const params = new URLSearchParams(body);
1250+
const basicAuth = parseBasicAuth(req);
12331251
if (
12341252
params.get('grant_type') === 'authorization_code' &&
12351253
params.get('code') === 'test-auth-code' &&
1236-
params.get('client_id') === 'test-client-id'
1254+
basicAuth?.clientId === 'test-client-id' &&
1255+
basicAuth?.clientSecret === 'test-client-secret'
12371256
) {
12381257
res.writeHead(200, { 'Content-Type': 'application/json' });
12391258
res.end(

0 commit comments

Comments
 (0)