Skip to content

Commit 5ffcd83

Browse files
author
David Ruzicka
committed
test(upstream-auth): add tests for effective upstream auth handling and bearer auth without value_from_env
1 parent 71b42ef commit 5ffcd83

3 files changed

Lines changed: 119 additions & 4 deletions

File tree

src/mcp/mcp-server.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4837,6 +4837,106 @@ paths:
48374837
});
48384838
});
48394839

4840+
// -------------------------------------------------------------------------
4841+
describe('getEffectiveUpstreamAuth', () => {
4842+
it('skips oauth and returns bearer when interceptors.auth = [oauth, bearer]', async () => {
4843+
const provider = { ...upstreamProvider }; // no provider.auth
4844+
(upstreamServer as any).profile.interceptors = {
4845+
auth: [
4846+
{ type: 'oauth' },
4847+
{ type: 'bearer', value_from_env: 'BEARER_TOKEN' },
4848+
],
4849+
};
4850+
(upstreamServer as any).httpTransport = {
4851+
...(upstreamServer as any).httpTransport,
4852+
getUpstreamMcpConfig: () => provider,
4853+
getSessionToken: () => 'client-token',
4854+
};
4855+
try {
4856+
await (upstreamServer as any).handleOtherRequest(
4857+
{ jsonrpc: '2.0', id: '1', method: 'tools/list', params: {} },
4858+
'session-123',
4859+
'upstream-profile',
4860+
);
4861+
// effectiveAuth should be bearer; effectiveProvider passed to client should have auth.type === 'bearer'
4862+
expect(mockGetUpstreamClient).toHaveBeenCalledWith(
4863+
'session-123',
4864+
expect.objectContaining({ auth: expect.objectContaining({ type: 'bearer' }) }),
4865+
'client-token',
4866+
);
4867+
} finally {
4868+
delete (upstreamServer as any).profile.interceptors;
4869+
}
4870+
});
4871+
4872+
it('returns undefined when interceptors.auth = [session-cookie] only', () => {
4873+
const server = new MCPServer();
4874+
(server as any).profile = {
4875+
profile_name: 'p',
4876+
tools: [],
4877+
interceptors: { auth: [{ type: 'session-cookie' }] },
4878+
};
4879+
const result = (server as any).getEffectiveUpstreamAuth({ name: 'x', transport: { type: 'http-streamable', url: 'https://example.com' } });
4880+
expect(result).toBeUndefined();
4881+
});
4882+
4883+
it('priority sort: lower priority value wins — query(priority:1) before bearer(priority:5)', () => {
4884+
const server = new MCPServer();
4885+
(server as any).profile = {
4886+
profile_name: 'p',
4887+
tools: [],
4888+
interceptors: {
4889+
auth: [
4890+
{ type: 'bearer', value_from_env: 'T', priority: 5 },
4891+
{ type: 'query', value_from_env: 'T', query_param: 'token', priority: 1 },
4892+
],
4893+
},
4894+
};
4895+
const result = (server as any).getEffectiveUpstreamAuth({ name: 'x', transport: { type: 'http-streamable', url: 'https://example.com' } });
4896+
expect(result?.type).toBe('query');
4897+
});
4898+
4899+
it('uses provider.auth directly and ignores interceptors.auth when provider.auth set', () => {
4900+
const server = new MCPServer();
4901+
(server as any).profile = {
4902+
profile_name: 'p',
4903+
tools: [],
4904+
interceptors: { auth: [{ type: 'bearer', value_from_env: 'INTERCEPTOR_TOKEN' }] },
4905+
};
4906+
const providerAuth = { type: 'bearer' as const, value_from_env: 'PROVIDER_TOKEN' };
4907+
const result = (server as any).getEffectiveUpstreamAuth({
4908+
name: 'x',
4909+
transport: { type: 'http-streamable', url: 'https://example.com' },
4910+
auth: providerAuth,
4911+
});
4912+
expect(result).toBe(providerAuth);
4913+
});
4914+
4915+
it('blocks anonymous HTTP session when interceptors.auth contains only oauth (gate bypass fix)', async () => {
4916+
const provider = { ...upstreamProvider }; // no provider.auth
4917+
(upstreamServer as any).profile.interceptors = {
4918+
auth: [{ type: 'oauth' }],
4919+
};
4920+
(upstreamServer as any).httpTransport = {
4921+
...(upstreamServer as any).httpTransport,
4922+
getUpstreamMcpConfig: () => provider,
4923+
getSessionToken: () => undefined, // anonymous HTTP session
4924+
};
4925+
try {
4926+
const response = await (upstreamServer as any).handleOtherRequest(
4927+
{ jsonrpc: '2.0', id: '1', method: 'tools/list', params: {} },
4928+
'session-123',
4929+
'upstream-profile',
4930+
) as any;
4931+
expect(mockGetUpstreamClient).not.toHaveBeenCalled();
4932+
expect(response.error).toBeDefined();
4933+
expect(response.error.code).toBe(-32603);
4934+
} finally {
4935+
delete (upstreamServer as any).profile.interceptors;
4936+
}
4937+
});
4938+
});
4939+
48404940
// -------------------------------------------------------------------------
48414941
describe('tool_prefix warning', () => {
48424942
it('emits a warning when tool_prefix is configured', async () => {

src/mcp/mcp-server.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1848,8 +1848,8 @@ export class MCPServer {
18481848
if (!raw) return undefined;
18491849
const configs = Array.isArray(raw) ? raw : [raw];
18501850
const sorted = [...configs].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
1851-
const selected = sorted.find(c => !['oauth', 'session-cookie'].includes(c.type)) ?? sorted[0];
1852-
if (!selected || !['bearer', 'query', 'custom-header'].includes(selected.type)) return undefined;
1851+
const selected = sorted.find(c => ['bearer', 'query', 'custom-header'].includes(c.type));
1852+
if (!selected) return undefined;
18531853
return {
18541854
type: selected.type as UpstreamMcpAuthConfig['type'],
18551855
header_name: selected.header_name,
@@ -1877,8 +1877,14 @@ export class MCPServer {
18771877
if (this.httpTransport && sessionId && profileId) {
18781878
const sessionToken = this.httpTransport.getSessionToken(profileId, sessionId);
18791879
if (sessionToken) return sessionToken;
1880-
// Reject anonymous HTTP sessions when auth is configured (directly or inherited)
1881-
if (effectiveAuth) {
1880+
// Reject anonymous HTTP sessions when ANY auth is configured (directly or inherited).
1881+
// effectiveAuth is undefined for oauth/session-cookie-only interceptors (those types can't
1882+
// be forwarded), but the presence of auth config still signals the endpoint must be protected.
1883+
const interceptorsAuth = this.profile?.interceptors?.auth;
1884+
const hasAnyInterceptorsAuth = Array.isArray(interceptorsAuth)
1885+
? interceptorsAuth.length > 0
1886+
: !!interceptorsAuth;
1887+
if (provider.auth || hasAnyInterceptorsAuth) {
18821888
throw new UpstreamConnectionError(
18831889
'upstream_mcp proxy requires an authenticated HTTP session — ' +
18841890
'the inbound client must supply a verified token.',

src/profile/upstream-mcp-config.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ describe('resolveUpstreamMcpConfig – validator error branches', () => {
9898
expect(() => resolveUpstreamMcpConfig(profile)).toThrow(/value_from_env must not be empty/);
9999
});
100100

101+
it('accepts bearer auth without value_from_env (HTTP session-passthrough)', () => {
102+
const profile = makeProfile({
103+
name: 'p1',
104+
transport: { type: 'http-streamable', url: 'https://example.com/mcp' },
105+
auth: { type: 'bearer' },
106+
});
107+
expect(() => resolveUpstreamMcpConfig(profile)).not.toThrow();
108+
});
109+
101110
it('rejects custom-header auth with unsafe header_name', () => {
102111
const profile = makeProfile({
103112
name: 'p1',

0 commit comments

Comments
 (0)