durable-mcp-oauth-unit.test.mjs
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf
mirror: GitHub Phase A durable MCP OAuth (#270)
Human
minor
⚠ breaking
10 days ago
| 1 | /** |
| 2 | * Phase A — durable MCP OAuth: unit tier. |
| 3 | * TTL alignment, hash-at-rest via refresh-token-core store, agent meta, provider wiring. |
| 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 jwt from 'jsonwebtoken'; |
| 11 | import { |
| 12 | createDurableMcpProvider, |
| 13 | mintMcpTokens, |
| 14 | TEST_SECRET, |
| 15 | } from './helpers/durable-mcp-oauth-harness.mjs'; |
| 16 | import { |
| 17 | DEFAULT_TOKEN_TTL_MS, |
| 18 | DEFAULT_FAMILY_TTL_MS, |
| 19 | sanitizeMeta, |
| 20 | mergeMeta, |
| 21 | } from '../hub/lib/refresh-token-core.mjs'; |
| 22 | import { |
| 23 | MCP_TOKEN_EXPIRY_SECONDS, |
| 24 | resolveMcpAgentLabel, |
| 25 | KnowtationOAuthProvider, |
| 26 | } from '../hub/gateway/mcp-oauth-provider.mjs'; |
| 27 | import { |
| 28 | subFromVerifiedPayload, |
| 29 | mcpScopesPermitMethod, |
| 30 | shouldMountDurableAgentAuth, |
| 31 | } from '../hub/gateway/access-token-authz.mjs'; |
| 32 | |
| 33 | const cleanups = []; |
| 34 | afterEach(async () => { |
| 35 | while (cleanups.length) { |
| 36 | const fn = cleanups.pop(); |
| 37 | await fn(); |
| 38 | } |
| 39 | }); |
| 40 | |
| 41 | describe('Phase A unit — MCP durable refresh', () => { |
| 42 | it('access TTL is ≤ 1h and refresh TTLs match refresh-token-core defaults', () => { |
| 43 | assert.equal(MCP_TOKEN_EXPIRY_SECONDS, 3600); |
| 44 | assert.equal(DEFAULT_TOKEN_TTL_MS, 30 * 24 * 60 * 60 * 1000); |
| 45 | assert.equal(DEFAULT_FAMILY_TTL_MS, 90 * 24 * 60 * 60 * 1000); |
| 46 | }); |
| 47 | |
| 48 | it('sanitizeMeta keeps agent / client_id / scopes and drops unknown fields', () => { |
| 49 | const meta = sanitizeMeta({ |
| 50 | agent: 'Hermes-bf', |
| 51 | client_id: 'abc', |
| 52 | scopes: 'vault:read vault:write', |
| 53 | evil: { nested: true }, |
| 54 | ua: 'x'.repeat(400), |
| 55 | }); |
| 56 | assert.equal(meta.agent, 'Hermes-bf'); |
| 57 | assert.equal(meta.client_id, 'abc'); |
| 58 | assert.equal(meta.scopes, 'vault:read vault:write'); |
| 59 | assert.equal(meta.evil, undefined); |
| 60 | assert.equal(meta.ua.length, 256); |
| 61 | }); |
| 62 | |
| 63 | it('mergeMeta preserves agent across rotate when incoming meta is empty', () => { |
| 64 | const merged = mergeMeta({ agent: 'A', client_id: 'c1', scopes: 'vault:read' }, {}); |
| 65 | assert.equal(merged.agent, 'A'); |
| 66 | assert.equal(merged.client_id, 'c1'); |
| 67 | }); |
| 68 | |
| 69 | it('resolveMcpAgentLabel prefers client_name then client_id', () => { |
| 70 | assert.equal(resolveMcpAgentLabel({ client_name: 'Hermes', client_id: 'id1' }), 'Hermes'); |
| 71 | assert.equal(resolveMcpAgentLabel({ client_id: 'id1' }), 'id1'); |
| 72 | assert.equal(resolveMcpAgentLabel({ client_id: 'id1' }, 'override'), 'override'); |
| 73 | }); |
| 74 | |
| 75 | it('KnowtationOAuthProvider requires refreshStore', () => { |
| 76 | assert.throws( |
| 77 | () => new KnowtationOAuthProvider({ sessionSecret: TEST_SECRET, baseUrl: 'http://localhost:3340' }), |
| 78 | /refreshStore/ |
| 79 | ); |
| 80 | }); |
| 81 | |
| 82 | it('issued refresh is hash-at-rest (raw secret absent from store file)', async () => { |
| 83 | const { provider, cleanup, dir } = await createDurableMcpProvider(); |
| 84 | cleanups.push(cleanup); |
| 85 | const { tokens } = await mintMcpTokens(provider, { clientName: 'Cursor IDE' }); |
| 86 | const secret = tokens.refresh_token.split('.')[1]; |
| 87 | const raw = await fs.readFile(path.join(dir, 'hosted_refresh_tokens.json'), 'utf8'); |
| 88 | assert.ok(!raw.includes(secret), 'raw refresh secret must not appear in store file'); |
| 89 | assert.ok(!raw.includes(tokens.access_token), 'access JWT must not be persisted in refresh store'); |
| 90 | const payload = jwt.verify(tokens.access_token, TEST_SECRET); |
| 91 | assert.equal(payload.type, 'mcp_access'); |
| 92 | assert.equal(payload.scopes[0], 'vault:read'); |
| 93 | }); |
| 94 | |
| 95 | it('refresh record meta includes agent label from client_name', async () => { |
| 96 | const { provider, store, cleanup } = await createDurableMcpProvider(); |
| 97 | cleanups.push(cleanup); |
| 98 | const { tokens } = await mintMcpTokens(provider, { clientName: 'Hermes-cofounder-1' }); |
| 99 | const peeked = await store.peek(tokens.refresh_token); |
| 100 | assert.ok(peeked); |
| 101 | assert.equal(peeked.meta.agent, 'Hermes-cofounder-1'); |
| 102 | assert.ok(peeked.meta.client_id); |
| 103 | assert.equal(peeked.meta.scopes, 'vault:read'); |
| 104 | }); |
| 105 | }); |
| 106 | |
| 107 | describe('Phase A unit — MCP scope REST guard helpers', () => { |
| 108 | it('vault:read permits GET but not POST', () => { |
| 109 | assert.equal(mcpScopesPermitMethod(['vault:read'], 'GET'), true); |
| 110 | assert.equal(mcpScopesPermitMethod(['vault:read'], 'POST'), false); |
| 111 | assert.equal(mcpScopesPermitMethod(['vault:write'], 'POST'), true); |
| 112 | }); |
| 113 | |
| 114 | it('subFromVerifiedPayload blocks mcp_access vault:read on mutating methods', () => { |
| 115 | const payload = { sub: 'google:1', type: 'mcp_access', scopes: ['vault:read'] }; |
| 116 | assert.equal(subFromVerifiedPayload(payload, { method: 'GET' }), 'google:1'); |
| 117 | assert.equal(subFromVerifiedPayload(payload, { method: 'POST' }), null); |
| 118 | }); |
| 119 | |
| 120 | it('web session JWT (no mcp_access type) is not scope-blocked here', () => { |
| 121 | const payload = { sub: 'google:1', role: 'member' }; |
| 122 | assert.equal(subFromVerifiedPayload(payload, { method: 'POST' }), 'google:1'); |
| 123 | }); |
| 124 | |
| 125 | it('shouldMountDurableAgentAuth is false under offline-lock or Netlify', () => { |
| 126 | assert.equal(shouldMountDurableAgentAuth({ |
| 127 | sessionSecret: 'x', netlify: false, offlineLockedActive: false, |
| 128 | }), true); |
| 129 | assert.equal(shouldMountDurableAgentAuth({ |
| 130 | sessionSecret: 'x', netlify: false, offlineLockedActive: true, |
| 131 | }), false); |
| 132 | assert.equal(shouldMountDurableAgentAuth({ |
| 133 | sessionSecret: 'x', netlify: true, offlineLockedActive: false, |
| 134 | }), false); |
| 135 | }); |
| 136 | }); |
File History
1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf
mirror: GitHub Phase A durable MCP OAuth (#270)
Human
minor
⚠
10 days ago