native-oauth-refresh-durability.test.mjs
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700
feat(companion): add production macOS distribution path
Human
minor
⚠ breaking
24 days ago
| 1 | import assert from 'node:assert/strict'; |
| 2 | import { createHash, randomUUID } from 'node:crypto'; |
| 3 | import fs from 'node:fs/promises'; |
| 4 | import os from 'node:os'; |
| 5 | import path from 'node:path'; |
| 6 | import { after, before, test } from 'node:test'; |
| 7 | import express from 'express'; |
| 8 | |
| 9 | import { createNativeOAuthRouter } from '../hub/gateway/native-oauth-provider.mjs'; |
| 10 | import { createGatewayRefreshStore } from '../hub/gateway/refresh-token-store.mjs'; |
| 11 | import { bindUserToCode, savePendingCode } from '../hub/gateway/native-as-store.mjs'; |
| 12 | |
| 13 | let dataDir; |
| 14 | |
| 15 | before(async () => { |
| 16 | dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'knowtation-native-refresh-')); |
| 17 | process.env.KNOWTATION_GATEWAY_DATA_DIR = dataDir; |
| 18 | }); |
| 19 | |
| 20 | after(async () => { |
| 21 | delete process.env.KNOWTATION_GATEWAY_DATA_DIR; |
| 22 | await fs.rm(dataDir, { recursive: true, force: true }); |
| 23 | }); |
| 24 | |
| 25 | async function startRouter(refreshStore) { |
| 26 | const app = express(); |
| 27 | const { router } = createNativeOAuthRouter({ |
| 28 | baseUrl: 'http://127.0.0.1', |
| 29 | issueAccessToken: async (sub) => `access-${sub}`, |
| 30 | grantedScopes: () => ['vault:read', 'vault:write'], |
| 31 | refreshStore, |
| 32 | }); |
| 33 | app.use('/api/v1/auth/native', router); |
| 34 | const server = await new Promise((resolve) => { |
| 35 | const listening = app.listen(0, '127.0.0.1', () => resolve(listening)); |
| 36 | }); |
| 37 | return { |
| 38 | origin: `http://127.0.0.1:${server.address().port}/api/v1/auth/native`, |
| 39 | close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())), |
| 40 | }; |
| 41 | } |
| 42 | |
| 43 | async function postForm(url, fields) { |
| 44 | const response = await fetch(url, { |
| 45 | method: 'POST', |
| 46 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, |
| 47 | body: new URLSearchParams(fields), |
| 48 | }); |
| 49 | return { status: response.status, json: await response.json() }; |
| 50 | } |
| 51 | |
| 52 | test('native refresh remains client-bound after the in-memory registration store restarts', async () => { |
| 53 | const refreshStore = createGatewayRefreshStore({ consistency: 'strong' }); |
| 54 | const first = await startRouter(refreshStore); |
| 55 | let second; |
| 56 | try { |
| 57 | const redirectUri = 'http://127.0.0.1:54123/callback'; |
| 58 | const registration = await fetch(`${first.origin}/register`, { |
| 59 | method: 'POST', |
| 60 | headers: { 'Content-Type': 'application/json' }, |
| 61 | body: JSON.stringify({ redirect_uris: [redirectUri] }), |
| 62 | }); |
| 63 | assert.equal(registration.status, 201); |
| 64 | const clientId = (await registration.json()).client_id; |
| 65 | const code = randomUUID(); |
| 66 | const verifier = 'durable-native-refresh-verifier'; |
| 67 | await savePendingCode(code, { |
| 68 | clientId, |
| 69 | codeChallenge: createHash('sha256').update(verifier).digest('base64url'), |
| 70 | redirectUri, |
| 71 | scopes: ['vault:read'], |
| 72 | }); |
| 73 | await bindUserToCode(code, 'google:durable-user'); |
| 74 | const issued = await postForm(`${first.origin}/token`, { |
| 75 | grant_type: 'authorization_code', |
| 76 | client_id: clientId, |
| 77 | code, |
| 78 | code_verifier: verifier, |
| 79 | redirect_uri: redirectUri, |
| 80 | }); |
| 81 | assert.equal(issued.status, 200); |
| 82 | assert.ok(issued.json.refresh_token); |
| 83 | const stored = await refreshStore.peek(issued.json.refresh_token); |
| 84 | assert.equal(stored.meta.client_id, clientId); |
| 85 | assert.equal(stored.meta.scopes, 'vault:read'); |
| 86 | |
| 87 | await first.close(); |
| 88 | second = await startRouter(refreshStore); |
| 89 | const wrongClient = await postForm(`${second.origin}/token`, { |
| 90 | grant_type: 'refresh_token', |
| 91 | client_id: 'wrong-client', |
| 92 | refresh_token: issued.json.refresh_token, |
| 93 | }); |
| 94 | assert.equal(wrongClient.status, 401); |
| 95 | |
| 96 | const refreshed = await postForm(`${second.origin}/token`, { |
| 97 | grant_type: 'refresh_token', |
| 98 | client_id: clientId, |
| 99 | refresh_token: issued.json.refresh_token, |
| 100 | }); |
| 101 | assert.equal(refreshed.status, 200); |
| 102 | assert.notEqual(refreshed.json.refresh_token, issued.json.refresh_token); |
| 103 | assert.equal(refreshed.json.scope, 'vault:read'); |
| 104 | } finally { |
| 105 | if (second) await second.close(); |
| 106 | else await first.close().catch(() => {}); |
| 107 | } |
| 108 | }); |
File History
1 commit
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700
feat(companion): add production macOS distribution path
Human
minor
⚠
24 days ago