mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 06:52:47 +00:00
feat(mcp/oauth): support audience parameter for Auth0/Cognito-style providers
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.
This commit is contained in:
parent
6d9c01927d
commit
1bf8b2a902
3 changed files with 95 additions and 0 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue