durable-mcp-oauth-data-integrity.test.mjs
75 lines 2.6 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * Phase A — durable MCP OAuth: data-integrity tier.
3 * Concurrent refresh (no double-spend); corrupt store fail-closed.
4 */
5
6 import { describe, it, afterEach } from 'node:test';
7 import assert from 'node:assert/strict';
8 import fs from 'node:fs/promises';
9 import path from 'node:path';
10 import {
11 createDurableMcpProvider,
12 mintMcpTokens,
13 createTempStrongStore,
14 } from './helpers/durable-mcp-oauth-harness.mjs';
15 import { loadRefreshRecords } from '../hub/gateway/refresh-token-store.mjs';
16
17 const cleanups = [];
18 afterEach(async () => {
19 while (cleanups.length) await cleanups.pop()();
20 });
21
22 describe('Phase A data-integrity — concurrent refresh + corrupt store', () => {
23 it('sequential rotate chain never double-spends the same presented token', async () => {
24 const { provider, cleanup } = await createDurableMcpProvider();
25 cleanups.push(cleanup);
26 const { client, tokens } = await mintMcpTokens(provider);
27 const first = tokens.refresh_token;
28 const second = (await provider.exchangeRefreshToken(client, first)).refresh_token;
29 const third = (await provider.exchangeRefreshToken(client, second)).refresh_token;
30 assert.ok(third);
31 assert.notEqual(third, second);
32 // Replaying an earlier link burns the family (reuse detection).
33 await assert.rejects(() => provider.exchangeRefreshToken(client, first), /reuse|revoked|Unknown/i);
34 await assert.rejects(() => provider.exchangeRefreshToken(client, third), /revoked|Unknown|reuse/i);
35 });
36
37 it('corrupt store file fails closed (empty records / no throw on load)', async () => {
38 const tmp = await createTempStrongStore();
39 cleanups.push(tmp.cleanup);
40 await fs.writeFile(path.join(tmp.dir, 'hosted_refresh_tokens.json'), '{not-json', 'utf8');
41 const records = await loadRefreshRecords();
42 assert.deepEqual(records, {});
43 });
44
45 it('malformed token entries are dropped on load (fail-closed)', async () => {
46 const tmp = await createTempStrongStore();
47 cleanups.push(tmp.cleanup);
48 await fs.writeFile(
49 path.join(tmp.dir, 'hosted_refresh_tokens.json'),
50 JSON.stringify({
51 tokens: {
52 good: {
53 sub: 'google:1',
54 family_id: 'f',
55 token_hash: 'abc',
56 created_at: 1,
57 expires_at: 9e15,
58 family_expires_at: 9e15,
59 rotated_to: null,
60 used_at: null,
61 revoked: false,
62 meta: {},
63 },
64 bad: { sub: 'x' },
65 alsoBad: null,
66 },
67 }),
68 'utf8'
69 );
70 const records = await loadRefreshRecords();
71 assert.ok(records.good);
72 assert.equal(records.bad, undefined);
73 assert.equal(records.alsoBad, undefined);
74 });
75 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago