From 1bf8b2a9025aca7e783ef328c2091f854f2ef6b1 Mon Sep 17 00:00:00 2001 From: Tim Freudenthal Date: Fri, 29 May 2026 18:30:21 +0000 Subject: [PATCH] feat(mcp/oauth): support audience parameter for Auth0/Cognito-style providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LibreChat already follows RFC 9728 (Protected Resource Metadata discovery) and RFC 8707 (resource indicators on /authorize). However, authorization servers that pre-date RFC 8707 — most prominently Auth0 — issue API-scoped access tokens only when an Auth0-specific 'audience' parameter is supplied on /authorize and /token. Without it, refresh_token responses strip the API audience and the next MCP call 401s. This change adds an optional 'audience' field to OAuthOptionsSchema and forwards it on: * pre-configured authorize URL build * discovered (DCR + RFC 9728) authorize URL build * refresh_token grant body 'resource' (RFC 8707) is left untouched and remains the standards-conformant route; 'audience' covers providers that ignore 'resource'. The two are independent — providers may accept either, both, or neither, so we forward whichever the operator configures. Schema tests added; no behavioral change for existing configs (field is optional with no default). Refs: MCP Authorization Spec 2025-06-18, RFC 9728, RFC 8707. --- packages/api/src/mcp/oauth/handler.ts | 38 ++++++++++++++++++++ packages/data-provider/specs/mcp.spec.ts | 45 ++++++++++++++++++++++++ packages/data-provider/src/mcp.ts | 12 +++++++ 3 files changed, 95 insertions(+) diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 6f0aed3101..b7b93572f7 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -577,6 +577,18 @@ export class MCPOAuthHandler { authorizationUrl.searchParams.set('state', state); logger.debug(`[MCPOAuth] Added state parameter to authorization URL`); + /** + * Auth0/Cognito-style `audience` parameter. Forwarded as-is; the provider + * decides whether it accepts RFC 8707 `resource`, the legacy `audience`, + * or both. See `OAuthOptionsSchema.audience`. + */ + if (config?.audience) { + authorizationUrl.searchParams.set('audience', config.audience); + logger.debug( + `[MCPOAuth] Added audience parameter (pre-configured): ${config.audience}`, + ); + } + const flowMetadata: MCPOAuthFlowMetadata = { serverName, userId, @@ -728,6 +740,7 @@ export class MCPOAuthHandler { `[MCPOAuth] Added resource parameter to authorization URL: ${canonicalResource}`, ); } else { + // resource omitted on purpose — see comment below in the `else` branch. /** * Reachable only when `discoverOAuthProtectedResourceMetadata` did not return a * document (404 / network error / server does not implement RFC 9728). If a PRM @@ -740,6 +753,18 @@ export class MCPOAuthHandler { 'This can cause issues with some Authorization Servers that expect a "resource" parameter.', ); } + + /** + * Auth0/Cognito-style `audience` parameter. Independent of `resource` (RFC 8707): + * some authorization servers ignore `resource` and only mint API-scoped tokens + * when `audience` is supplied. See `OAuthOptionsSchema.audience`. + */ + if (config?.audience) { + authorizationUrl.searchParams.set('audience', config.audience); + logger.debug( + `[MCPOAuth] Added audience parameter (discovered flow): ${config.audience}`, + ); + } } catch (error) { logger.error(`[MCPOAuth] startAuthorization failed:`, error); throw error; @@ -1170,6 +1195,19 @@ export class MCPOAuthHandler { body.append('scope', metadata.clientInfo.scope); } + /** + * Forward Auth0/Cognito-style `audience` on refresh as well — providers that + * required it on `/authorize` typically require it on `/token` too, otherwise + * the refresh exchange returns a token without the API audience and the next + * MCP call 401s. See `OAuthOptionsSchema.audience`. + */ + if (config?.audience) { + body.append('audience', config.audience); + logger.debug( + `[MCPOAuth] Added audience parameter to refresh request: ${config.audience}`, + ); + } + const headers: HeadersInit = { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/packages/data-provider/specs/mcp.spec.ts b/packages/data-provider/specs/mcp.spec.ts index aba23db52a..b8a2aab3f0 100644 --- a/packages/data-provider/specs/mcp.spec.ts +++ b/packages/data-provider/specs/mcp.spec.ts @@ -272,5 +272,50 @@ describe('MCP schemas', () => { expect(result.success).toBe(true); }); + + it('should accept audience parameter (Auth0/Cognito-style)', () => { + const result = MCPOptionsSchema.safeParse({ + type: 'streamable-http', + url: 'https://mcp-server.com/http', + oauth: { + audience: 'https://api.example.com', + }, + }); + + expect(result.success).toBe(true); + if (result.success && result.data.oauth) { + expect(result.data.oauth.audience).toBe('https://api.example.com'); + } + }); + + it('should accept audience alongside scope and other OAuth fields', () => { + const result = MCPOptionsSchema.safeParse({ + type: 'streamable-http', + url: 'https://mcp-server.com/http', + oauth: { + authorization_url: 'https://auth.example.com/authorize', + token_url: 'https://auth.example.com/token', + scope: 'read execute', + audience: 'https://api.example.com', + }, + }); + + expect(result.success).toBe(true); + }); + + it('should treat audience as optional (omitting it is fine)', () => { + const result = MCPOptionsSchema.safeParse({ + type: 'streamable-http', + url: 'https://mcp-server.com/http', + oauth: { + scope: 'read', + }, + }); + + expect(result.success).toBe(true); + if (result.success && result.data.oauth) { + expect(result.data.oauth.audience).toBeUndefined(); + } + }); }); }); diff --git a/packages/data-provider/src/mcp.ts b/packages/data-provider/src/mcp.ts index 69efb4693f..4263f070b8 100644 --- a/packages/data-provider/src/mcp.ts +++ b/packages/data-provider/src/mcp.ts @@ -28,6 +28,18 @@ const OAuthOptionsSchema = z code_challenge_methods_supported: z.array(z.string()).optional(), /** Skip code challenge validation and force S256 (useful for providers like AWS Cognito that support S256 but don't advertise it) */ skip_code_challenge_check: z.boolean().optional(), + /** + * Auth0/Cognito-style `audience` parameter. Authorization servers that pre-date + * RFC 8707 — most prominently Auth0 — issue API-scoped access tokens only when + * the authorization (and refresh) request advertises an `audience`. RFC 8707 + * `resource` (set automatically from Protected Resource Metadata) is the + * standards-conformant route; `audience` covers the providers that ignore it. + * + * When set, the value is forwarded as-is on `/authorize` and `/token` + * (including refresh) — no canonicalization, since the audience identifier is + * provider-defined and may differ from the MCP server URL. + */ + audience: z.string().optional(), /** OAuth revocation endpoint (optional - can be auto-discovered) */ revocation_endpoint: z.string().url().optional(), /** OAuth revocation endpoint authentication methods supported (optional - can be auto-discovered) */