@@ -9,6 +9,7 @@ import { configStore } from "../stores/config.js";
99import {
1010 CLIENT_VERSION_HEADER_NAME ,
1111 CLIENT_VERSION_HEADER_VALUE ,
12+ DEFAULT_AUTH_TIMEOUT ,
1213} from "./constants.js" ;
1314import { ResponseError } from "./errors.js" ;
1415import { ParamType } from "./types.js" ;
@@ -49,18 +50,77 @@ async function getOAuthServerUrls(apiUrl: string) {
4950 throw new Error ( "Failed to fetch OAuth server metadata" ) ;
5051}
5152
52- export async function waitForAccessToken ( baseUrl : string , apiUrl : string ) {
53- const { authorizationEndpoint, tokenEndpoint, registrationEndpoint } =
54- await getOAuthServerUrls ( apiUrl ) ;
53+ async function registerClient (
54+ registrationEndpoint : string ,
55+ redirectUri : string ,
56+ ) {
57+ const registrationResponse = await fetch ( registrationEndpoint , {
58+ method : "POST" ,
59+ headers : {
60+ "Content-Type" : "application/json" ,
61+ } ,
62+ body : JSON . stringify ( {
63+ client_name : "Bucket CLI" ,
64+ token_endpoint_auth_method : "none" ,
65+ grant_types : [ "authorization_code" ] ,
66+ redirect_uris : [ redirectUri ] ,
67+ } ) ,
68+ signal : AbortSignal . timeout ( 5000 ) ,
69+ } ) ;
5570
56- let resolve : ( args : waitForAccessToken ) => void ;
57- let reject : ( arg0 : Error ) => void ;
71+ if ( ! registrationResponse . ok ) {
72+ throw new Error ( `Could not register client with OAuth server` ) ;
73+ }
5874
59- const promise = new Promise < waitForAccessToken > ( ( res , rej ) => {
60- resolve = res ;
61- reject = rej ;
75+ const registrationData = ( await registrationResponse . json ( ) ) as {
76+ client_id : string ;
77+ } ;
78+
79+ return registrationData . client_id ;
80+ }
81+
82+ async function exchangeCodeForToken (
83+ tokenEndpoint : string ,
84+ clientId : string ,
85+ code : string ,
86+ codeVerifier : string ,
87+ redirectUri : string ,
88+ ) {
89+ const response = await fetch ( tokenEndpoint , {
90+ method : "POST" ,
91+ headers : {
92+ "Content-Type" : "application/x-www-form-urlencoded" ,
93+ } ,
94+ body : new URLSearchParams ( {
95+ grant_type : "authorization_code" ,
96+ client_id : clientId ,
97+ code,
98+ code_verifier : codeVerifier ,
99+ redirect_uri : redirectUri ,
100+ } ) ,
101+ signal : AbortSignal . timeout ( 5000 ) ,
62102 } ) ;
63103
104+ if ( ! response . ok ) {
105+ let errorDescription : string | undefined ;
106+
107+ try {
108+ const jsonResponse = await response . json ( ) ;
109+ errorDescription = jsonResponse . error_description || jsonResponse . error ;
110+ } catch {
111+ // ignore
112+ }
113+
114+ return { error : errorDescription ?? "unknown error" } ;
115+ }
116+
117+ return {
118+ accessToken : ( await response . json ( ) ) . access_token ,
119+ expiresAt : new Date ( Date . now ( ) + ( await response . json ( ) ) . expires_in * 1000 ) ,
120+ } ;
121+ }
122+
123+ function createChallenge ( ) {
64124 // PKCE code verifier and challenge
65125 const codeVerifier = crypto . randomBytes ( 32 ) . toString ( "base64url" ) ;
66126 const codeChallenge = crypto
@@ -71,13 +131,34 @@ export async function waitForAccessToken(baseUrl: string, apiUrl: string) {
71131 . replace ( / \+ / g, "-" )
72132 . replace ( / \/ / g, "_" ) ;
73133
134+ const state = crypto . randomUUID ( ) ;
135+
136+ return { codeVerifier, codeChallenge, state } ;
137+ }
138+
139+ export async function waitForAccessToken ( baseUrl : string , apiUrl : string ) {
140+ const { authorizationEndpoint, tokenEndpoint, registrationEndpoint } =
141+ await getOAuthServerUrls ( apiUrl ) ;
142+
143+ let resolve : ( args : waitForAccessToken ) => void ;
144+ let reject : ( arg0 : Error ) => void ;
145+
146+ const promise = new Promise < waitForAccessToken > ( ( res , rej ) => {
147+ resolve = res ;
148+ reject = rej ;
149+ } ) ;
150+
151+ const { codeVerifier, codeChallenge, state } = createChallenge ( ) ;
152+
74153 const timeout = setTimeout ( ( ) => {
75- cleanupAndReject ( new Error ( "Authentication timed out after 60 seconds" ) ) ;
76- } , 60000 ) ;
154+ cleanupAndReject (
155+ `authentication timed out after ${ DEFAULT_AUTH_TIMEOUT / 1000 } seconds` ,
156+ ) ;
157+ } , DEFAULT_AUTH_TIMEOUT ) ;
77158
78- function cleanupAndReject ( error : Error ) {
159+ function cleanupAndReject ( message : string ) {
79160 cleanup ( ) ;
80- reject ( error ) ;
161+ reject ( new Error ( `Could not authenticate: ${ message } ` ) ) ;
81162 }
82163
83164 function cleanup ( ) {
@@ -95,37 +176,16 @@ export async function waitForAccessToken(baseUrl: string, apiUrl: string) {
95176 throw new Error ( "Could not start server" ) ;
96177 }
97178
98- const redirectUri = `http://localhost:${ address . port } /callback` ;
99-
100- const registrationResponse = await fetch ( registrationEndpoint , {
101- method : "POST" ,
102- headers : {
103- "Content-Type" : "application/json" ,
104- } ,
105- body : JSON . stringify ( {
106- client_name : "Bucket CLI" ,
107- token_endpoint_auth_method : "none" ,
108- grant_types : [ "authorization_code" ] ,
109- redirect_uris : [ redirectUri ] ,
110- } ) ,
111- signal : AbortSignal . timeout ( 5000 ) ,
112- } ) ;
113-
114- if ( ! registrationResponse . ok ) {
115- throw new Error ( `Could not register client with OAuth server` ) ;
116- }
117-
118- const registrationData = ( await registrationResponse . json ( ) ) as {
119- client_id : string ;
120- } ;
179+ const callbackPath = "/oauth_callback" ;
180+ const redirectUri = `http://localhost:${ address . port } ${ callbackPath } ` ;
121181
122- const clientId = registrationData . client_id ;
182+ const clientId = await registerClient ( registrationEndpoint , redirectUri ) ;
123183
124184 const params = {
125185 response_type : "code" ,
126186 client_id : clientId ,
127187 redirect_uri : redirectUri ,
128- state : crypto . randomUUID ( ) ,
188+ state,
129189 code_challenge : codeChallenge ,
130190 code_challenge_method : "S256" ,
131191 } ;
@@ -134,20 +194,18 @@ export async function waitForAccessToken(baseUrl: string, apiUrl: string) {
134194
135195 server . on ( "request" , async ( req , res ) => {
136196 if ( ! clientId || ! redirectUri ) {
137- res . writeHead ( 500 ) . end ( "Could not authenticate: something went wrong" ) ;
197+ res . writeHead ( 500 ) . end ( "Something went wrong" ) ;
138198
139- cleanupAndReject (
140- new Error ( "Could not authenticate: something went wrong" ) ,
141- ) ;
199+ cleanupAndReject ( "something went wrong" ) ;
142200 return ;
143201 }
144202
145203 const url = new URL ( req . url ?? "/" , "http://127.0.0.1" ) ;
146204
147- if ( url . pathname !== "/callback" ) {
205+ if ( url . pathname !== callbackPath ) {
148206 res . writeHead ( 404 ) . end ( "Invalid path" ) ;
149207
150- cleanupAndReject ( new Error ( "Could not authenticate: invalid path") ) ;
208+ cleanupAndReject ( " invalid path") ;
151209 return ;
152210 }
153211
@@ -156,64 +214,48 @@ export async function waitForAccessToken(baseUrl: string, apiUrl: string) {
156214 res . writeHead ( 400 ) . end ( "Could not authenticate" ) ;
157215
158216 const errorDescription = url . searchParams . get ( "error_description" ) ;
159- cleanupAndReject (
160- new Error ( `Could not authenticate: ${ errorDescription || error } ` ) ,
161- ) ;
217+ cleanupAndReject ( `${ errorDescription || error } ` ) ;
162218 return ;
163219 }
164220
165221 const code = url . searchParams . get ( "code" ) ;
166222 if ( ! code ) {
167223 res . writeHead ( 400 ) . end ( "Could not authenticate" ) ;
168224
169- cleanupAndReject ( new Error ( "Could not authenticate: no code provided") ) ;
225+ cleanupAndReject ( " no code provided") ;
170226 return ;
171227 }
172228
173- const response = await fetch ( tokenEndpoint , {
174- method : "POST" ,
175- headers : {
176- "Content-Type" : "application/x-www-form-urlencoded" ,
177- } ,
178- body : new URLSearchParams ( {
179- grant_type : "authorization_code" ,
180- client_id : clientId ,
181- code,
182- code_verifier : codeVerifier ,
183- redirect_uri : redirectUri ,
184- } ) ,
185- signal : AbortSignal . timeout ( 5000 ) ,
186- } ) ;
229+ const response = await exchangeCodeForToken (
230+ tokenEndpoint ,
231+ clientId ,
232+ code ,
233+ codeVerifier ,
234+ redirectUri ,
235+ ) ;
187236
188- if ( ! response . ok ) {
237+ if ( "error" in response ) {
189238 res
190239 . writeHead ( 302 , {
191240 location : errorUrl (
192241 baseUrl ,
193- "Could not authenticate: Unable to fetch access token" ,
242+ "Could not authenticate: unable to fetch access token" ,
194243 ) ,
195244 } )
196245 . end ( "Could not authenticate" ) ;
197246
198- const json = await response . json ( ) ;
199- cleanupAndReject (
200- new Error ( `Could not authenticate: ${ JSON . stringify ( json ) } ` ) ,
201- ) ;
247+ cleanupAndReject ( JSON . stringify ( response . error ) ) ;
202248 return ;
203249 }
250+
204251 res
205252 . writeHead ( 302 , {
206253 location : successUrl ( baseUrl ) ,
207254 } )
208255 . end ( "Authentication successful" ) ;
209256
210- const jsonResponse = await response . json ( ) ;
211-
212257 cleanup ( ) ;
213- resolve ( {
214- accessToken : jsonResponse . access_token ,
215- expiresAt : new Date ( Date . now ( ) + jsonResponse . expires_in * 1000 ) ,
216- } ) ;
258+ resolve ( response ) ;
217259 } ) ;
218260
219261 console . log (
0 commit comments