device-oauth-e2e.test.mjs
103 lines 3.3 KB
Raw
sha256:c2dbf04d56308f3bbf2d06e6d2eb022b8948b1e827195fe525a44e5e18d5f9c0 feat(auth): Phase B Connect cloud agent (RFC 8628) + Hermes… Human minor ⚠ breaking 10 days ago
1 /**
2 * Phase B — device OAuth: e2e tier.
3 *
4 * Scripted Hub + agent journey: authorize, pending list, approve, token exchange,
5 * refresh revoke; and deny path ending in access_denied.
6 */
7
8 import { describe, it, afterEach } from 'node:test';
9 import assert from 'node:assert/strict';
10 import jwt from 'jsonwebtoken';
11 import {
12 startDeviceOAuthApp,
13 authHeaders,
14 TEST_SECRET,
15 DEVICE_GRANT_TYPE,
16 } from './helpers/device-oauth-harness.mjs';
17
18 const cleanups = [];
19 afterEach(async () => {
20 while (cleanups.length) await cleanups.pop()();
21 });
22
23 describe('Phase B e2e — full Hub + agent journey', () => {
24 it('authorize → pending list → approve → token → revoke refresh', async () => {
25 const app = await startDeviceOAuthApp();
26 cleanups.push(async () => {
27 await app.stop();
28 await app.tmp.cleanup();
29 });
30
31 const authRes = await app.fetch('POST', `${app.mountPath}/authorize`, {
32 client_id: 'e2e-agent',
33 client_name: 'E2E Cloud Agent',
34 });
35 assert.equal(authRes.status, 200);
36 const { device_code: deviceCode, user_code: userCode } = authRes.json;
37
38 const pendingRes = await app.fetch('GET', `${app.mountPath}/pending`, undefined, authHeaders());
39 assert.equal(pendingRes.status, 200);
40 assert.ok(Array.isArray(pendingRes.json.pending));
41 assert.ok(
42 pendingRes.json.pending.some((p) => p.userCode === userCode),
43 'pending list must include user_code'
44 );
45 assert.ok(!JSON.stringify(pendingRes.json).includes(deviceCode), 'pending must not expose device_code');
46
47 const approveRes = await app.fetch(
48 'POST',
49 `${app.mountPath}/approve`,
50 { user_code: userCode },
51 authHeaders()
52 );
53 assert.equal(approveRes.status, 200);
54
55 const tokenRes = await app.fetch('POST', `${app.mountPath}/token`, {
56 grant_type: DEVICE_GRANT_TYPE,
57 device_code: deviceCode,
58 });
59 assert.equal(tokenRes.status, 200);
60 const payload = jwt.verify(tokenRes.json.access_token, TEST_SECRET);
61 assert.equal(payload.type, 'mcp_access');
62
63 const revokeRes = await app.fetch('POST', `${app.mountPath}/revoke`, {
64 refresh_token: tokenRes.json.refresh_token,
65 });
66 assert.equal(revokeRes.status, 200);
67 assert.equal(revokeRes.json.ok, true);
68
69 const refreshFail = await app.fetch('POST', `${app.mountPath}/token`, {
70 grant_type: 'refresh_token',
71 refresh_token: tokenRes.json.refresh_token,
72 });
73 assert.equal(refreshFail.status, 400);
74 assert.equal(refreshFail.json.error, 'invalid_grant');
75 });
76
77 it('deny path: authorize → deny → poll access_denied', async () => {
78 const app = await startDeviceOAuthApp();
79 cleanups.push(async () => {
80 await app.stop();
81 await app.tmp.cleanup();
82 });
83
84 const authRes = await app.fetch('POST', `${app.mountPath}/authorize`, {});
85 const { device_code: deviceCode, user_code: userCode } = authRes.json;
86
87 const denyRes = await app.fetch(
88 'POST',
89 `${app.mountPath}/deny`,
90 { user_code: userCode },
91 authHeaders()
92 );
93 assert.equal(denyRes.status, 200);
94 assert.equal(denyRes.json.ok, true);
95
96 const tokenRes = await app.fetch('POST', `${app.mountPath}/token`, {
97 grant_type: DEVICE_GRANT_TYPE,
98 device_code: deviceCode,
99 });
100 assert.equal(tokenRes.status, 400);
101 assert.equal(tokenRes.json.error, 'access_denied');
102 });
103 });
File History 1 commit
sha256:c2dbf04d56308f3bbf2d06e6d2eb022b8948b1e827195fe525a44e5e18d5f9c0 feat(auth): Phase B Connect cloud agent (RFC 8628) + Hermes… Human minor 10 days ago