Session 10: Internal auth refactor, prefetch cascade keys, Sentry, welcome email (1286 tests)
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// internalAuth middleware (Session 10) — tests both the new short-form
|
||||
// header (x-internal-key) and the legacy long form
|
||||
// (X-VYNDR-Internal-Key), the timing-safe compare, and the optional
|
||||
// loopback restriction.
|
||||
|
||||
const { requireInternalAuth, __internals } = require('../../src/middleware/internalAuth');
|
||||
|
||||
function fakeReqRes({ headers = {}, ip = '127.0.0.1' } = {}) {
|
||||
const req = {
|
||||
headers: Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])),
|
||||
ip,
|
||||
socket: { remoteAddress: ip },
|
||||
get(name) {
|
||||
return this.headers[name.toLowerCase()];
|
||||
},
|
||||
};
|
||||
const res = {
|
||||
statusCode: null,
|
||||
body: null,
|
||||
status(code) { this.statusCode = code; return this; },
|
||||
json(body) { this.body = body; return this; },
|
||||
};
|
||||
return { req, res };
|
||||
}
|
||||
|
||||
function run(mw, req, res) {
|
||||
return new Promise((resolve) => {
|
||||
mw(req, res, () => resolve('next'));
|
||||
if (res.statusCode != null) resolve('blocked');
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.VYNDR_INTERNAL_KEY;
|
||||
});
|
||||
|
||||
describe('requireInternalAuth', () => {
|
||||
describe('configuration guard', () => {
|
||||
test('returns 503 when VYNDR_INTERNAL_KEY env var is unset', async () => {
|
||||
const mw = requireInternalAuth();
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'anything' } });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('blocked');
|
||||
expect(res.statusCode).toBe(503);
|
||||
expect(res.body.error).toMatch(/not configured/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('header compatibility', () => {
|
||||
beforeEach(() => { process.env.VYNDR_INTERNAL_KEY = 'real-key-12345'; });
|
||||
|
||||
test('accepts the new short-form header (x-internal-key)', async () => {
|
||||
const mw = requireInternalAuth();
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' } });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('next');
|
||||
});
|
||||
|
||||
test('accepts the legacy long-form header (X-VYNDR-Internal-Key)', async () => {
|
||||
const mw = requireInternalAuth();
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-vyndr-internal-key': 'real-key-12345' } });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('next');
|
||||
});
|
||||
|
||||
test('returns 401 on missing header', async () => {
|
||||
const mw = requireInternalAuth();
|
||||
const { req, res } = fakeReqRes({});
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('blocked');
|
||||
expect(res.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('returns 401 on wrong key (timing-safe)', async () => {
|
||||
const mw = requireInternalAuth();
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'wrong-key' } });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('blocked');
|
||||
expect(res.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('returns 401 even when prefix matches but suffix differs', async () => {
|
||||
// Guards against early-exit string compare leaking timing info.
|
||||
const mw = requireInternalAuth();
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-1234X' } });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('blocked');
|
||||
expect(res.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('returns 401 on length mismatch (avoids any timingSafeEqual throw)', async () => {
|
||||
const mw = requireInternalAuth();
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'short' } });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('blocked');
|
||||
expect(res.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loopbackOnly option', () => {
|
||||
beforeEach(() => { process.env.VYNDR_INTERNAL_KEY = 'real-key-12345'; });
|
||||
|
||||
test('passes from 127.0.0.1', async () => {
|
||||
const mw = requireInternalAuth({ loopbackOnly: true });
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '127.0.0.1' });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('next');
|
||||
});
|
||||
|
||||
test('passes from IPv6 loopback', async () => {
|
||||
const mw = requireInternalAuth({ loopbackOnly: true });
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '::1' });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('next');
|
||||
});
|
||||
|
||||
test('passes from IPv4-mapped IPv6 loopback', async () => {
|
||||
const mw = requireInternalAuth({ loopbackOnly: true });
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '::ffff:127.0.0.1' });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('next');
|
||||
});
|
||||
|
||||
test('returns 403 from off-host IP', async () => {
|
||||
const mw = requireInternalAuth({ loopbackOnly: true });
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '10.0.0.42' });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('blocked');
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(res.body.error).toMatch(/Origin not permitted/);
|
||||
});
|
||||
|
||||
test('WITHOUT loopbackOnly, off-host IPs are accepted (n8n case)', async () => {
|
||||
const mw = requireInternalAuth({ loopbackOnly: false });
|
||||
const { req, res } = fakeReqRes({ headers: { 'x-internal-key': 'real-key-12345' }, ip: '172.18.0.3' });
|
||||
const out = await run(mw, req, res);
|
||||
expect(out).toBe('next');
|
||||
});
|
||||
});
|
||||
|
||||
describe('timingSafeStringEqual', () => {
|
||||
const { timingSafeStringEqual } = __internals;
|
||||
|
||||
test('equal strings → true', () => {
|
||||
expect(timingSafeStringEqual('abc123', 'abc123')).toBe(true);
|
||||
});
|
||||
test('different strings same length → false', () => {
|
||||
expect(timingSafeStringEqual('abc123', 'abc124')).toBe(false);
|
||||
});
|
||||
test('different lengths → false (does not throw)', () => {
|
||||
expect(() => timingSafeStringEqual('a', 'abc')).not.toThrow();
|
||||
expect(timingSafeStringEqual('a', 'abc')).toBe(false);
|
||||
});
|
||||
test('non-string inputs → false', () => {
|
||||
expect(timingSafeStringEqual(null, 'a')).toBe(false);
|
||||
expect(timingSafeStringEqual('a', undefined)).toBe(false);
|
||||
expect(timingSafeStringEqual(123, '123')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user