Skip to content

Commit f58f9e4

Browse files
authored
fix: Cookie header forwarding for MCP server authentication (#1394)
* fix: cookie header forwarding for MCP server authentication - Add cookie header validation to auth middleware (x-forwarded-cookie and cookie) - Forward user session headers through A2A task metadata - Transform browser cookie header to x-forwarded-cookie for downstream forwarding - Include forwarded headers in MCP client cache key to prevent stale connections - Add header redaction for cookie and x-forwarded-cookie in loggers - Add security comment about not using debugLogger with sensitive headers * fix: add missing header method to mock context in A2A handler tests
1 parent c890035 commit f58f9e4

File tree

14 files changed

+162
-9
lines changed

14 files changed

+162
-9
lines changed

.changeset/vertical-green-boa.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@inkeep/agents-cli": patch
3+
"@inkeep/agents-core": patch
4+
"@inkeep/agents-manage-api": patch
5+
"@inkeep/agents-manage-ui": patch
6+
"@inkeep/agents-run-api": patch
7+
"@inkeep/agents-sdk": patch
8+
"@inkeep/create-agents": patch
9+
"@inkeep/ai-sdk-provider": patch
10+
---
11+
12+
Fix cookie header forwarding for MCP server authentication

agents-manage-api/src/middleware/auth.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,28 @@ export const apiKeyAuth = () =>
4949
return;
5050
}
5151

52-
// 2. Try to validate as a better-auth session token (from device authorization flow)
52+
// 2. Try to validate as a better-auth session token (from device authorization flow or cookie)
5353
const auth = c.get('auth');
5454
if (auth) {
5555
try {
5656
// Create headers with the Authorization header for bearer token validation
5757
const headers = new Headers();
5858
headers.set('Authorization', authHeader);
5959

60+
// Also include cookie for session validation - check x-forwarded-cookie first (from MCP/SDK calls)
61+
const forwardedCookie = c.req.header('x-forwarded-cookie');
62+
const cookie = c.req.header('cookie');
63+
if (forwardedCookie) {
64+
headers.set('cookie', forwardedCookie);
65+
logger.debug(
66+
{ source: 'x-forwarded-cookie' },
67+
'Using x-forwarded-cookie for session validation'
68+
);
69+
} else if (cookie) {
70+
headers.set('cookie', cookie);
71+
logger.debug({ source: 'cookie' }, 'Using cookie for session validation');
72+
}
73+
6074
const session = await auth.api.getSession({ headers });
6175

6276
if (session?.user) {

agents-manage-api/src/routes/mcp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ app.all('/', async (c) => {
4646

4747
// Create SDK with custom hooks
4848
// Note: hooks is passed as an extended option (not in SDKOptions type but accepted by ClientSDK)
49+
// SECURITY: Do not pass debugLogger - it would log all headers including sensitive auth cookies
4950
return new InkeepAgentsCore({
5051
serverURL: env.INKEEP_AGENTS_MANAGE_API_URL,
5152
hooks,

agents-manage-ui/src/lib/logger.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ const logger = pino({
1010
serializers: {
1111
obj: (value) => ({ ...value }),
1212
},
13-
redact: ['req.headers.authorization', 'req.headers["x-inkeep-admin-authentication"]'],
13+
redact: [
14+
'req.headers.authorization',
15+
'req.headers["x-inkeep-admin-authentication"]',
16+
'req.headers.cookie',
17+
'req.headers["x-forwarded-cookie"]',
18+
],
1419
// Only use pretty transport in development
1520
...(NODE_ENV === 'development' && {
1621
transport: {

agents-run-api/src/__tests__/a2a/handlers.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ describe('A2A Handlers', () => {
6666
req: {
6767
json: vi.fn(),
6868
param: vi.fn().mockReturnValue('test-agent'),
69+
header: vi.fn().mockReturnValue(undefined),
6970
},
7071
json: vi.fn().mockImplementation((data) => new Response(JSON.stringify(data))),
7172
text: vi.fn().mockImplementation((text) => new Response(text)),

agents-run-api/src/a2a/handlers.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,21 @@ async function handleMessageSend(
9090
const executionContext = getRequestExecutionContext(c);
9191
const { agentId } = executionContext;
9292

93+
// Extract forwarded headers from the request (passed from ExecutionHandler via A2AClient)
94+
// Transform cookie -> x-forwarded-cookie for downstream forwarding
95+
const forwardedHeaders: Record<string, string> = {};
96+
const xForwardedCookie = c.req.header('x-forwarded-cookie');
97+
const authorization = c.req.header('authorization');
98+
const cookie = c.req.header('cookie');
99+
100+
// Priority: x-forwarded-cookie (explicit) > cookie (browser-sent)
101+
if (xForwardedCookie) {
102+
forwardedHeaders['x-forwarded-cookie'] = xForwardedCookie;
103+
} else if (cookie) {
104+
forwardedHeaders['x-forwarded-cookie'] = cookie;
105+
}
106+
if (authorization) forwardedHeaders.authorization = authorization;
107+
93108
const task: A2ATask = {
94109
id: generateId(),
95110
input: {
@@ -105,6 +120,8 @@ async function handleMessageSend(
105120
blocking: params.configuration?.blocking ?? false,
106121
custom: { agent_id: agentId || '' },
107122
...params.message.metadata,
123+
// Pass forwarded headers to taskHandler for MCP server authentication
124+
forwardedHeaders: Object.keys(forwardedHeaders).length > 0 ? forwardedHeaders : undefined,
108125
},
109126
},
110127
};
@@ -421,6 +438,21 @@ async function handleMessageStream(
421438
} satisfies JsonRpcResponse);
422439
}
423440

441+
// Extract forwarded headers from the request (passed from ExecutionHandler via A2AClient)
442+
// Transform cookie -> x-forwarded-cookie for downstream forwarding
443+
const forwardedHeaders: Record<string, string> = {};
444+
const xForwardedCookie = c.req.header('x-forwarded-cookie');
445+
const authorization = c.req.header('authorization');
446+
const cookie = c.req.header('cookie');
447+
448+
// Priority: x-forwarded-cookie (explicit) > cookie (browser-sent)
449+
if (xForwardedCookie) {
450+
forwardedHeaders['x-forwarded-cookie'] = xForwardedCookie;
451+
} else if (cookie) {
452+
forwardedHeaders['x-forwarded-cookie'] = cookie;
453+
}
454+
if (authorization) forwardedHeaders.authorization = authorization;
455+
424456
const task: A2ATask = {
425457
id: generateId(),
426458
input: {
@@ -435,6 +467,8 @@ async function handleMessageStream(
435467
metadata: {
436468
blocking: false, // Streaming is always non-blocking
437469
custom: { agent_id: agentId || '' },
470+
// Pass forwarded headers to taskHandler for MCP server authentication
471+
forwardedHeaders: Object.keys(forwardedHeaders).length > 0 ? forwardedHeaders : undefined,
438472
},
439473
},
440474
};

agents-run-api/src/agents/Agent.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ export type AgentConfig = {
145145
sandboxConfig?: SandboxConfig;
146146
/** User ID for user-scoped credential lookup (from temp JWT) */
147147
userId?: string;
148+
/** Headers to forward to MCP servers (e.g., x-forwarded-cookie for user session auth) */
149+
forwardedHeaders?: Record<string, string>;
148150
};
149151

150152
export type ExternalAgentRelationConfig = {
@@ -954,7 +956,12 @@ export class Agent {
954956
}
955957

956958
async getMcpTool(tool: McpTool) {
957-
const cacheKey = `${this.config.tenantId}-${this.config.projectId}-${tool.id}-${tool.credentialReferenceId || 'no-cred'}`;
959+
// Include forwarded headers hash in cache key to ensure user session-specific connections
960+
// This prevents reusing a connection created without cookies for requests that have them
961+
const forwardedHeadersHash = this.config.forwardedHeaders
962+
? Object.keys(this.config.forwardedHeaders).sort().join(',')
963+
: 'no-fwd';
964+
const cacheKey = `${this.config.tenantId}-${this.config.projectId}-${tool.id}-${tool.credentialReferenceId || 'no-cred'}-${forwardedHeadersHash}`;
958965

959966
const credentialReferenceId = tool.credentialReferenceId;
960967

@@ -1098,12 +1105,21 @@ export class Agent {
10981105
serverConfig.url = urlObj.toString();
10991106
}
11001107

1108+
// Merge forwarded headers (user session auth) into server config
1109+
if (this.config.forwardedHeaders && Object.keys(this.config.forwardedHeaders).length > 0) {
1110+
serverConfig.headers = {
1111+
...serverConfig.headers,
1112+
...this.config.forwardedHeaders,
1113+
};
1114+
}
1115+
11011116
logger.info(
11021117
{
11031118
toolName: tool.name,
11041119
credentialReferenceId,
11051120
transportType: serverConfig.type,
11061121
headers: tool.headers,
1122+
hasForwardedHeaders: !!this.config.forwardedHeaders,
11071123
},
11081124
'Built MCP server config with credentials'
11091125
);

agents-run-api/src/agents/generateTaskHandler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ export const createTaskHandler = (
7272
};
7373
}
7474

75+
// Extract forwarded headers from task metadata (passed from A2A handlers)
76+
const forwardedHeaders = task.context?.metadata?.forwardedHeaders as
77+
| Record<string, string>
78+
| undefined;
79+
7580
const [
7681
internalRelations,
7782
externalRelations,
@@ -494,6 +499,7 @@ export const createTaskHandler = (
494499
contextConfigId: config.contextConfigId || undefined,
495500
conversationHistoryConfig: config.conversationHistoryConfig,
496501
sandboxConfig: config.sandboxConfig,
502+
forwardedHeaders,
497503
},
498504
credentialStoreRegistry
499505
);

agents-run-api/src/handlers/executionHandler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ interface ExecutionHandlerParams {
3737
requestId: string;
3838
sseHelper: StreamHelper;
3939
emitOperations?: boolean;
40+
/** Headers to forward to MCP servers (e.g., x-forwarded-cookie for auth) */
41+
forwardedHeaders?: Record<string, string>;
4042
}
4143

4244
interface ExecutionResult {
@@ -72,6 +74,7 @@ export class ExecutionHandler {
7274
requestId,
7375
sseHelper,
7476
emitOperations,
77+
forwardedHeaders,
7578
} = params;
7679

7780
const { tenantId, projectId, agentId, apiKey, baseUrl } = executionContext;
@@ -262,6 +265,8 @@ export class ExecutionHandler {
262265
'x-inkeep-project-id': projectId,
263266
'x-inkeep-agent-id': agentId,
264267
'x-inkeep-sub-agent-id': currentAgentId,
268+
// Forward user session headers for MCP tool authentication
269+
...(forwardedHeaders || {}),
265270
},
266271
});
267272

agents-run-api/src/routes/chat.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,22 @@ app.openapi(chatCompletionsRoute, async (c) => {
364364
const emitOperationsHeader = c.req.header('x-emit-operations');
365365
const emitOperations = emitOperationsHeader === 'true';
366366

367+
// Extract headers to forward to MCP servers (for user session auth)
368+
// Transform cookie -> x-forwarded-cookie since downstream services expect it
369+
const forwardedHeaders: Record<string, string> = {};
370+
const xForwardedCookie = c.req.header('x-forwarded-cookie');
371+
const authorization = c.req.header('authorization');
372+
const cookie = c.req.header('cookie');
373+
374+
// Priority: x-forwarded-cookie (explicit) > cookie (browser-sent)
375+
// Transform cookie to x-forwarded-cookie for downstream forwarding
376+
if (xForwardedCookie) {
377+
forwardedHeaders['x-forwarded-cookie'] = xForwardedCookie;
378+
} else if (cookie) {
379+
forwardedHeaders['x-forwarded-cookie'] = cookie;
380+
}
381+
if (authorization) forwardedHeaders.authorization = authorization;
382+
367383
const executionHandler = new ExecutionHandler();
368384
const result = await executionHandler.execute({
369385
executionContext,
@@ -373,6 +389,7 @@ app.openapi(chatCompletionsRoute, async (c) => {
373389
requestId,
374390
sseHelper,
375391
emitOperations,
392+
forwardedHeaders,
376393
});
377394

378395
logger.info(

0 commit comments

Comments
 (0)