mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 17:30:16 +01:00
🔐 feat: Add API key authentication support for MCP servers (#10936)
* 🔐 feat: Add API key authentication support for MCP servers
* Chore: Copilot comments fixes
---------
Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
This commit is contained in:
parent
abeaab6e17
commit
e15d37b399
11 changed files with 836 additions and 84 deletions
|
|
@ -28,6 +28,7 @@ enum AuthorizationTypeEnum {
|
|||
export interface AuthConfig {
|
||||
auth_type?: AuthTypeEnum;
|
||||
api_key?: string;
|
||||
api_key_source?: 'admin' | 'user'; // Whether admin provides key for all or each user provides their own
|
||||
api_key_authorization_type?: AuthorizationTypeEnum;
|
||||
api_key_custom_header?: string;
|
||||
oauth_client_id?: string;
|
||||
|
|
@ -171,8 +172,6 @@ export default function MCPAuth({
|
|||
{localize('com_ui_none')}
|
||||
</label>
|
||||
</div>
|
||||
{/*
|
||||
TODO Support API keys for auth
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="auth-apikey" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
|
|
@ -189,7 +188,7 @@ export default function MCPAuth({
|
|||
</RadioGroup.Item>
|
||||
{localize('com_ui_api_key')}
|
||||
</label>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="auth-oauth" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
|
|
@ -228,21 +227,84 @@ export default function MCPAuth({
|
|||
const ApiKey = ({ inputClasses }: { inputClasses: string }) => {
|
||||
const localize = useLocalize();
|
||||
const { register, watch, setValue } = useFormContext();
|
||||
const authorization_type = watch('api_key_authorization_type') || AuthorizationTypeEnum.Basic;
|
||||
const api_key_source = watch('api_key_source') || 'admin';
|
||||
const authorization_type = watch('api_key_authorization_type') || AuthorizationTypeEnum.Bearer;
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_api_key')}</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className={inputClasses}
|
||||
{...register('api_key')}
|
||||
/>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_auth_type')}</label>
|
||||
{/* API Key Source selection */}
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_api_key_source')}</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthorizationTypeEnum.Basic}
|
||||
defaultValue="admin"
|
||||
onValueChange={(value) => setValue('api_key_source', value)}
|
||||
value={api_key_source}
|
||||
role="radiogroup"
|
||||
aria-required="true"
|
||||
dir="ltr"
|
||||
className="mb-3 flex flex-col gap-2"
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="source-admin" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value="admin"
|
||||
id="source-admin"
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
{localize('com_ui_admin_provides_key')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="source-user" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value="user"
|
||||
id="source-user"
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
{localize('com_ui_user_provides_key')}
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
|
||||
{/* API Key input - only show for admin-provided mode */}
|
||||
{api_key_source === 'admin' && (
|
||||
<>
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_api_key')}</label>
|
||||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className={inputClasses}
|
||||
{...register('api_key')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* User-provided mode info */}
|
||||
{api_key_source === 'user' && (
|
||||
<div className="mb-3 rounded-lg border border-border-medium bg-surface-secondary p-3">
|
||||
<p className="text-sm text-text-secondary">{localize('com_ui_user_provides_key_note')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header Format selection - shown for both modes */}
|
||||
<label className="mb-1 block text-sm font-medium">{localize('com_ui_header_format')}</label>
|
||||
<RadioGroup.Root
|
||||
defaultValue={AuthorizationTypeEnum.Bearer}
|
||||
onValueChange={(value) => setValue('api_key_authorization_type', value)}
|
||||
value={authorization_type}
|
||||
role="radiogroup"
|
||||
|
|
@ -251,23 +313,6 @@ const ApiKey = ({ inputClasses }: { inputClasses: string }) => {
|
|||
className="mb-2 flex gap-6 overflow-hidden rounded-lg"
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="auth-basic" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthorizationTypeEnum.Basic}
|
||||
id="auth-basic"
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
{localize('com_ui_basic')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="auth-bearer" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
|
|
@ -285,6 +330,23 @@ const ApiKey = ({ inputClasses }: { inputClasses: string }) => {
|
|||
{localize('com_ui_bearer')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="auth-basic" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
type="button"
|
||||
role="radio"
|
||||
value={AuthorizationTypeEnum.Basic}
|
||||
id="auth-basic"
|
||||
className={cn(
|
||||
'mr-1 flex h-5 w-5 items-center justify-center rounded-full border',
|
||||
'border-border-heavy bg-surface-primary',
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-text-primary" />
|
||||
</RadioGroup.Item>
|
||||
{localize('com_ui_basic')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="auth-custom" className="flex cursor-pointer items-center gap-1">
|
||||
<RadioGroup.Item
|
||||
|
|
|
|||
|
|
@ -85,10 +85,13 @@ export default function MCPServerDialog({
|
|||
let authType: AuthTypeEnum = AuthTypeEnum.None;
|
||||
if (server.config.oauth) {
|
||||
authType = AuthTypeEnum.OAuth;
|
||||
} else if ('api_key' in server.config) {
|
||||
} else if ('apiKey' in server.config && server.config.apiKey) {
|
||||
authType = AuthTypeEnum.ServiceHttp;
|
||||
}
|
||||
|
||||
// Extract apiKey config if present
|
||||
const apiKeyConfig = 'apiKey' in server.config ? server.config.apiKey : undefined;
|
||||
|
||||
return {
|
||||
title: server.config.title || '',
|
||||
description: server.config.description || '',
|
||||
|
|
@ -97,9 +100,12 @@ export default function MCPServerDialog({
|
|||
icon: server.config.iconPath || '',
|
||||
auth: {
|
||||
auth_type: authType,
|
||||
api_key: '',
|
||||
api_key_authorization_type: AuthorizationTypeEnum.Basic,
|
||||
api_key_custom_header: '',
|
||||
api_key: '', // NEVER pre-fill secrets
|
||||
api_key_source: (apiKeyConfig?.source as 'admin' | 'user') || 'admin',
|
||||
api_key_authorization_type:
|
||||
(apiKeyConfig?.authorization_type as AuthorizationTypeEnum) ||
|
||||
AuthorizationTypeEnum.Bearer,
|
||||
api_key_custom_header: apiKeyConfig?.custom_header || '',
|
||||
oauth_client_id: server.config.oauth?.client_id || '',
|
||||
oauth_client_secret: '', // NEVER pre-fill secrets
|
||||
oauth_authorization_url: server.config.oauth?.authorization_url || '',
|
||||
|
|
@ -119,7 +125,8 @@ export default function MCPServerDialog({
|
|||
auth: {
|
||||
auth_type: AuthTypeEnum.None,
|
||||
api_key: '',
|
||||
api_key_authorization_type: AuthorizationTypeEnum.Basic,
|
||||
api_key_source: 'admin',
|
||||
api_key_authorization_type: AuthorizationTypeEnum.Bearer,
|
||||
api_key_custom_header: '',
|
||||
oauth_client_id: '',
|
||||
oauth_client_secret: '',
|
||||
|
|
@ -251,6 +258,22 @@ export default function MCPServerDialog({
|
|||
}
|
||||
}
|
||||
|
||||
// Add API Key if auth type is service_http
|
||||
if (formData.auth.auth_type === AuthTypeEnum.ServiceHttp) {
|
||||
const source = formData.auth.api_key_source || 'admin';
|
||||
const authorizationType = formData.auth.api_key_authorization_type || 'bearer';
|
||||
|
||||
config.apiKey = {
|
||||
source,
|
||||
authorization_type: authorizationType,
|
||||
...(source === 'admin' && formData.auth.api_key && { key: formData.auth.api_key }),
|
||||
...(authorizationType === 'custom' &&
|
||||
formData.auth.api_key_custom_header && {
|
||||
custom_header: formData.auth.api_key_custom_header,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const params: MCPServerCreateParams = {
|
||||
config,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue