@@ -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 } ; SameSite=Lax; HttpOnly; Max-Age=3` ) ;
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