From 150a3902913adfd9674222940a1cc6f5000ce127 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 19 Sep 2024 15:20:46 +0300 Subject: [PATCH 1/4] fix(backend): Add more error reasons for refresh token query param --- .changeset/weak-trees-perform.md | 5 ++ packages/backend/src/errors.ts | 12 +++++ packages/backend/src/tokens/request.ts | 65 ++++++++++++++++++++------ 3 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 .changeset/weak-trees-perform.md diff --git a/.changeset/weak-trees-perform.md b/.changeset/weak-trees-perform.md new file mode 100644 index 00000000000..3b633264060 --- /dev/null +++ b/.changeset/weak-trees-perform.md @@ -0,0 +1,5 @@ +--- +"@clerk/backend": patch +--- + +Introduce more refresh token error reasons. diff --git a/packages/backend/src/errors.ts b/packages/backend/src/errors.ts index 111cf4d8ec1..8f662eb1510 100644 --- a/packages/backend/src/errors.ts +++ b/packages/backend/src/errors.ts @@ -23,6 +23,18 @@ export const TokenVerificationErrorReason = { JWKKidMismatch: 'jwk-kid-mismatch', }; +export const RefreshTokenErrorReason = { + NoCookie: 'no-cookie', + NonEligible: 'non-eligible', + InvalidSessionToken: 'invalid-session-token', + MissingApiClient: 'missing-api-client', + MissingSessionToken: 'missing-session-token', + MissingRefreshToken: 'missing-refresh-token', + SessionTokenDecodeFailed: 'session-token-decode-failed', + FetchNetworkError: 'fetch-network-error', + UnexpectedRefreshError: 'unexpected-refresh-error', +} as const; + export type TokenVerificationErrorReason = (typeof TokenVerificationErrorReason)[keyof typeof TokenVerificationErrorReason]; diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 53c5c19f2d9..ba53a1fc15a 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -3,7 +3,7 @@ import type { JwtPayload } from '@clerk/types'; import type { ApiClient } from '../api'; import { constants } from '../constants'; import type { TokenCarrier } from '../errors'; -import { TokenVerificationError, TokenVerificationErrorReason } from '../errors'; +import { RefreshTokenErrorReason, TokenVerificationError, TokenVerificationErrorReason } from '../errors'; import { decodeJwt } from '../jwt/verifyJwt'; import { assertValidSecretKey } from '../util/optionsAssertions'; import { isDevelopmentFromSecretKey } from '../util/shared'; @@ -195,16 +195,36 @@ ${error.getFullMessage()}`, } async function refreshToken(authenticateContext: AuthenticateContext): Promise { - // To perform a token refresh, apiClient must be defined. - assertApiClient(options.apiClient); + try { + // To perform a token refresh, apiClient must be defined. + assertApiClient(options.apiClient); + } catch (err: any) { + throw { errors: [{ code: RefreshTokenErrorReason.MissingApiClient, message: err?.message || '' }] }; + } const { sessionToken: expiredSessionToken, refreshTokenInCookie: refreshToken } = authenticateContext; - if (!expiredSessionToken || !refreshToken) { - throw new Error('Clerk: refreshTokenInCookie and sessionToken must be provided.'); + if (!expiredSessionToken) { + throw { + errors: [ + { + code: RefreshTokenErrorReason.MissingSessionToken, + message: 'Session token must be provided.', + }, + ], + }; + } + if (!refreshToken) { + throw { + errors: [{ code: RefreshTokenErrorReason.MissingRefreshToken, message: 'Refresh token must be provided.' }], + }; } // The token refresh endpoint requires a sessionId, so we decode that from the expired token. const { data: decodeResult, errors: decodedErrors } = decodeJwt(expiredSessionToken); if (!decodeResult || decodedErrors) { - throw new Error(`Clerk: unable to decode session token.`); + throw { + errors: [ + { code: RefreshTokenErrorReason.SessionTokenDecodeFailed, message: 'Unable to decode session token.' }, + ], + }; } // Perform the actual token refresh. const tokenResponse = await options.apiClient.sessions.refreshSession(decodeResult.payload.sid, { @@ -226,10 +246,19 @@ ${error.getFullMessage()}`, sessionToken = await refreshToken(authenticateContext); } catch (err: any) { if (err?.errors?.length) { + if (err.errors[0].code === 'unexpected_error') { + return { + data: null, + error: { + message: `Fetch unexpected error`, + cause: { reason: RefreshTokenErrorReason.FetchNetworkError, errors: err.errors }, + }, + }; + } return { data: null, error: { - message: `Clerk: unable to refresh session token.`, + message: err.errors[0].code, cause: { reason: err.errors[0].code, errors: err.errors }, }, }; @@ -247,7 +276,7 @@ ${error.getFullMessage()}`, data: null, error: { message: `Clerk: unable to verify refreshed session token.`, - cause: { reason: 'invalid-session-token', errors }, + cause: { reason: RefreshTokenErrorReason.InvalidSessionToken, errors }, }, }; } @@ -263,7 +292,11 @@ ${error.getFullMessage()}`, ): SignedInState | SignedOutState | HandshakeState { if (isRequestEligibleForHandshake(authenticateContext)) { // If a refresh error is not passed in, we default to 'no-cookie' or 'non-eligible'. - refreshError = refreshError || (authenticateContext.refreshTokenInCookie ? 'non-eligible' : 'no-cookie'); + refreshError = + refreshError || + (authenticateContext.refreshTokenInCookie + ? RefreshTokenErrorReason.NonEligible + : RefreshTokenErrorReason.NoCookie); // Right now the only usage of passing in different headers is for multi-domain sync, which redirects somewhere else. // In the future if we want to decorate the handshake redirect with additional headers per call we need to tweak this logic. @@ -398,7 +431,9 @@ ${error.getFullMessage()}`, ); const authErrReason = AuthErrorReason.SatelliteCookieNeedsSyncing; redirectURL.searchParams.append(constants.QueryParameters.HandshakeReason, authErrReason); - const refreshTokenError = authenticateContext.refreshTokenInCookie ? 'non-eligible' : 'no-cookie'; + const refreshTokenError = authenticateContext.refreshTokenInCookie + ? RefreshTokenErrorReason.NonEligible + : RefreshTokenErrorReason.NoCookie; redirectURL.searchParams.append(constants.QueryParameters.RefreshTokenError, refreshTokenError); const headers = new Headers({ [constants.Headers.Location]: redirectURL.toString() }); @@ -422,7 +457,9 @@ ${error.getFullMessage()}`, redirectBackToSatelliteUrl.searchParams.append(constants.QueryParameters.ClerkSynced, 'true'); const authErrReason = AuthErrorReason.PrimaryRespondsToSyncing; redirectBackToSatelliteUrl.searchParams.append(constants.QueryParameters.HandshakeReason, authErrReason); - const refreshTokenError = authenticateContext.refreshTokenInCookie ? 'non-eligible' : 'no-cookie'; + const refreshTokenError = authenticateContext.refreshTokenInCookie + ? RefreshTokenErrorReason.NonEligible + : RefreshTokenErrorReason.NoCookie; redirectBackToSatelliteUrl.searchParams.append(constants.QueryParameters.RefreshTokenError, refreshTokenError); const headers = new Headers({ [constants.Headers.Location]: redirectBackToSatelliteUrl.toString() }); @@ -480,7 +517,9 @@ ${error.getFullMessage()}`, return signedOut(authenticateContext, AuthErrorReason.UnexpectedError); } - let refreshError: string = authenticateContext.refreshTokenInCookie ? 'non-eligible' : 'no-cookie'; + let refreshError: string = authenticateContext.refreshTokenInCookie + ? RefreshTokenErrorReason.NonEligible + : RefreshTokenErrorReason.NoCookie; if (isRequestEligibleForRefresh(err, authenticateContext, request)) { const { data, error } = await attemptRefresh(authenticateContext); @@ -493,7 +532,7 @@ ${error.getFullMessage()}`, if (error?.cause?.reason) { refreshError = error.cause.reason; } else { - refreshError = 'unexpected-refresh-error'; + refreshError = RefreshTokenErrorReason.UnexpectedRefreshError; } } From e6a0601649f136c000db216c955dfeace631b131 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 19 Sep 2024 15:47:31 +0300 Subject: [PATCH 2/4] Move refresh token errors internally, because `/errors` are publicly exported --- packages/backend/src/errors.ts | 12 ------------ packages/backend/src/tokens/request.ts | 14 +++++++++++++- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/errors.ts b/packages/backend/src/errors.ts index 8f662eb1510..111cf4d8ec1 100644 --- a/packages/backend/src/errors.ts +++ b/packages/backend/src/errors.ts @@ -23,18 +23,6 @@ export const TokenVerificationErrorReason = { JWKKidMismatch: 'jwk-kid-mismatch', }; -export const RefreshTokenErrorReason = { - NoCookie: 'no-cookie', - NonEligible: 'non-eligible', - InvalidSessionToken: 'invalid-session-token', - MissingApiClient: 'missing-api-client', - MissingSessionToken: 'missing-session-token', - MissingRefreshToken: 'missing-refresh-token', - SessionTokenDecodeFailed: 'session-token-decode-failed', - FetchNetworkError: 'fetch-network-error', - UnexpectedRefreshError: 'unexpected-refresh-error', -} as const; - export type TokenVerificationErrorReason = (typeof TokenVerificationErrorReason)[keyof typeof TokenVerificationErrorReason]; diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index ba53a1fc15a..d1343b8a84e 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -3,7 +3,7 @@ import type { JwtPayload } from '@clerk/types'; import type { ApiClient } from '../api'; import { constants } from '../constants'; import type { TokenCarrier } from '../errors'; -import { RefreshTokenErrorReason, TokenVerificationError, TokenVerificationErrorReason } from '../errors'; +import { TokenVerificationError, TokenVerificationErrorReason } from '../errors'; import { decodeJwt } from '../jwt/verifyJwt'; import { assertValidSecretKey } from '../util/optionsAssertions'; import { isDevelopmentFromSecretKey } from '../util/shared'; @@ -17,6 +17,18 @@ import { verifyHandshakeToken } from './handshake'; import type { AuthenticateRequestOptions } from './types'; import { verifyToken } from './verify'; +const RefreshTokenErrorReason = { + NoCookie: 'no-cookie', + NonEligible: 'non-eligible', + InvalidSessionToken: 'invalid-session-token', + MissingApiClient: 'missing-api-client', + MissingSessionToken: 'missing-session-token', + MissingRefreshToken: 'missing-refresh-token', + SessionTokenDecodeFailed: 'session-token-decode-failed', + FetchNetworkError: 'fetch-network-error', + UnexpectedRefreshError: 'unexpected-refresh-error', +} as const; + function assertSignInUrlExists(signInUrl: string | undefined, key: string): asserts signInUrl is string { if (!signInUrl && isDevelopmentFromSecretKey(key)) { throw new Error(`Missing signInUrl. Pass a signInUrl for dev instances if an app is satellite`); From 51c436a5a710801176cff8387dcb27a82baa0a77 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Fri, 20 Sep 2024 16:37:34 +0300 Subject: [PATCH 3/4] Refactor to use the error pattern --- packages/backend/src/tokens/request.ts | 95 ++++++++++++++------------ 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index d1343b8a84e..5fc8602efda 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -1,6 +1,5 @@ import type { JwtPayload } from '@clerk/types'; -import type { ApiClient } from '../api'; import { constants } from '../constants'; import type { TokenCarrier } from '../errors'; import { TokenVerificationError, TokenVerificationErrorReason } from '../errors'; @@ -54,12 +53,6 @@ function assertSignInUrlFormatAndOrigin(_signInUrl: string, origin: string) { } } -function assertApiClient(apiClient: ApiClient | undefined): asserts apiClient is ApiClient { - if (!apiClient) { - throw new Error(`Missing apiClient. An apiClient is needed to perform token refresh.`); - } -} - /** * Currently, a request is only eligible for a handshake if we can say it's *probably* a request for a document, not a fetch or some other exotic request. * This heuristic should give us a reliable enough signal for browsers that support `Sec-Fetch-Dest` and for those that don't. @@ -206,56 +199,60 @@ ${error.getFullMessage()}`, throw error; } - async function refreshToken(authenticateContext: AuthenticateContext): Promise { - try { - // To perform a token refresh, apiClient must be defined. - assertApiClient(options.apiClient); - } catch (err: any) { - throw { errors: [{ code: RefreshTokenErrorReason.MissingApiClient, message: err?.message || '' }] }; + async function refreshToken( + authenticateContext: AuthenticateContext, + ): Promise<{ data: string; error: null } | { data: null; error: any }> { + // To perform a token refresh, apiClient must be defined. + if (!options.apiClient) { + return { + data: null, + error: { + message: 'An apiClient is needed to perform token refresh.', + cause: { reason: RefreshTokenErrorReason.MissingApiClient }, + }, + }; } const { sessionToken: expiredSessionToken, refreshTokenInCookie: refreshToken } = authenticateContext; if (!expiredSessionToken) { - throw { - errors: [ - { - code: RefreshTokenErrorReason.MissingSessionToken, - message: 'Session token must be provided.', - }, - ], + return { + data: null, + error: { + message: 'Session token must be provided.', + cause: { reason: RefreshTokenErrorReason.MissingSessionToken }, + }, }; } if (!refreshToken) { - throw { - errors: [{ code: RefreshTokenErrorReason.MissingRefreshToken, message: 'Refresh token must be provided.' }], + return { + data: null, + error: { + message: 'Refresh token must be provided.', + cause: { reason: RefreshTokenErrorReason.MissingRefreshToken }, + }, }; } // The token refresh endpoint requires a sessionId, so we decode that from the expired token. const { data: decodeResult, errors: decodedErrors } = decodeJwt(expiredSessionToken); if (!decodeResult || decodedErrors) { - throw { - errors: [ - { code: RefreshTokenErrorReason.SessionTokenDecodeFailed, message: 'Unable to decode session token.' }, - ], + return { + data: null, + error: { + message: 'Unable to decode session token.', + cause: { reason: RefreshTokenErrorReason.SessionTokenDecodeFailed, errors: decodedErrors }, + }, }; } - // Perform the actual token refresh. - const tokenResponse = await options.apiClient.sessions.refreshSession(decodeResult.payload.sid, { - expired_token: expiredSessionToken || '', - refresh_token: refreshToken || '', - request_origin: authenticateContext.clerkUrl.origin, - // The refresh endpoint expects headers as Record, so we need to transform it. - request_headers: Object.fromEntries(Array.from(request.headers.entries()).map(([k, v]) => [k, [v]])), - }); - - return tokenResponse.jwt; - } - async function attemptRefresh( - authenticateContext: AuthenticateContext, - ): Promise<{ data: { jwtPayload: JwtPayload; sessionToken: string }; error: null } | { data: null; error: any }> { - let sessionToken: string; try { - sessionToken = await refreshToken(authenticateContext); + // Perform the actual token refresh. + const tokenResponse = await options.apiClient.sessions.refreshSession(decodeResult.payload.sid, { + expired_token: expiredSessionToken || '', + refresh_token: refreshToken || '', + request_origin: authenticateContext.clerkUrl.origin, + // The refresh endpoint expects headers as Record, so we need to transform it. + request_headers: Object.fromEntries(Array.from(request.headers.entries()).map(([k, v]) => [k, [v]])), + }); + return { data: tokenResponse.jwt, error: null }; } catch (err: any) { if (err?.errors?.length) { if (err.errors[0].code === 'unexpected_error') { @@ -281,6 +278,16 @@ ${error.getFullMessage()}`, }; } } + } + + async function attemptRefresh( + authenticateContext: AuthenticateContext, + ): Promise<{ data: { jwtPayload: JwtPayload; sessionToken: string }; error: null } | { data: null; error: any }> { + const { data: sessionToken, error } = await refreshToken(authenticateContext); + if (!sessionToken) { + return { data: null, error }; + } + // Since we're going to return a signedIn response, we need to decode the data from the new sessionToken. const { data: jwtPayload, errors } = await verifyToken(sessionToken, authenticateContext); if (errors) { @@ -535,8 +542,8 @@ ${error.getFullMessage()}`, if (isRequestEligibleForRefresh(err, authenticateContext, request)) { const { data, error } = await attemptRefresh(authenticateContext); - if (!error) { - return signedIn(authenticateContext, data!.jwtPayload, undefined, data!.sessionToken); + if (data) { + return signedIn(authenticateContext, data.jwtPayload, undefined, data.sessionToken); } // If there's any error, simply fallback to the handshake flow. From a62c0ba04f35d7ae988c2b51f66f6eb85ed25fd3 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Mon, 23 Sep 2024 15:29:03 +0300 Subject: [PATCH 4/4] Fix tests --- integration/tests/handshake.test.ts | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/integration/tests/handshake.test.ts b/integration/tests/handshake.test.ts index d17a185403e..e79d1dd80cd 100644 --- a/integration/tests/handshake.test.ts +++ b/integration/tests/handshake.test.ts @@ -162,7 +162,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://${config.pkHost}/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie${devBrowserQuery}`, + )}&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie${devBrowserQuery}`, ); }); @@ -185,7 +185,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://${config.pkHost}/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie`, + )}&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie`, ); }); @@ -209,7 +209,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://${config.pkHost}/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie`, + )}&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie`, ); }); @@ -232,7 +232,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://${config.pkHost}/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie${devBrowserQuery}`, + )}&suffixed_cookies=false&__clerk_hs_reason=session-token-not-active-yet&__clerk_refresh=no-cookie${devBrowserQuery}`, ); }); @@ -256,7 +256,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://${config.pkHost}/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie${devBrowserQuery}`, + )}&suffixed_cookies=false&__clerk_hs_reason=session-token-not-active-yet&__clerk_refresh=no-cookie${devBrowserQuery}`, ); }); @@ -280,7 +280,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://example.com/clerk/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie${devBrowserQuery}`, + )}&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie${devBrowserQuery}`, ); }); @@ -304,7 +304,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://example.com/clerk/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie`, + )}&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie`, ); }); @@ -328,7 +328,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://${config.pkHost}/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie${devBrowserQuery}`, + )}&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie${devBrowserQuery}`, ); }); @@ -352,7 +352,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://clerk.example.com/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie`, + )}&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie`, ); }); @@ -555,7 +555,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://${config.pkHost}/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}hello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie${devBrowserQuery}`, + )}hello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie${devBrowserQuery}`, ); }); @@ -578,7 +578,7 @@ test.describe('Client handshake @generic', () => { expect(res.headers.get('location')).toBe( `https://${config.pkHost}/v1/client/handshake?redirect_url=${encodeURIComponent( `${app.serverUrl}/`, - )}hello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie`, + )}hello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie`, ); }); @@ -601,7 +601,7 @@ test.describe('Client handshake @generic', () => { }); expect(res.status).toBe(307); expect(res.headers.get('location')).toBe( - `https://${config.pkHost}/v1/client/handshake?redirect_url=https%3A%2F%2Fexample.com%2Fhello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie${devBrowserQuery}`, + `https://${config.pkHost}/v1/client/handshake?redirect_url=https%3A%2F%2Fexample.com%2Fhello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie${devBrowserQuery}`, ); }); @@ -624,7 +624,7 @@ test.describe('Client handshake @generic', () => { }); expect(res.status).toBe(307); expect(res.headers.get('location')).toBe( - `https://${config.pkHost}/v1/client/handshake?redirect_url=https%3A%2F%2Fexample.com%2Fhello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie`, + `https://${config.pkHost}/v1/client/handshake?redirect_url=https%3A%2F%2Fexample.com%2Fhello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie`, ); }); @@ -647,7 +647,7 @@ test.describe('Client handshake @generic', () => { }); expect(res.status).toBe(307); expect(res.headers.get('location')).toBe( - `https://${config.pkHost}/v1/client/handshake?redirect_url=https%3A%2F%2Fexample.com%3A3213%2Fhello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie${devBrowserQuery}`, + `https://${config.pkHost}/v1/client/handshake?redirect_url=https%3A%2F%2Fexample.com%3A3213%2Fhello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie${devBrowserQuery}`, ); }); @@ -670,7 +670,7 @@ test.describe('Client handshake @generic', () => { }); expect(res.status).toBe(307); expect(res.headers.get('location')).toBe( - `https://${config.pkHost}/v1/client/handshake?redirect_url=https%3A%2F%2Fexample.com%3A3213%2Fhello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-outdated&__clerk_refresh=no-cookie`, + `https://${config.pkHost}/v1/client/handshake?redirect_url=https%3A%2F%2Fexample.com%3A3213%2Fhello%3Ffoo%3Dbar&suffixed_cookies=false&__clerk_hs_reason=session-token-expired&__clerk_refresh=no-cookie`, ); });