phase8-p1b-local-credential-store.test.mjs
117 lines 4.3 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * Phase 8 P1b — unit tier: credential store, Argon2id, issueLocalToken (§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 jwt from 'jsonwebtoken';
10 import {
11 normalizeUsername,
12 hashPassphrase,
13 verifyPassphrase,
14 parsePhcParams,
15 assertArgon2ParamsFloor,
16 loadCredentialStore,
17 saveCredentialStore,
18 createLocalCredential,
19 issueLocalToken,
20 credentialsPath,
21 DECOY_ARGON2_PHC,
22 chmod0600,
23 } from '../../hub/lib/local-auth.mjs';
24 import { resolveLocalAuthRole } from '../../hub/lib/local-auth-role.mjs';
25 import { makeTempDataDir, TEST_PASSPHRASE, TEST_SECRET } from '../helpers/phase8-p1b-local-auth-harness.mjs';
26
27 describe('phase8-p1b local credential store (unit)', () => {
28 /** @type {{ dataDir: string, cleanup: () => void }} */
29 let tmp;
30
31 before(async () => {
32 tmp = await makeTempDataDir();
33 });
34
35 after(() => tmp.cleanup());
36
37 test('normalizeUsername: NFC, trim, lowercase-fold', () => {
38 assert.equal(normalizeUsername(' Admin '), 'admin');
39 assert.equal(normalizeUsername('café'), normalizeUsername('café'));
40 });
41
42 test('Argon2id hash/verify round-trip', async () => {
43 const phc = await hashPassphrase(TEST_PASSPHRASE);
44 assert.ok(phc.startsWith('$argon2id$'));
45 assert.equal(await verifyPassphrase(phc, TEST_PASSPHRASE), true);
46 assert.equal(await verifyPassphrase(phc, 'wrong-passphrase'), false);
47 });
48
49 test('PHC parse enforces parameter floors', () => {
50 const phc = '$argon2id$v=19$m=65536,t=3,p=4$salt$hash';
51 assert.equal(parsePhcParams(phc).ok, true);
52 assert.equal(assertArgon2ParamsFloor({ timeCost: 2, memoryCost: 65536, parallelism: 4 }), false);
53 assert.equal(assertArgon2ParamsFloor({ timeCost: 3, memoryCost: 32768, parallelism: 4 }), false);
54 assert.equal(assertArgon2ParamsFloor({ timeCost: 3, memoryCost: 65536, parallelism: 2 }), false);
55 });
56
57 test('credential store CRUD writes 0600 perms', async () => {
58 await createLocalCredential(tmp.dataDir, 'admin', TEST_PASSPHRASE, { role: 'admin' });
59 const st = fs.statSync(credentialsPath(tmp.dataDir));
60 assert.equal(st.mode & 0o777, 0o600);
61 const store = loadCredentialStore(tmp.dataDir);
62 assert.equal(Object.keys(store.credentials).length, 1);
63 saveCredentialStore(tmp.dataDir, store);
64 assert.equal(fs.statSync(credentialsPath(tmp.dataDir)).mode & 0o777, 0o600);
65 });
66
67 test('issueLocalToken payload matches OAuth shape with provider local', async () => {
68 const { userId } = await createLocalCredential(tmp.dataDir, 'user2', TEST_PASSPHRASE, {
69 role: 'admin',
70 });
71 const store = loadCredentialStore(tmp.dataDir);
72 const cred = store.credentials[`local:${userId}`];
73 const token = issueLocalToken(cred, 'admin', TEST_SECRET, '24h');
74 const payload = jwt.verify(token, TEST_SECRET);
75 assert.equal(payload.sub, `local:${userId}`);
76 assert.equal(payload.provider, 'local');
77 assert.equal(payload.id, userId);
78 assert.equal(payload.name, cred.username);
79 assert.equal(payload.role, 'admin');
80 });
81
82 test('timing-safe verify uses decoy when username absent', async () => {
83 const t0 = Date.now();
84 await verifyPassphrase(null, 'missing-user-pass');
85 const t1 = Date.now();
86 const phc = await hashPassphrase(TEST_PASSPHRASE);
87 const t2 = Date.now();
88 await verifyPassphrase(phc, 'bad');
89 const t3 = Date.now();
90 assert.ok(t1 - t0 > 0);
91 assert.ok(t3 - t2 > 0);
92 assert.equal(DECOY_ARGON2_PHC.startsWith('$argon2id$'), true);
93 });
94
95 test('effectiveRole parity: local sub resolves admin; empty store denied when offline-locked', async () => {
96 const sub = 'local:admin_001';
97 const role = resolveLocalAuthRole(tmp.dataDir, sub, { offlineLockedActive: true });
98 assert.equal(role, 'admin');
99 const emptyDenied = resolveLocalAuthRole(tmp.dataDir, 'local:nobody', {
100 offlineLockedActive: true,
101 });
102 assert.equal(emptyDenied, 'member');
103 const empty = await makeTempDataDir();
104 const legacyAdmin = resolveLocalAuthRole(empty.dataDir, 'google:1', {
105 offlineLockedActive: false,
106 });
107 assert.equal(legacyAdmin, 'admin');
108 empty.cleanup();
109 });
110
111 test('chmod0600 helper', () => {
112 const f = path.join(tmp.dataDir, 'perm-test.json');
113 fs.writeFileSync(f, '{}');
114 chmod0600(f);
115 assert.equal(fs.statSync(f).mode & 0o777, 0o600);
116 });
117 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago