import { stringify as stringifyQueryString } from "querystring" ; import { createHash , randomBytes } from "crypto" ; import { CloudFrontRequestHandler } from "aws-lambda" ; import { getConfigWithJwtVerifier , extractAndParseCookies , decodeToken , urlSafe , sign , timestampInSeconds , } from "../shared/shared" ; const CONFIG = getConfigWithJwtVerifier ( ) ; CONFIG . logger . debug ( "Configuration loaded:" , CONFIG ) ; export const handler = async ( event ) => { CONFIG . logger . debug ( "Event:" , event ) ; const request = event . Records [ 0 ] . cf . request ; const domainName = request . headers [ "host" ] [ 0 ] . value ; const requestedUri = `${ request . uri }${ request . querystring ? "?" + request . querystring : "" }` ; try { const { idToken , refreshToken , nonce , nonceHmac } = extractAndParseCookies ( request . headers , CONFIG . clientId , CONFIG . cookieCompatibility ) ; CONFIG . logger . debug ( "Extracted cookies:\n" , { idToken , refreshToken , nonce , nonceHmac , } ) ; if ( ! idToken ) { throw new Error ( "No ID token present in cookies" ) ; } const { exp } = decodeToken ( idToken ) ; CONFIG . logger . debug ( "ID token exp:" , exp , new Date ( exp * 1000 ) . toISOString ( ) ) ; if ( Date . now ( ) / 1000 > exp - 60 * 10 && refreshToken ) { CONFIG . logger . info ( "Will redirect to refresh endpoint for refreshing tokens using refresh token" ) ; const nonce = generateNonce ( ) ; const response = { status : "307" , statusDescription : "Temporary Redirect" , headers : { location : [ { key : "location" , value : `https://${ domainName }${ CONFIG . redirectPathAuthRefresh }?${ stringifyQueryString ( { requestedUri , nonce } ) }` , } , ] , "set-cookie" : [ { key : "set-cookie" , value : `spa-auth-edge-nonce=${ encodeURIComponent ( nonce ) }; ${ CONFIG . cookieSettings . nonce }` , } , { key : "set-cookie" , value : `spa-auth-edge-nonce-hmac=${ encodeURIComponent ( sign ( nonce , CONFIG . nonceSigningSecret , CONFIG . nonceLength ) ) }; ${ CONFIG . cookieSettings . nonce }` , } , ] , ... CONFIG . cloudFrontHeaders , } , } ; CONFIG . logger . debug ( "Returning response:\n" , response ) ; return response ; } await CONFIG . jwtVerifier . verify ( idToken ) ; CONFIG . logger . debug ( "Returning request:\n" , request ) ; return request ; } catch ( err ) { CONFIG . logger . info ( `Will redirect to Cognito for sign-in because: ${ err }` ) ; const nonce = generateNonce ( ) ; const state = { nonce , nonceHmac : sign ( nonce , CONFIG . nonceSigningSecret , CONFIG . nonceLength ) , ... generatePkceVerifier ( ) , } ; CONFIG . logger . debug ( "Using new state\n" , state ) ; const loginQueryString = stringifyQueryString ( { redirect_uri : `https://${ domainName }${ CONFIG . redirectPathSignIn }` , response_type : "code" , client_id : CONFIG . clientId , state : urlSafe . stringify ( Buffer . from ( JSON . stringify ( { nonce : state . nonce , requestedUri } ) ) . toString ( "base64" ) ) , scope : CONFIG . oauthScopes . join ( " " ) , code_challenge_method : "S256" , code_challenge : state . pkceHash , } ) ; const response = { status : "307" , statusDescription : "Temporary Redirect" , headers : { location : [ { key : "location" , value : `https://${ CONFIG . cognitoAuthDomain }/oauth2/authorize?${ loginQueryString }` , } , ] , "set-cookie" : [ { key : "set-cookie" , value : `spa-auth-edge-nonce=${ encodeURIComponent ( state . nonce ) }; ${ CONFIG . cookieSettings . nonce }` , } , { key : "set-cookie" , value : `spa-auth-edge-nonce-hmac=${ encodeURIComponent ( state . nonceHmac ) }; ${ CONFIG . cookieSettings . nonce }` , } , { key : "set-cookie" , value : `spa-auth-edge-pkce=${ encodeURIComponent ( state . pkce ) }; ${ CONFIG . cookieSettings . nonce }` , } , ] , ... CONFIG . cloudFrontHeaders , } , } ; CONFIG . logger . debug ( "Returning response:\n" , response ) ; return response ; } } ; function generatePkceVerifier ( pkce ? ) { if ( ! pkce ) { pkce = [ ... new Array ( CONFIG . pkceLength ) ] . map ( ( ) => randomChoiceFromIndexable ( CONFIG . secretAllowedCharacters ) ) . join ( "" ) ; } const verifier = { pkce , pkceHash : urlSafe . stringify ( createHash ( "sha256" ) . update ( pkce , "utf8" ) . digest ( "base64" ) ) , } ; CONFIG . logger . debug ( "Generated PKCE verifier:\n" , verifier ) ; return verifier ; } function generateNonce ( ) { const randomString = [ ... new Array ( CONFIG . nonceLength ) ] . map ( ( ) => randomChoiceFromIndexable ( CONFIG . secretAllowedCharacters ) ) . join ( "" ) ; const nonce = `${ timestampInSeconds ( ) }T${ randomString }` ; CONFIG . logger . debug ( "Generated new nonce:" , nonce ) ; return nonce ; } function randomChoiceFromIndexable ( indexable ) { if ( indexable . length > 256 ) { throw new Error ( `indexable is too large: ${ indexable . length }` ) ; } const chunks = Math . floor ( 256 / indexable . length ) ; const firstBiassedIndex = indexable . length * chunks ; let randomNumber ; do { randomNumber = randomBytes ( 1 ) [ 0 ] ; } while ( randomNumber >= firstBiassedIndex ) ; const index = randomNumber % indexable . length ; return indexable [ index ] ; }