fix(mcp): the OAuth proxy advertises and returns token auth method none

The proxy issues no client secrets, but its authorization server metadata
named no token_endpoint_auth_methods_supported (RFC 8414 then implies
client_secret_basic) and /oauth/register echoed a requested secret-based
method. A client following that default registered with client_secret_basic
and then failed the token exchange ("client_secret_basic authentication
requires a client_secret"), reproduced with the SDK calls a self-hosted chat
client makes. The metadata now lists ["none"] and registration always answers
none (RFC 7591 lets the server replace a requested value).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-09-24 13:35:47 +02:00
parent c59b366560
commit 4a4bbc056a
2 changed files with 28 additions and 2 deletions
@@ -84,6 +84,12 @@ describe('/.well-known/oauth-authorization-server — RFC 8414 metadata', () =>
expect(json.issuer).toBe(baseUrl);
expect(json.authorization_response_iss_parameter_supported).toBe(true);
});
it('advertises public clients only, so clients never pick a secret-based token auth method', async () => {
const res = await fetch(`${baseUrl}/.well-known/oauth-authorization-server`);
const json = (await res.json()) as Record<string, unknown>;
expect(json.token_endpoint_auth_methods_supported).toEqual(['none']);
});
});
describe('/oauth/register — dynamic client registration (RFC 7591)', () => {
@@ -99,6 +105,19 @@ describe('/oauth/register — dynamic client registration (RFC 7591)', () => {
expect(json.token_endpoint_auth_method).toBe('none');
});
it('answers a requested secret-based auth method with none and issues no secret', async () => {
// A self-hosted client following the RFC 8414 default registers with client_secret_basic;
// echoing it back made the client demand a secret at the token endpoint.
const { status, json } = await register({
client_name: 'Self-hosted client',
redirect_uris: ['http://localhost:3080/api/mcp/example/oauth/callback'],
token_endpoint_auth_method: 'client_secret_basic',
});
expect(status).toBe(201);
expect(json.token_endpoint_auth_method).toBe('none');
expect(json.client_secret).toBeUndefined();
});
it('registers a VS Code client (loopback + vscode.dev redirect) — GitHub Copilot DCR', async () => {
const { status, json } = await register({
client_name: 'Visual Studio Code',
@@ -581,6 +581,10 @@ export function createOAuthRouter(): Router {
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token'],
code_challenge_methods_supported: ['S256'],
// Every client of this proxy is public (PKCE); it issues no client secrets. Without this
// field RFC 8414 implies client_secret_basic, and clients that follow it (LibreChat) register
// with a secret method and then fail the token exchange for lack of a secret.
token_endpoint_auth_methods_supported: ['none'],
// MCP spec 2026-07-28 client registration: clients that support Client ID
// Metadata Documents pick them over dynamic registration when this flag
// is present (cimd.ts resolves the document at /oauth/authorize).
@@ -602,7 +606,7 @@ export function createOAuthRouter(): Router {
// registration itself, an org-wide auth DoS. The .well-known discovery docs
// stay unthrottled (static JSON, no outbound work, no logging).
router.post('/oauth/register', oauthRateLimit, (req, res) => {
const { client_name, redirect_uris, grant_types, response_types, token_endpoint_auth_method } = req.body ?? {};
const { client_name, redirect_uris, grant_types, response_types } = req.body ?? {};
// Field-level bounds — backstop the route-level body-size limit. The
// parser caps total body bytes; these caps stop a single registration
@@ -671,7 +675,10 @@ export function createOAuthRouter(): Router {
redirect_uris: uris,
grant_types: grant_types ?? ['authorization_code', 'refresh_token'],
response_types: response_types ?? ['code'],
token_endpoint_auth_method: token_endpoint_auth_method ?? 'none',
// Always 'none', whatever was requested: no secret is ever issued (RFC 7591 §3.2.1 lets the
// server replace a requested value). Echoing a secret-based method made SDK clients demand a
// client_secret at the token endpoint that does not exist.
token_endpoint_auth_method: 'none',
});
});