phase8-p1b-no-secrets-and-fail-closed.test.mjs
112 lines 4.1 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * Phase 8 P1b — security tier: no secrets in logs, timing-safe, OAuth fail-closed (§9).
3 */
4
5 import { describe, test, before, after } from 'node:test';
6 import assert from 'node:assert/strict';
7 import fs from 'fs';
8 import path from 'path';
9 import {
10 makeTempDataDir,
11 enableOfflineLockedTestEnv,
12 createLocalAuthTestApp,
13 bootstrapTestAdmin,
14 TEST_PASSPHRASE,
15 TEST_SECRET,
16 } from '../helpers/phase8-p1b-local-auth-harness.mjs';
17 import { generateSetupToken } from '../../hub/lib/local-auth-bootstrap.mjs';
18 import { credentialsPath } from '../../hub/lib/local-auth.mjs';
19
20 describe('phase8-p1b no secrets and fail-closed (security)', () => {
21 /** @type {{ dataDir: string, cleanup: () => void }} */
22 let tmp;
23 /** @type {{ baseUrl: string, close: () => Promise<void> } | null} */
24 let hub = null;
25 const setupToken = 'test-setup-token-value-never-log';
26 let originalFetch;
27
28 before(async () => {
29 tmp = await makeTempDataDir();
30 enableOfflineLockedTestEnv(tmp.dataDir);
31 await bootstrapTestAdmin(tmp.dataDir);
32 hub = await createLocalAuthTestApp(tmp.dataDir);
33 originalFetch = globalThis.fetch;
34 });
35
36 after(async () => {
37 if (hub) await hub.close();
38 globalThis.fetch = originalFetch;
39 tmp.cleanup();
40 delete process.env.KNOWTATION_OFFLINE_LOCKED_AUTH;
41 delete process.env.KNOWTATION_OFFLINE_LOCKED_AUTH_TEST_SHIPPED;
42 });
43
44 test('audit log contains no passphrase, hash, setup token, or JWT', async () => {
45 await fetch(`${hub.baseUrl}/api/v1/auth/local/login`, {
46 method: 'POST',
47 headers: { 'Content-Type': 'application/json' },
48 body: JSON.stringify({ username: 'admin', passphrase: TEST_PASSPHRASE }),
49 });
50 const logPath = path.join(tmp.dataDir, 'hub_audit.log');
51 assert.ok(fs.existsSync(logPath));
52 const log = fs.readFileSync(logPath, 'utf8');
53 const store = JSON.parse(fs.readFileSync(credentialsPath(tmp.dataDir), 'utf8'));
54 const hash = store.credentials['local:admin_001'].argon2id;
55 assert.ok(!log.includes(TEST_PASSPHRASE));
56 assert.ok(!log.includes(hash));
57 assert.ok(!log.includes(setupToken));
58 assert.ok(!log.match(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/));
59 });
60
61 test('timing-safe username lookup within ±10ms over samples', async () => {
62 const samples = 20;
63 const unknownTimes = [];
64 const badPassTimes = [];
65 for (let i = 0; i < samples; i++) {
66 const t0 = performance.now();
67 await fetch(`${hub.baseUrl}/api/v1/auth/local/login`, {
68 method: 'POST',
69 headers: { 'Content-Type': 'application/json' },
70 body: JSON.stringify({ username: 'nosuchuser' + i, passphrase: 'WrongPass123!X' }),
71 });
72 unknownTimes.push(performance.now() - t0);
73 const t1 = performance.now();
74 await fetch(`${hub.baseUrl}/api/v1/auth/local/login`, {
75 method: 'POST',
76 headers: { 'Content-Type': 'application/json' },
77 body: JSON.stringify({ username: 'admin', passphrase: 'WrongPass123!X' }),
78 });
79 badPassTimes.push(performance.now() - t1);
80 }
81 const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
82 const diff = Math.abs(avg(unknownTimes) - avg(badPassTimes));
83 assert.ok(diff <= 10 || diff <= avg(badPassTimes) * 0.5, `timing diff ${diff}ms too large`);
84 });
85
86 test('OAuth fail-closed: fetch never called with OAuth provider URL', async () => {
87 let oauthFetchCalled = false;
88 globalThis.fetch = async (input) => {
89 const url = typeof input === 'string' ? input : input.url;
90 let host = '';
91 try {
92 host = new URL(url).hostname;
93 } catch (_) {
94 /* relative URL */
95 }
96 if (host === 'accounts.google.com' || host === 'github.com') {
97 oauthFetchCalled = true;
98 throw new Error('OAuth egress');
99 }
100 return originalFetch(input);
101 };
102 const res = await fetch(`${hub.baseUrl}/auth/login?provider=google`);
103 assert.equal(res.status, 403);
104 assert.equal(oauthFetchCalled, false);
105 globalThis.fetch = originalFetch;
106 });
107
108 test('hub_local_credentials.json mode 0600 after write', () => {
109 const st = fs.statSync(credentialsPath(tmp.dataDir));
110 assert.equal(st.mode & 0o777, 0o600);
111 });
112 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago