mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-14 14:38:11 +01:00
🛡️ fix: Secure MCP/Actions OAuth Flows, Resolve Race Condition & Tool Cache Cleanup (#11756)
* 🔧 fix: Update OAuth error message for clarity - Changed the default error message in the OAuth error route from 'Unknown error' to 'Unknown OAuth error' to provide clearer context during authentication failures. * 🔒 feat: Enhance OAuth flow with CSRF protection and session management - Implemented CSRF protection for OAuth flows by introducing `generateOAuthCsrfToken`, `setOAuthCsrfCookie`, and `validateOAuthCsrf` functions. - Added session management for OAuth with `setOAuthSession` and `validateOAuthSession` middleware. - Updated routes to bind CSRF tokens for MCP and action OAuth flows, ensuring secure authentication. - Enhanced tests to validate CSRF handling and session management in OAuth processes. * 🔧 refactor: Invalidate cached tools after user plugin disconnection - Added a call to `invalidateCachedTools` in the `updateUserPluginsController` to ensure that cached tools are refreshed when a user disconnects from an MCP server after a plugin authentication update. This change improves the accuracy of tool data for users. * chore: imports order * fix: domain separator regex usage in ToolService - Moved the declaration of `domainSeparatorRegex` to avoid redundancy in the `loadActionToolsForExecution` function, improving code clarity and performance. * chore: OAuth flow error handling and CSRF token generation - Enhanced the OAuth callback route to validate the flow ID format, ensuring proper error handling for invalid states. - Updated the CSRF token generation function to require a JWT secret, throwing an error if not provided, which improves security and clarity in token generation. - Adjusted tests to reflect changes in flow ID handling and ensure robust validation across various scenarios.
This commit is contained in:
parent
72a30cd9c4
commit
599f4a11f1
14 changed files with 523 additions and 141 deletions
|
|
@ -1,7 +1,12 @@
|
|||
import { useMemo, useState, useEffect, useRef, useLayoutEffect } from 'react';
|
||||
import { useMemo, useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
|
||||
import { Button } from '@librechat/client';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
|
||||
import {
|
||||
Constants,
|
||||
dataService,
|
||||
actionDelimiter,
|
||||
actionDomainSeparator,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TAttachment } from 'librechat-data-provider';
|
||||
import { useLocalize, useProgress } from '~/hooks';
|
||||
import { AttachmentGroup } from './Parts';
|
||||
|
|
@ -36,9 +41,9 @@ export default function ToolCall({
|
|||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const prevShowInfoRef = useRef<boolean>(showInfo);
|
||||
|
||||
const { function_name, domain, isMCPToolCall } = useMemo(() => {
|
||||
const { function_name, domain, isMCPToolCall, mcpServerName } = useMemo(() => {
|
||||
if (typeof name !== 'string') {
|
||||
return { function_name: '', domain: null, isMCPToolCall: false };
|
||||
return { function_name: '', domain: null, isMCPToolCall: false, mcpServerName: '' };
|
||||
}
|
||||
if (name.includes(Constants.mcp_delimiter)) {
|
||||
const [func, server] = name.split(Constants.mcp_delimiter);
|
||||
|
|
@ -46,6 +51,7 @@ export default function ToolCall({
|
|||
function_name: func || '',
|
||||
domain: server && (server.replaceAll(actionDomainSeparator, '.') || null),
|
||||
isMCPToolCall: true,
|
||||
mcpServerName: server || '',
|
||||
};
|
||||
}
|
||||
const [func, _domain] = name.includes(actionDelimiter)
|
||||
|
|
@ -55,9 +61,40 @@ export default function ToolCall({
|
|||
function_name: func || '',
|
||||
domain: _domain && (_domain.replaceAll(actionDomainSeparator, '.') || null),
|
||||
isMCPToolCall: false,
|
||||
mcpServerName: '',
|
||||
};
|
||||
}, [name]);
|
||||
|
||||
const actionId = useMemo(() => {
|
||||
if (isMCPToolCall || !auth) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const url = new URL(auth);
|
||||
const redirectUri = url.searchParams.get('redirect_uri') || '';
|
||||
const match = redirectUri.match(/\/api\/actions\/([^/]+)\/oauth\/callback/);
|
||||
return match?.[1] || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}, [auth, isMCPToolCall]);
|
||||
|
||||
const handleOAuthClick = useCallback(async () => {
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isMCPToolCall && mcpServerName) {
|
||||
await dataService.bindMCPOAuth(mcpServerName);
|
||||
} else if (actionId) {
|
||||
await dataService.bindActionOAuth(actionId);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Failed to bind OAuth CSRF cookie', e);
|
||||
}
|
||||
window.open(auth, '_blank', 'noopener,noreferrer');
|
||||
}, [auth, isMCPToolCall, mcpServerName, actionId]);
|
||||
|
||||
const error =
|
||||
typeof output === 'string' && output.toLowerCase().includes('error processing tool');
|
||||
|
||||
|
|
@ -230,7 +267,7 @@ export default function ToolCall({
|
|||
className="font-mediu inline-flex items-center justify-center rounded-xl px-4 py-2 text-sm"
|
||||
variant="default"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => window.open(auth, '_blank', 'noopener,noreferrer')}
|
||||
onClick={handleOAuthClick}
|
||||
>
|
||||
{localize('com_ui_sign_in_to_domain', { 0: authDomain })}
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue