device-oauth-integration.test.mjs
142 lines 4.6 KB
Raw
sha256:c2dbf04d56308f3bbf2d06e6d2eb022b8948b1e827195fe525a44e5e18d5f9c0 feat(auth): Phase B Connect cloud agent (RFC 8628) + Hermes… Human minor ⚠ breaking 11 days ago
1 /**
2 * Phase B — device OAuth: integration tier.
3 *
4 * HTTP authorize → approve → token poll; refresh rotation; setup-pack non-secrets;
5 * durability across a new router instance on the same data directory.
6 */
7
8 import { describe, it, afterEach } from 'node:test';
9 import assert from 'node:assert/strict';
10 import jwt from 'jsonwebtoken';
11 import { createGatewayRefreshStore } from '../hub/gateway/refresh-token-store.mjs';
12 import {
13 startDeviceOAuthApp,
14 authHeaders,
15 TEST_SECRET,
16 DEVICE_GRANT_TYPE,
17 createTempStrongStore,
18 } from './helpers/device-oauth-harness.mjs';
19
20 const cleanups = [];
21 afterEach(async () => {
22 while (cleanups.length) await cleanups.pop()();
23 });
24
25 describe('Phase B integration — HTTP device flow', () => {
26 it('authorize → approve → poll /token returns mcp_access + refresh', async () => {
27 const app = await startDeviceOAuthApp();
28 cleanups.push(async () => {
29 await app.stop();
30 await app.tmp.cleanup();
31 });
32
33 const authRes = await app.fetch('POST', `${app.mountPath}/authorize`, {
34 client_id: 'hermes-cloud',
35 client_name: 'Hermes Agent',
36 scope: 'vault:read vault:write',
37 });
38 assert.equal(authRes.status, 200);
39 assert.ok(authRes.json.device_code);
40 assert.ok(authRes.json.user_code);
41
42 const approveRes = await app.fetch(
43 'POST',
44 `${app.mountPath}/approve`,
45 { user_code: authRes.json.user_code },
46 authHeaders()
47 );
48 assert.equal(approveRes.status, 200);
49 assert.equal(approveRes.json.ok, true);
50
51 const tokenRes = await app.fetch('POST', `${app.mountPath}/token`, {
52 grant_type: DEVICE_GRANT_TYPE,
53 device_code: authRes.json.device_code,
54 });
55 assert.equal(tokenRes.status, 200);
56 assert.ok(tokenRes.json.access_token);
57 assert.ok(tokenRes.json.refresh_token);
58 assert.equal(tokenRes.json.token_type, 'bearer');
59
60 const payload = jwt.verify(tokenRes.json.access_token, TEST_SECRET);
61 assert.equal(payload.type, 'mcp_access');
62 assert.equal(payload.sub, 'google:device-test');
63 assert.ok(payload.scopes.includes('vault:read'));
64 });
65
66 it('refresh_token grant rotates refresh secret', async () => {
67 const app = await startDeviceOAuthApp();
68 cleanups.push(async () => {
69 await app.stop();
70 await app.tmp.cleanup();
71 });
72
73 const authRes = await app.fetch('POST', `${app.mountPath}/authorize`, {});
74 await app.fetch(
75 'POST',
76 `${app.mountPath}/approve`,
77 { user_code: authRes.json.user_code },
78 authHeaders()
79 );
80 const first = await app.fetch('POST', `${app.mountPath}/token`, {
81 grant_type: DEVICE_GRANT_TYPE,
82 device_code: authRes.json.device_code,
83 });
84 const refresh1 = first.json.refresh_token;
85
86 const rotated = await app.fetch('POST', `${app.mountPath}/token`, {
87 grant_type: 'refresh_token',
88 refresh_token: refresh1,
89 });
90 assert.equal(rotated.status, 200);
91 assert.ok(rotated.json.access_token);
92 assert.ok(rotated.json.refresh_token);
93 assert.notEqual(rotated.json.refresh_token, refresh1);
94 });
95
96 it('setup-pack returns secrets:false and no token fields', async () => {
97 const app = await startDeviceOAuthApp();
98 cleanups.push(async () => {
99 await app.stop();
100 await app.tmp.cleanup();
101 });
102
103 const res = await app.fetch('GET', `${app.mountPath}/setup-pack`);
104 assert.equal(res.status, 200);
105 assert.equal(res.json.secrets, false);
106 assert.ok(!('access_token' in res.json));
107 assert.ok(!('refresh_token' in res.json));
108 assert.ok(!('device_code' in res.json));
109 assert.equal(res.json.phase_b?.status, 'available');
110 });
111
112 it('restart durability: pending code survives new router on same store dir', async () => {
113 const tmp = await createTempStrongStore();
114 cleanups.push(tmp.cleanup);
115
116 const app1 = await startDeviceOAuthApp({ tmp });
117 cleanups.push(async () => app1.stop());
118 const authRes = await app1.fetch('POST', `${app1.mountPath}/authorize`, {
119 client_name: 'Restart Test',
120 });
121
122 const store2 = createGatewayRefreshStore({ consistency: 'strong' });
123 const app2 = await startDeviceOAuthApp({ tmp, refreshStore: store2 });
124 cleanups.push(async () => app2.stop());
125 await app1.stop();
126
127 const approveRes = await app2.fetch(
128 'POST',
129 `${app2.mountPath}/approve`,
130 { user_code: authRes.json.user_code },
131 authHeaders()
132 );
133 assert.equal(approveRes.status, 200);
134
135 const tokenRes = await app2.fetch('POST', `${app2.mountPath}/token`, {
136 grant_type: DEVICE_GRANT_TYPE,
137 device_code: authRes.json.device_code,
138 });
139 assert.equal(tokenRes.status, 200);
140 assert.ok(tokenRes.json.access_token);
141 });
142 });
File History 1 commit
sha256:93bcf8f9bd56d8c5b9339f4ec73b9ebd66571398d56262d38eedc2cfa9db9882 fix(test): align Band B landing assertion with desktop MCP … Human 11 days ago