With TOTP enabled, totpAuthMiddleware answered 401 before
jwtApiKeyAuthMiddleware ever ran: API keys and JWTs were rejected on
every TOTP-protected install (live-verified during the 2026-10-27
sweep with a freshly minted dk_ key getting 401 on /api/v1/mcp/info).
MCP-over-HTTP ('Connect your AI'), fleet endpoints and every machine
integration were broken in production.
Fix: the TOTP gate now verifies Bearer JWT then x-api-key itself
(try/catch, fall-through to the unchanged 401) and sets req.auth;
the JWT/API-key gate passes through on already-set machine auth
(no double verification). Session/public/TOTP-disabled paths
byte-identical.
New chain-level test (no req.auth injection — the gap that hid this
bug) boots the real configureMiddleware chain; VERIFIED it fails 2/5
with the fix stashed and passes 5/5 with it. Judge polish (call-count
assertion) noted as optional follow-up.
Full suite: 172/172 suites, 3731/3731 tests.
105 lines
4.1 KiB
JavaScript
105 lines
4.1 KiB
JavaScript
/**
|
|
* Audit P0-1 (2026-10-27): with TOTP enabled, totpAuthMiddleware answered 401
|
|
* BEFORE jwtApiKeyAuthMiddleware ever ran, so machine credentials (dk_ API
|
|
* keys, JWTs) were dead on every TOTP-protected install — live-verified with
|
|
* a freshly minted key getting 401 on /api/v1/mcp/info. The route-level tests
|
|
* missed it because they inject req.auth directly instead of exercising the
|
|
* real chain.
|
|
*
|
|
* This suite boots the REAL configureMiddleware chain (both auth gates in
|
|
* registration order) and drives synthetic requests through it — the test the
|
|
* codebase was missing. No req.auth injection anywhere.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const express = require('express');
|
|
const request = require('supertest');
|
|
const crypto = require('crypto');
|
|
const configureMiddleware = require('../../src/utilities/middleware');
|
|
|
|
const KEY = Buffer.alloc(32, 7);
|
|
|
|
function makeDeps() {
|
|
return {
|
|
siteConfig: { dashboardHost: 'status.sami', tld: '.sami' },
|
|
totpConfig: { enabled: true, sessionDuration: '24h' },
|
|
tailscaleConfig: { enabled: false, requireAuth: false },
|
|
metrics: { recordRequest: jest.fn() },
|
|
auditLogger: { middleware: () => (_req, _res, next) => next() },
|
|
authManager: {
|
|
verifyJWT: jest.fn(async (token) =>
|
|
token === 'good.jwt' ? { userId: 'u1', scope: ['read'] } : null),
|
|
verifyAPIKey: jest.fn(async (key) =>
|
|
key === 'dk_goodkey'
|
|
? { keyId: 'k1', name: 'test-key', scopes: ['read', 'write'] }
|
|
: null),
|
|
},
|
|
log: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
cryptoUtils: { loadOrCreateKey: () => KEY },
|
|
isValidContainerId: () => true,
|
|
isTailscaleIP: () => false,
|
|
getTailscaleStatus: async () => null,
|
|
};
|
|
}
|
|
|
|
// Mint a session cookie exactly the way setSessionCookie does, so the
|
|
// session path of the chain is exercised with real HMAC math too.
|
|
function mintSessionCookie() {
|
|
const payload = { v: true, exp: Date.now() + 86400000 };
|
|
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
const sig = crypto.createHmac('sha256', KEY).update(payloadB64).digest('base64url');
|
|
return `dashcaddy_session=${payloadB64}.${sig}`;
|
|
}
|
|
|
|
function buildApp() {
|
|
const app = express();
|
|
configureMiddleware(app, makeDeps());
|
|
// Probe route registered AFTER the middleware chain, like a real mount.
|
|
// NOT in PUBLIC_ROUTES — it must go through the full chain.
|
|
app.get('/api/v1/mcp/info', (req, res) => res.json({ ok: true, auth: req.auth }));
|
|
return app;
|
|
}
|
|
|
|
describe('middleware chain: machine credentials pass the TOTP gate (audit P0-1)', () => {
|
|
test('valid x-api-key reaches the route when TOTP is enabled', async () => {
|
|
const res = await request(buildApp())
|
|
.get('/api/v1/mcp/info')
|
|
.set('x-api-key', 'dk_goodkey');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.ok).toBe(true);
|
|
expect(res.body.auth).toMatchObject({ type: 'apikey', keyId: 'k1', name: 'test-key' });
|
|
});
|
|
|
|
test('valid Bearer JWT reaches the route when TOTP is enabled', async () => {
|
|
const res = await request(buildApp())
|
|
.get('/api/v1/mcp/info')
|
|
.set('Authorization', 'Bearer good.jwt');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.auth).toMatchObject({ type: 'jwt', userId: 'u1' });
|
|
});
|
|
|
|
test('INVALID api key is still rejected with requiresTotp', async () => {
|
|
const res = await request(buildApp())
|
|
.get('/api/v1/mcp/info')
|
|
.set('x-api-key', 'dk_wrong');
|
|
expect(res.status).toBe(401);
|
|
expect(res.body.error).toContain('DC-110');
|
|
expect(res.body.requiresTotp).toBe(true);
|
|
});
|
|
|
|
test('no credentials at all is still rejected with requiresTotp', async () => {
|
|
const res = await request(buildApp()).get('/api/v1/mcp/info');
|
|
expect(res.status).toBe(401);
|
|
expect(res.body.requiresTotp).toBe(true);
|
|
});
|
|
|
|
test('valid session cookie still authenticates (session path unchanged)', async () => {
|
|
const res = await request(buildApp())
|
|
.get('/api/v1/mcp/info')
|
|
.set('Cookie', mintSessionCookie());
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.auth).toMatchObject({ type: 'session', scope: ['admin'] });
|
|
});
|
|
});
|