native-oauth-refresh-durability.test.mjs
108 lines 3.9 KB
Raw
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