device-oauth-e2e.test.mjs
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