Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions backend/src/middleware/__tests__/requireDeploymentAuth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';

const getById = vi.fn();
const verifyApiKey = vi.fn();

vi.mock('../../repositories/deploymentRepository.js', () => ({
createDeploymentRepository: () => ({ getById }),
verifyApiKey: (...args: unknown[]) => verifyApiKey(...args),
}));

vi.mock('../../repositories/projectRepository.js', () => ({
getProjectRepository: () => ({}),
}));

vi.mock('../../services/authService.js', () => ({
authService: { verifyAccessToken: vi.fn() },
}));

vi.mock('../resourceOwnership.js', () => ({
verifyProjectOwnership: vi.fn(),
}));

import { requireDeploymentAuth } from '../requireDeploymentAuth.js';

function mockRes() {
const res: any = {};
res.status = vi.fn().mockReturnValue(res);
res.json = vi.fn().mockReturnValue(res);
return res;
}

describe('requireDeploymentAuth enumeration resistance', () => {
beforeEach(() => {
getById.mockReset();
verifyApiKey.mockReset();
});

it('returns 404 Not found for missing deployment', async () => {
getById.mockResolvedValue(null);
const req: any = { params: { deploymentId: 'missing' }, headers: {} };
const res = mockRes();
const next = vi.fn();
await requireDeploymentAuth(req, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: 'Not found' });
expect(next).not.toHaveBeenCalled();
});

it('returns the same 404 Not found for a bad API key', async () => {
getById.mockResolvedValue({ deploymentId: 'dep-1', projectId: 'p1' });
verifyApiKey.mockResolvedValue(null);
const req: any = {
params: { deploymentId: 'dep-1' },
headers: { 'x-api-key': 'bad-key' },
};
const res = mockRes();
const next = vi.fn();
await requireDeploymentAuth(req, res, next);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: 'Not found' });
expect(next).not.toHaveBeenCalled();
});
});
3 changes: 2 additions & 1 deletion backend/src/middleware/requireDeploymentAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ export async function requireDeploymentAuth(
const apiKey = req.headers['x-api-key'] as string | undefined;
if (apiKey) {
const keyRecord = await verifyApiKey(apiKey, deploymentRepo);
// Same 404 body as a missing deployment so API keys cannot enumerate IDs.
if (!keyRecord || keyRecord.deploymentId !== deployment.deploymentId) {
res.status(403).json({ error: 'Forbidden' });
res.status(404).json({ error: 'Not found' });
return;
}
req.deployment = deployment;
Expand Down