this repo has no description
1/**
2 * OAuth flow helpers for e2e tests
3 */
4
5import { randomBytes } from 'node:crypto';
6import { DpopClient } from './dpop.js';
7
8const BASE = 'http://localhost:8787';
9
10/**
11 * Fetch with retry for flaky wrangler dev
12 * @param {string} url
13 * @param {RequestInit} options
14 * @param {number} maxAttempts
15 * @returns {Promise<Response>}
16 */
17async function fetchWithRetry(url, options, maxAttempts = 3) {
18 let lastError;
19 for (let attempt = 0; attempt < maxAttempts; attempt++) {
20 try {
21 const res = await fetch(url, options);
22 // Check if we got an HTML error page instead of expected response
23 const contentType = res.headers.get('content-type') || '';
24 if (!res.ok && contentType.includes('text/html')) {
25 // Wrangler dev error page - retry
26 if (attempt < maxAttempts - 1) {
27 await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
28 continue;
29 }
30 }
31 return res;
32 } catch (err) {
33 lastError = err;
34 if (attempt < maxAttempts - 1) {
35 await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
36 }
37 }
38 }
39 throw lastError || new Error('Fetch failed after retries');
40}
41
42/**
43 * Get an OAuth token with a specific scope via full PAR -> authorize -> token flow
44 * @param {string} scope - The scope to request
45 * @param {string} did - The DID to authenticate as
46 * @param {string} password - The password for authentication
47 * @returns {Promise<{accessToken: string, refreshToken: string, dpop: DpopClient}>}
48 */
49export async function getOAuthTokenWithScope(scope, did, password) {
50 const dpop = await DpopClient.create();
51 const clientId = 'http://localhost:3000';
52 const redirectUri = 'http://localhost:3000/callback';
53 const codeVerifier = randomBytes(32).toString('base64url');
54 const challengeBuffer = await crypto.subtle.digest(
55 'SHA-256',
56 new TextEncoder().encode(codeVerifier),
57 );
58 const codeChallenge = Buffer.from(challengeBuffer).toString('base64url');
59
60 // PAR request (with retry for flaky wrangler dev)
61 let parData;
62 for (let attempt = 0; attempt < 3; attempt++) {
63 // Generate fresh DPoP proof for each attempt
64 const parProof = await dpop.createProof('POST', `${BASE}/oauth/par`);
65 const parRes = await fetchWithRetry(`${BASE}/oauth/par`, {
66 method: 'POST',
67 headers: {
68 'Content-Type': 'application/x-www-form-urlencoded',
69 DPoP: parProof,
70 },
71 body: new URLSearchParams({
72 client_id: clientId,
73 redirect_uri: redirectUri,
74 response_type: 'code',
75 scope: scope,
76 code_challenge: codeChallenge,
77 code_challenge_method: 'S256',
78 login_hint: did,
79 }).toString(),
80 });
81 if (parRes.ok) {
82 parData = await parRes.json();
83 break;
84 }
85 if (attempt < 2) {
86 await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
87 } else {
88 const text = await parRes.text();
89 throw new Error(
90 `PAR request failed: ${parRes.status} - ${text.slice(0, 100)}`,
91 );
92 }
93 }
94
95 // Authorize (with retry)
96 let authCode;
97 for (let attempt = 0; attempt < 3; attempt++) {
98 const authRes = await fetchWithRetry(`${BASE}/oauth/authorize`, {
99 method: 'POST',
100 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
101 body: new URLSearchParams({
102 request_uri: parData.request_uri,
103 client_id: clientId,
104 password: password,
105 }).toString(),
106 redirect: 'manual',
107 });
108 const location = authRes.headers.get('location');
109 if (location) {
110 authCode = new URL(location).searchParams.get('code');
111 if (authCode) break;
112 }
113 if (attempt < 2) {
114 await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
115 } else {
116 throw new Error('Authorize request failed to return code');
117 }
118 }
119
120 // Token exchange (with retry and fresh DPoP proof)
121 let tokenData;
122 for (let attempt = 0; attempt < 3; attempt++) {
123 const tokenProof = await dpop.createProof('POST', `${BASE}/oauth/token`);
124 const tokenRes = await fetchWithRetry(`${BASE}/oauth/token`, {
125 method: 'POST',
126 headers: {
127 'Content-Type': 'application/x-www-form-urlencoded',
128 DPoP: tokenProof,
129 },
130 body: new URLSearchParams({
131 grant_type: 'authorization_code',
132 code: authCode,
133 client_id: clientId,
134 redirect_uri: redirectUri,
135 code_verifier: codeVerifier,
136 }).toString(),
137 });
138 if (tokenRes.ok) {
139 tokenData = await tokenRes.json();
140 break;
141 }
142 if (attempt < 2) {
143 await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
144 } else {
145 const text = await tokenRes.text();
146 throw new Error(
147 `Token request failed: ${tokenRes.status} - ${text.slice(0, 100)}`,
148 );
149 }
150 }
151
152 return {
153 accessToken: tokenData.access_token,
154 refreshToken: tokenData.refresh_token,
155 dpop,
156 };
157}