feat(vscode-ide-companion): add auth token validation to IDE server (#8491)

This commit is contained in:
Shreya Keshive
2025-09-15 18:49:15 -04:00
committed by GitHub
parent c4c81e3d3b
commit 12f584fff8
3 changed files with 188 additions and 26 deletions
@@ -12,6 +12,10 @@ import * as path from 'node:path';
import { IDEServer } from './ide-server.js';
import type { DiffManager } from './diff-manager.js';
vi.mock('node:crypto', () => ({
randomUUID: vi.fn(() => 'test-auth-token'),
}));
const mocks = vi.hoisted(() => ({
diffManager: {
onDidChange: vi.fn(() => ({ dispose: vi.fn() })),
@@ -21,6 +25,7 @@ const mocks = vi.hoisted(() => ({
vi.mock('node:fs/promises', () => ({
writeFile: vi.fn(() => Promise.resolve(undefined)),
unlink: vi.fn(() => Promise.resolve(undefined)),
chmod: vi.fn(() => Promise.resolve(undefined)),
}));
vi.mock('node:os', async (importOriginal) => {
@@ -134,6 +139,7 @@ describe('IDEServer', () => {
port: parseInt(port, 10),
workspacePath: expectedWorkspacePaths,
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
@@ -143,6 +149,8 @@ describe('IDEServer', () => {
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
});
it('should set a single folder path', async () => {
@@ -169,6 +177,7 @@ describe('IDEServer', () => {
port: parseInt(port, 10),
workspacePath: '/foo/bar',
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
@@ -178,6 +187,8 @@ describe('IDEServer', () => {
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
});
it('should set an empty string if no folders are open', async () => {
@@ -204,6 +215,7 @@ describe('IDEServer', () => {
port: parseInt(port, 10),
workspacePath: '',
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
@@ -213,6 +225,8 @@ describe('IDEServer', () => {
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
});
it('should update the path when workspace folders change', async () => {
@@ -253,6 +267,7 @@ describe('IDEServer', () => {
port: parseInt(port, 10),
workspacePath: expectedWorkspacePaths,
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
@@ -262,6 +277,8 @@ describe('IDEServer', () => {
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
// Simulate removing a folder
vscodeMock.workspace.workspaceFolders = [{ uri: { fsPath: '/baz/qux' } }];
@@ -275,6 +292,7 @@ describe('IDEServer', () => {
port: parseInt(port, 10),
workspacePath: '/baz/qux',
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
@@ -284,6 +302,8 @@ describe('IDEServer', () => {
expectedPpidPortFile,
expectedContent2,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
});
it('should clear env vars and delete port file on stop', async () => {
@@ -335,6 +355,7 @@ describe('IDEServer', () => {
port: parseInt(port, 10),
workspacePath: expectedWorkspacePaths,
ppid: process.ppid,
authToken: 'test-auth-token',
});
expect(fs.writeFile).toHaveBeenCalledWith(
expectedPortFile,
@@ -344,6 +365,94 @@ describe('IDEServer', () => {
expectedPpidPortFile,
expectedContent,
);
expect(fs.chmod).toHaveBeenCalledWith(expectedPortFile, 0o600);
expect(fs.chmod).toHaveBeenCalledWith(expectedPpidPortFile, 0o600);
},
);
describe('auth token', () => {
let port: number;
beforeEach(async () => {
await ideServer.start(mockContext);
port = (ideServer as unknown as { port: number }).port;
});
it('should allow request without auth token for backwards compatibility', async () => {
const response = await fetch(`http://localhost:${port}/mcp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: {},
id: 1,
}),
});
expect(response.status).not.toBe(401);
});
it('should allow request with valid auth token', async () => {
const response = await fetch(`http://localhost:${port}/mcp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer test-auth-token`,
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: {},
id: 1,
}),
});
expect(response.status).not.toBe(401);
});
it('should reject request with invalid auth token', async () => {
const response = await fetch(`http://localhost:${port}/mcp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer invalid-token',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: {},
id: 1,
}),
});
expect(response.status).toBe(401);
const body = await response.text();
expect(body).toBe('Unauthorized');
});
it('should reject request with malformed auth token', async () => {
const malformedHeaders = [
'Bearer',
'invalid-token',
'Bearer token extra',
];
for (const header of malformedHeaders) {
const response = await fetch(`http://localhost:${port}/mcp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: header,
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: {},
id: 1,
}),
});
expect(response.status, `Failed for header: ${header}`).toBe(401);
const body = await response.text();
expect(body, `Failed for header: ${header}`).toBe('Unauthorized');
}
});
});
});