[grade=A urn:ump:5bm325dgncwgwsl2joztkf4br3wh5lh6jam3rn32fvyr62zyoysa] fix(auth): machine credentials (dk_ keys, JWTs) dead under TOTP — audit P0-1
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.
This commit is contained in:
2 files changed
+136
-1
No files matched your search
@@ -528,7 +528,7 @@ module.exports = function configureMiddleware(app, {
|
||||
}
|
||||
|
||||
// ── TOTP auth middleware ──
|
||||
const totpAuthMiddleware = (req, res, next) => {
|
||||
const totpAuthMiddleware = async (req, res, next) => {
|
||||
// If TOTP is not enabled at all, skip auth entirely — this is the initial-setup state
|
||||
if (!totpConfig.enabled) {
|
||||
req.auth = {
|
||||
@@ -542,6 +542,32 @@ module.exports = function configureMiddleware(app, {
|
||||
if (isPublicRoute(req)) return next();
|
||||
if (isSessionValid(req)) return next();
|
||||
|
||||
// Audit P0-1 (2026-10-27): this gate previously answered 401 before the
|
||||
// JWT/API-key middleware below ever ran, so machine credentials (dk_
|
||||
// keys, JWTs) were dead on every TOTP-protected install — live-verified
|
||||
// with a freshly minted key getting 401 on /api/v1/mcp/info. Verify them
|
||||
// HERE before rejecting; the gate below passes req.auth through.
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
try {
|
||||
const jwtPayload = await authManager.verifyJWT(authHeader.substring(7));
|
||||
if (jwtPayload) {
|
||||
req.auth = { type: 'jwt', userId: jwtPayload.userId, scope: jwtPayload.scope || [] };
|
||||
return next();
|
||||
}
|
||||
} catch (_) { /* invalid JWT — fall through to the API-key check */ }
|
||||
}
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
if (apiKey) {
|
||||
try {
|
||||
const keyData = await authManager.verifyAPIKey(apiKey);
|
||||
if (keyData) {
|
||||
req.auth = { type: 'apikey', keyId: keyData.keyId, name: keyData.name, scope: keyData.scopes || [] };
|
||||
return next();
|
||||
}
|
||||
} catch (_) { /* invalid key — fall through to 401 */ }
|
||||
}
|
||||
|
||||
return errorResponse(res, 401, '[DC-110] Authentication required', { requiresTotp: true });
|
||||
};
|
||||
|
||||
@@ -549,6 +575,11 @@ module.exports = function configureMiddleware(app, {
|
||||
|
||||
// ── JWT/API Key authentication middleware ──
|
||||
const jwtApiKeyAuthMiddleware = async (req, res, next) => {
|
||||
// Audit P0-1 (2026-10-27): the TOTP gate (registered above) may have
|
||||
// already authenticated this request via its machine-credential path —
|
||||
// pass through instead of re-verifying the same credential.
|
||||
if (req.auth && (req.auth.type === 'jwt' || req.auth.type === 'apikey')) return next();
|
||||
|
||||
if (req.totpSessionValid || isSessionValid(req)) {
|
||||
req.auth = {
|
||||
type: 'session',
|
||||
|
||||
Reference in new issue
Block a user