@@ -173,9 +173,19 @@ ${error.getFullMessage()}`,
173173    if  ( isRequestEligibleForHandshake ( authenticateContext ) )  { 
174174      // Right now the only usage of passing in different headers is for multi-domain sync, which redirects somewhere else. 
175175      // In the future if we want to decorate the handshake redirect with additional headers per call we need to tweak this logic. 
176-       return  handshake ( authenticateContext ,  reason ,  message ,  headers  ??  buildRedirectToHandshake ( ) ) ; 
176+       const  handshakeHeaders  =  headers  ??  buildRedirectToHandshake ( ) ; 
177+       // Introduce the mechanism to protect for infinite handshake redirect loops 
178+       // using a cookie and returning true if it's infinite redirect loop or false if we can 
179+       // proceed with triggering handshake. 
180+       const  isRedirectLoop  =  setHandshakeInfiniteRedirectionLoopHeaders ( handshakeHeaders ) ; 
181+       if  ( isRedirectLoop )  { 
182+         const  msg  =  `Clerk: Refreshing the session token resulted in an infinite redirect loop. This usually means that your Clerk instance keys do not match - make sure to copy the correct publishable and secret keys from the Clerk dashboard.` ; 
183+         console . log ( msg ) ; 
184+         return  signedOut ( authenticateContext ,  reason ,  message ) ; 
185+       } 
186+       return  handshake ( authenticateContext ,  reason ,  message ,  handshakeHeaders ) ; 
177187    } 
178-     return  signedOut ( authenticateContext ,  reason ,  message ,   new   Headers ( ) ) ; 
188+     return  signedOut ( authenticateContext ,  reason ,  message ) ; 
179189  } 
180190
181191  async  function  authenticateRequestWithTokenInHeader ( )  { 
@@ -193,6 +203,34 @@ ${error.getFullMessage()}`,
193203    } 
194204  } 
195205
206+   // We want to prevent infinite handshake redirection loops. 
207+   // We incrementally set a `__clerk_redirection_loop` cookie, and when it loops 3 times, we throw an error. 
208+   // We also utilize the `referer` header to skip the prefetch requests. 
209+   function  setHandshakeInfiniteRedirectionLoopHeaders ( headers : Headers ) : boolean  { 
210+     if  ( authenticateContext . handshakeRedirectLoopCounter  ===  3 )  { 
211+       return  true ; 
212+     } 
213+ 
214+     const  newCounterValue  =  authenticateContext . handshakeRedirectLoopCounter  +  1 ; 
215+     const  cookieName  =  constants . Cookies . RedirectCount ; 
216+     headers . append ( 'Set-Cookie' ,  `${ cookieName } ${ newCounterValue }  ) ; 
217+     return  false ; 
218+   } 
219+ 
220+   function  handleHandshakeTokenVerificationErrorInDevelopment ( error : TokenVerificationError )  { 
221+     // In development, the handshake token is being transferred in the URL as a query parameter, so there is no 
222+     // possibility of collision with a handshake token of another app running on the same local domain 
223+     // (etc one app on localhost:3000 and one on localhost:3001). 
224+     // Therefore, if the handshake token is invalid, it is likely that the user has switched Clerk keys locally. 
225+     // We make sure to throw a descriptive error message and then stop the handshake flow in every case, 
226+     // to avoid the possibility of an infinite loop. 
227+     if  ( error . reason  ===  TokenVerificationErrorReason . TokenInvalidSignature )  { 
228+       const  msg  =  `Clerk: Handshake token verification failed due to an invalid signature. If you have switched Clerk keys locally, clear your cookies and try again.` ; 
229+       throw  new  Error ( msg ) ; 
230+     } 
231+     throw  new  Error ( `Clerk: Handshake token verification failed: ${ error . getFullMessage ( ) }  ) ; 
232+   } 
233+ 
196234  async  function  authenticateRequestWithTokenInCookie ( )  { 
197235    const  hasActiveClient  =  authenticateContext . clientUat ; 
198236    const  hasSessionToken  =  ! ! authenticateContext . sessionTokenInCookie ; 
@@ -210,34 +248,22 @@ ${error.getFullMessage()}`,
210248      try  { 
211249        return  await  resolveHandshake ( ) ; 
212250      }  catch  ( error )  { 
213-         // If for some reason the handshake token is invalid or stale, we ignore it and continue trying to authenticate the request. 
214-         // Worst case, the handshake will trigger again and return a refreshed token. 
215-         if  ( error  instanceof  TokenVerificationError )  { 
216-           if  ( authenticateContext . instanceType  ===  'development' )  { 
217-             if  ( error . reason  ===  TokenVerificationErrorReason . TokenInvalidSignature )  { 
218-               throw  new  Error ( 
219-                 `Clerk: Handshake token verification failed due to an invalid signature. If you have switched Clerk keys locally, clear your cookies and try again.` , 
220-               ) ; 
221-             } 
222- 
223-             throw  new  Error ( `Clerk: Handshake token verification failed: ${ error . getFullMessage ( ) }  ) ; 
224-           } 
225- 
226-           if  ( 
227-             error . reason  ===  TokenVerificationErrorReason . TokenInvalidSignature  || 
228-             error . reason  ===  TokenVerificationErrorReason . InvalidSecretKey 
229-           )  { 
230-             // Avoid infinite redirect loops due to incorrect secret-keys 
231-             return  signedOut ( 
232-               authenticateContext , 
233-               AuthErrorReason . UnexpectedError , 
234-               `Clerk: Handshake token verification failed with "${ error . reason }  , 
235-             ) ; 
236-           } 
251+         // In production, the handshake token is being transferred as a cookie, so there is a possibility of collision 
252+         // with a handshake token of another app running on the same etld+1 domain. 
253+         // For example, if one app is running on sub1.clerk.com and another on sub2.clerk.com, the handshake token 
254+         // cookie for both apps will be set on etld+1 (clerk.com) so there's a possibility that one app will accidentally 
255+         // use the handshake token of a different app during the handshake flow. 
256+         // In this scenario, verification will fail with TokenInvalidSignature. In contrast to the development case, 
257+         // we need to allow the flow to continue so the app eventually retries another handshake with the correct token. 
258+         // We need to make sure, however, that we don't allow the flow to continue indefinitely, so we throw an error after X 
259+         // retries to avoid an infinite loop. An infinite loop can happen if the customer switched Clerk keys for their prod app. 
260+ 
261+         // Check the handleHandshakeTokenVerificationErrorInDevelopment function for the development case. 
262+         if  ( error  instanceof  TokenVerificationError  &&  authenticateContext . instanceType  ===  'development' )  { 
263+           handleHandshakeTokenVerificationErrorInDevelopment ( error ) ; 
237264        } 
238265      } 
239266    } 
240- 
241267    /** 
242268     * Otherwise, check for "known unknown" auth states that we can resolve with a handshake. 
243269     */ 
0 commit comments