1
- import { sign } from '@tsndr/cloudflare-worker-jwt' ;
1
+ import * as jwt from '@tsndr/cloudflare-worker-jwt' ;
2
2
import { Router } from 'itty-router' ;
3
3
4
4
import { IntegrationInstallationConfiguration } from '@gitbook/api' ;
@@ -39,6 +39,14 @@ type OIDCProps = {
39
39
40
40
export type OIDCAction = { action : 'save.config' } ;
41
41
42
+ type OIDCTokenResponseData = {
43
+ access_token ?: string ;
44
+ id_token ?: string ;
45
+ refresh_token ?: string ;
46
+ token_type : 'Bearer' ;
47
+ expires_in : number ;
48
+ } ;
49
+
42
50
const getDomainWithHttps = ( url : string ) : string => {
43
51
const sanitizedURL = url . trim ( ) ;
44
52
if ( sanitizedURL . startsWith ( 'https://' ) ) {
@@ -271,11 +279,76 @@ const handleFetchEvent: FetchEventCallback<OIDCRuntimeContext> = async (request,
271
279
router . get ( '/visitor-auth/response' , async ( request ) => {
272
280
if ( 'site' in siteInstallation && siteInstallation . site ) {
273
281
const publishedContentUrls = await getPublishedContentUrls ( context ) ;
274
- const privateKey = context . environment . signingSecrets . siteInstallation ! ;
275
- let token ;
282
+
283
+ const accessTokenEndpoint = siteInstallation . configuration . access_token_endpoint ;
284
+ const clientId = siteInstallation . configuration . client_id ;
285
+ const clientSecret = siteInstallation . configuration . client_secret ;
286
+
287
+ if ( ! clientId || ! clientSecret || ! accessTokenEndpoint ) {
288
+ return new Response (
289
+ 'Error: Either client id, client secret or access token endpoint is missing in configuration' ,
290
+ {
291
+ status : 400 ,
292
+ } ,
293
+ ) ;
294
+ }
295
+ const searchParams = new URLSearchParams ( {
296
+ grant_type : 'authorization_code' ,
297
+ client_id : clientId ,
298
+ client_secret : clientSecret ,
299
+ code : `${ request . query . code } ` ,
300
+ redirect_uri : `${ installationURL } /visitor-auth/response` ,
301
+ } ) ;
302
+
303
+ const accessTokenResp = await fetch ( accessTokenEndpoint , {
304
+ method : 'POST' ,
305
+ headers : { 'content-type' : 'application/x-www-form-urlencoded' } ,
306
+ body : searchParams ,
307
+ } ) ;
308
+
309
+ if ( ! accessTokenResp . ok ) {
310
+ return new Response (
311
+ 'Error: Could not fetch access token from your authentication provider' ,
312
+ {
313
+ status : 401 ,
314
+ } ,
315
+ ) ;
316
+ }
317
+
318
+ const accessTokenData = await accessTokenResp . json < OIDCTokenResponseData > ( ) ;
319
+ if ( ! accessTokenData . access_token ) {
320
+ logger . debug ( JSON . stringify ( accessTokenResp , null , 2 ) ) ;
321
+ logger . debug (
322
+ `Did not receive access token. Error: ${ accessTokenResp && 'error' in accessTokenResp ? accessTokenResp . error : '' } ${
323
+ accessTokenResp && 'error_description' in accessTokenResp
324
+ ? accessTokenResp . error_description
325
+ : ''
326
+ } `,
327
+ ) ;
328
+ return new Response (
329
+ 'Error: No access token found in response from your authentication provider' ,
330
+ {
331
+ status : 401 ,
332
+ } ,
333
+ ) ;
334
+ }
335
+
336
+ // TODO: verify token using JWKS and check audience (aud) claims
337
+ const decodedAccessToken = await jwt . decode ( accessTokenData . access_token ) ;
338
+ const privateKey = context . environment . signingSecrets . siteInstallation ;
339
+ if ( ! privateKey ) {
340
+ return new Response ( 'Error: Missing private key from site installation' , {
341
+ status : 400 ,
342
+ } ) ;
343
+ }
344
+
345
+ let jwtToken : string | undefined ;
276
346
try {
277
- token = await sign (
278
- { exp : Math . floor ( Date . now ( ) / 1000 ) + 1 * ( 60 * 60 ) } ,
347
+ jwtToken = await jwt . sign (
348
+ {
349
+ ...( decodedAccessToken . payload ?? { } ) ,
350
+ exp : Math . floor ( Date . now ( ) / 1000 ) + 1 * ( 60 * 60 ) ,
351
+ } ,
279
352
privateKey ,
280
353
) ;
281
354
} catch ( e ) {
@@ -284,76 +357,23 @@ const handleFetchEvent: FetchEventCallback<OIDCRuntimeContext> = async (request,
284
357
} ) ;
285
358
}
286
359
287
- const accessTokenEndpoint = siteInstallation . configuration . access_token_endpoint ;
288
- const clientId = siteInstallation . configuration . client_id ;
289
- const clientSecret = siteInstallation . configuration . client_secret ;
290
- if ( clientId && clientSecret && accessTokenEndpoint ) {
291
- const searchParams = new URLSearchParams ( {
292
- grant_type : 'authorization_code' ,
293
- client_id : clientId ,
294
- client_secret : clientSecret ,
295
- code : `${ request . query . code } ` ,
296
- redirect_uri : `${ installationURL } /visitor-auth/response` ,
297
- } ) ;
298
-
299
- const resp : any = await fetch ( accessTokenEndpoint , {
300
- method : 'POST' ,
301
- headers : { 'content-type' : 'application/x-www-form-urlencoded' } ,
302
- body : searchParams ,
303
- } )
304
- . then ( ( response ) => response . json ( ) )
305
- . catch ( ( err ) => {
306
- return new Response (
307
- 'Error: Could not fetch access token from your authentication provider' ,
308
- {
309
- status : 401 ,
310
- } ,
311
- ) ;
312
- } ) ;
313
-
314
- if ( 'access_token' in resp ) {
315
- let url ;
316
- const state = request . query . state ! . toString ( ) ;
317
- const location = state . substring ( state . indexOf ( '-' ) + 1 ) ;
318
- if ( location ) {
319
- url = new URL ( `${ publishedContentUrls ?. published } ${ location } ` ) ;
320
- url . searchParams . append ( 'jwt_token' , token ) ;
321
- } else {
322
- url = new URL ( publishedContentUrls ?. published ! ) ;
323
- url . searchParams . append ( 'jwt_token' , token ) ;
324
- }
325
- if ( publishedContentUrls ?. published && token ) {
326
- return Response . redirect ( url . toString ( ) ) ;
327
- } else {
328
- return new Response (
329
- "Error: Either JWT token or space's published URL is missing" ,
330
- {
331
- status : 500 ,
332
- } ,
333
- ) ;
334
- }
335
- } else {
336
- logger . debug ( JSON . stringify ( resp , null , 2 ) ) ;
337
- logger . debug (
338
- `Did not receive access token. Error: ${ ( resp && resp . error ) || '' } ${
339
- ( resp && resp . error_description ) || ''
340
- } `,
341
- ) ;
342
- return new Response (
343
- 'Error: No Access Token found in response from your OIDC provider' ,
344
- {
345
- status : 401 ,
346
- } ,
347
- ) ;
348
- }
349
- } else {
360
+ const publishedContentUrl = publishedContentUrls ?. published ;
361
+ if ( ! publishedContentUrl || ! jwtToken ) {
350
362
return new Response (
351
- ' Error: Either ClientId or Client Secret or Access Token Endpoint is missing' ,
363
+ " Error: Either JWT token or site's published URL is missing" ,
352
364
{
353
- status : 400 ,
365
+ status : 500 ,
354
366
} ,
355
367
) ;
356
368
}
369
+
370
+ const state = request . query . state ?. toString ( ) ;
371
+ const location = state ? state . substring ( state . indexOf ( '-' ) + 1 ) : '' ;
372
+
373
+ const url = new URL ( `${ publishedContentUrl } ${ location || '' } ` ) ;
374
+ url . searchParams . append ( 'jwt_token' , jwtToken ) ;
375
+
376
+ return Response . redirect ( url . toString ( ) ) ;
357
377
}
358
378
} ) ;
359
379
0 commit comments