refactor: use fixed localhost-only CORS policy, make auth mandatory

This commit is contained in:
2025-10-25 21:15:11 -04:00
parent 5ce7488597
commit f34dd31ed3
5 changed files with 467 additions and 68 deletions

250
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"@types/express": "^4.17.21",
"@types/jest": "^30.0.0",
"@types/node": "^16.11.6",
"@types/supertest": "^6.0.3",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
@@ -25,6 +26,7 @@
"esbuild": "0.17.3",
"jest": "^30.2.0",
"obsidian": "latest",
"supertest": "^7.1.4",
"ts-jest": "^29.4.5",
"tslib": "2.4.0",
"typescript": "4.7.4"
@@ -1799,6 +1801,19 @@
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1837,6 +1852,16 @@
"node": ">= 8"
}
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
"integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -2014,6 +2039,13 @@
"@types/node": "*"
}
},
"node_modules/@types/cookiejar": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
@@ -2126,6 +2158,13 @@
"@types/node": "*"
}
},
"node_modules/@types/methods": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
"integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -2204,6 +2243,30 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/superagent": {
"version": "8.1.9",
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz",
"integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/cookiejar": "^2.1.5",
"@types/methods": "^1.1.4",
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/supertest": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz",
"integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/methods": "^1.1.4",
"@types/superagent": "^8.1.0"
}
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
@@ -2859,6 +2922,20 @@
"node": ">=8"
}
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true,
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/babel-jest": {
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
@@ -3374,6 +3451,29 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/component-emitter": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3424,6 +3524,13 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookiejar": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
"dev": true,
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@@ -3588,6 +3695,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -3625,6 +3742,17 @@
"license": "MIT",
"optional": true
},
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"dev": true,
"license": "ISC",
"dependencies": {
"asap": "^2.0.0",
"wrappy": "1"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -3804,6 +3932,22 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
@@ -4336,6 +4480,13 @@
"license": "MIT",
"peer": true
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"dev": true,
"license": "MIT"
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -4498,6 +4649,41 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formidable": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"dezalgo": "^1.0.4",
"once": "^1.4.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -4871,6 +5057,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -7558,6 +7760,54 @@
"node": ">= 8.0"
}
},
"node_modules/superagent": {
"version": "10.2.3",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz",
"integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==",
"dev": true,
"license": "MIT",
"dependencies": {
"component-emitter": "^1.3.1",
"cookiejar": "^2.1.4",
"debug": "^4.3.7",
"fast-safe-stringify": "^2.1.1",
"form-data": "^4.0.4",
"formidable": "^3.5.4",
"methods": "^1.1.2",
"mime": "2.6.0",
"qs": "^6.11.2"
},
"engines": {
"node": ">=14.18.0"
}
},
"node_modules/superagent/node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"dev": true,
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/supertest": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz",
"integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"methods": "^1.1.2",
"superagent": "^10.2.3"
},
"engines": {
"node": ">=14.18.0"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",

View File

@@ -30,6 +30,7 @@
"@types/express": "^4.17.21",
"@types/jest": "^30.0.0",
"@types/node": "^16.11.6",
"@types/supertest": "^6.0.3",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
@@ -37,6 +38,7 @@
"esbuild": "0.17.3",
"jest": "^30.2.0",
"obsidian": "latest",
"supertest": "^7.1.4",
"ts-jest": "^29.4.5",
"tslib": "2.4.0",
"typescript": "4.7.4"

View File

@@ -8,52 +8,51 @@ export function setupMiddleware(app: Express, settings: MCPServerSettings, creat
// Parse JSON bodies
app.use(express.json());
// CORS configuration
if (settings.enableCORS) {
const corsOptions = {
origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
// Allow requests with no origin (like mobile apps or curl requests)
if (!origin) return callback(null, true);
if (settings.allowedOrigins.includes('*') ||
settings.allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
};
app.use(cors(corsOptions));
}
// CORS configuration - Always enabled with fixed localhost-only policy
const corsOptions = {
origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
// Allow requests with no origin (like CLI clients, curl, MCP SDKs)
if (!origin) {
return callback(null, true);
}
// Authentication middleware
if (settings.enableAuth) {
app.use((req: Request, res: Response, next: any) => {
// Defensive check: if auth is enabled but no API key is set, reject all requests
if (!settings.apiKey || settings.apiKey.trim() === '') {
return res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Server misconfigured: Authentication enabled but no API key set'));
// Allow localhost and 127.0.0.1 on any port, both HTTP and HTTPS
const localhostRegex = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
if (localhostRegex.test(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
const authHeader = req.headers.authorization;
const apiKey = authHeader?.replace('Bearer ', '');
if (apiKey !== settings.apiKey) {
return res.status(401).json(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Unauthorized'));
}
next();
});
}
},
credentials: true
};
app.use(cors(corsOptions));
// Authentication middleware - Always enabled
app.use((req: Request, res: Response, next: any) => {
// Defensive check: if no API key is set, reject all requests
if (!settings.apiKey || settings.apiKey.trim() === '') {
return res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Server misconfigured: No API key set'));
}
const authHeader = req.headers.authorization;
const providedKey = authHeader?.replace('Bearer ', '');
if (providedKey !== settings.apiKey) {
return res.status(401).json(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Unauthorized'));
}
next();
});
// Origin validation for security (DNS rebinding protection)
app.use((req: Request, res: Response, next: any) => {
const host = req.headers.host;
// Only allow localhost connections
if (host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
return res.status(403).json(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Only localhost connections allowed'));
}
next();
});
}

View File

@@ -57,38 +57,6 @@ export class MCPServerSettingTab extends PluginSettingTab {
}
}));
// CORS setting
new Setting(containerEl)
.setName('Enable CORS')
.setDesc('Enable Cross-Origin Resource Sharing (requires restart)')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableCORS)
.onChange(async (value) => {
this.plugin.settings.enableCORS = value;
await this.plugin.saveSettings();
if (this.plugin.mcpServer?.isRunning()) {
new Notice('⚠️ Server restart required for CORS changes to take effect');
}
}));
// Allowed origins
new Setting(containerEl)
.setName('Allowed origins')
.setDesc('Comma-separated list of allowed origins (* for all, requires restart)')
.addText(text => text
.setPlaceholder('*')
.setValue(this.plugin.settings.allowedOrigins.join(', '))
.onChange(async (value) => {
this.plugin.settings.allowedOrigins = value
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
await this.plugin.saveSettings();
if (this.plugin.mcpServer?.isRunning()) {
new Notice('⚠️ Server restart required for origin changes to take effect');
}
}));
// Authentication
new Setting(containerEl)
.setName('Enable authentication')

180
tests/middleware.test.ts Normal file
View File

@@ -0,0 +1,180 @@
import express, { Express } from 'express';
import request from 'supertest';
import { setupMiddleware } from '../src/server/middleware';
import { MCPServerSettings } from '../src/types/settings-types';
import { ErrorCodes } from '../src/types/mcp-types';
describe('Middleware', () => {
let app: Express;
const mockCreateError = jest.fn((id, code, message) => ({
jsonrpc: '2.0',
id,
error: { code, message }
}));
const createTestSettings = (overrides?: Partial<MCPServerSettings>): MCPServerSettings => ({
port: 3000,
apiKey: 'test-api-key-12345',
enableAuth: true,
...overrides
});
beforeEach(() => {
app = express();
mockCreateError.mockClear();
});
describe('CORS', () => {
it('should allow localhost origin on any port', async () => {
setupMiddleware(app, createTestSettings(), mockCreateError);
app.get('/test', (req, res) => res.json({ ok: true }));
const response = await request(app)
.get('/test')
.set('Origin', 'http://localhost:8080')
.set('Host', 'localhost:3000')
.set('Authorization', 'Bearer test-api-key-12345');
expect(response.headers['access-control-allow-origin']).toBe('http://localhost:8080');
});
it('should allow 127.0.0.1 origin on any port', async () => {
setupMiddleware(app, createTestSettings(), mockCreateError);
app.get('/test', (req, res) => res.json({ ok: true }));
const response = await request(app)
.get('/test')
.set('Origin', 'http://127.0.0.1:9000')
.set('Host', '127.0.0.1:3000')
.set('Authorization', 'Bearer test-api-key-12345');
expect(response.headers['access-control-allow-origin']).toBe('http://127.0.0.1:9000');
});
it('should allow https localhost origins', async () => {
setupMiddleware(app, createTestSettings(), mockCreateError);
app.get('/test', (req, res) => res.json({ ok: true }));
const response = await request(app)
.get('/test')
.set('Origin', 'https://localhost:443')
.set('Host', 'localhost:3000')
.set('Authorization', 'Bearer test-api-key-12345');
expect(response.headers['access-control-allow-origin']).toBe('https://localhost:443');
});
it('should reject non-localhost origins', async () => {
setupMiddleware(app, createTestSettings(), mockCreateError);
app.get('/test', (req, res) => res.json({ ok: true }));
const response = await request(app)
.get('/test')
.set('Origin', 'http://evil.com')
.set('Host', 'localhost:3000')
.set('Authorization', 'Bearer test-api-key-12345');
expect(response.status).toBe(500); // CORS error
});
it('should allow requests with no origin (CLI clients)', async () => {
setupMiddleware(app, createTestSettings(), mockCreateError);
app.get('/test', (req, res) => res.json({ ok: true }));
const response = await request(app)
.get('/test')
.set('Host', 'localhost:3000')
.set('Authorization', 'Bearer test-api-key-12345');
expect(response.status).toBe(200);
});
});
describe('Authentication', () => {
it('should require Bearer token when auth enabled', async () => {
setupMiddleware(app, createTestSettings({ enableAuth: true }), mockCreateError);
app.post('/mcp', (req, res) => res.json({ ok: true }));
const response = await request(app)
.post('/mcp')
.set('Host', 'localhost:3000');
expect(response.status).toBe(401);
});
it('should accept valid Bearer token', async () => {
setupMiddleware(app, createTestSettings({ enableAuth: true, apiKey: 'secret123' }), mockCreateError);
app.post('/mcp', (req, res) => res.json({ ok: true }));
const response = await request(app)
.post('/mcp')
.set('Host', 'localhost:3000')
.set('Authorization', 'Bearer secret123');
expect(response.status).toBe(200);
});
it('should reject invalid Bearer token', async () => {
setupMiddleware(app, createTestSettings({ enableAuth: true, apiKey: 'secret123' }), mockCreateError);
app.post('/mcp', (req, res) => res.json({ ok: true }));
const response = await request(app)
.post('/mcp')
.set('Host', 'localhost:3000')
.set('Authorization', 'Bearer wrong-token');
expect(response.status).toBe(401);
});
it('should reject requests when API key is empty', async () => {
setupMiddleware(app, createTestSettings({ apiKey: '' }), mockCreateError);
app.post('/mcp', (req, res) => res.json({ ok: true }));
const response = await request(app)
.post('/mcp')
.set('Host', 'localhost:3000')
.set('Authorization', 'Bearer any-token');
expect(response.status).toBe(500);
expect(response.body.error.message).toContain('No API key set');
});
});
describe('Host validation', () => {
it('should allow localhost host header', async () => {
setupMiddleware(app, createTestSettings(), mockCreateError);
app.get('/test', (req, res) => res.json({ ok: true }));
const response = await request(app)
.get('/test')
.set('Host', 'localhost:3000')
.set('Authorization', 'Bearer test-api-key-12345');
expect(response.status).toBe(200);
});
it('should allow 127.0.0.1 host header', async () => {
setupMiddleware(app, createTestSettings(), mockCreateError);
app.get('/test', (req, res) => res.json({ ok: true }));
const response = await request(app)
.get('/test')
.set('Host', '127.0.0.1:3000')
.set('Authorization', 'Bearer test-api-key-12345');
expect(response.status).toBe(200);
});
it('should reject non-localhost host header', async () => {
setupMiddleware(app, createTestSettings(), mockCreateError);
app.get('/test', (req, res) => res.json({ ok: true }));
const response = await request(app)
.get('/test')
.set('Host', 'evil.com')
.set('Authorization', 'Bearer test-api-key-12345');
expect(response.status).toBe(403);
});
});
});