11import type { JwtPayload } from '@clerk/types' ;
22
3- import type { ApiClient } from '../api' ;
43import { constants } from '../constants' ;
54import type { TokenCarrier } from '../errors' ;
65import { TokenVerificationError , TokenVerificationErrorReason } from '../errors' ;
@@ -17,6 +16,18 @@ import { verifyHandshakeToken } from './handshake';
1716import type { AuthenticateRequestOptions } from './types' ;
1817import { verifyToken } from './verify' ;
1918
19+ const RefreshTokenErrorReason = {
20+ NoCookie : 'no-cookie' ,
21+ NonEligible : 'non-eligible' ,
22+ InvalidSessionToken : 'invalid-session-token' ,
23+ MissingApiClient : 'missing-api-client' ,
24+ MissingSessionToken : 'missing-session-token' ,
25+ MissingRefreshToken : 'missing-refresh-token' ,
26+ SessionTokenDecodeFailed : 'session-token-decode-failed' ,
27+ FetchNetworkError : 'fetch-network-error' ,
28+ UnexpectedRefreshError : 'unexpected-refresh-error' ,
29+ } as const ;
30+
2031function assertSignInUrlExists ( signInUrl : string | undefined , key : string ) : asserts signInUrl is string {
2132 if ( ! signInUrl && isDevelopmentFromSecretKey ( key ) ) {
2233 throw new Error ( `Missing signInUrl. Pass a signInUrl for dev instances if an app is satellite` ) ;
@@ -42,12 +53,6 @@ function assertSignInUrlFormatAndOrigin(_signInUrl: string, origin: string) {
4253 }
4354}
4455
45- function assertApiClient ( apiClient : ApiClient | undefined ) : asserts apiClient is ApiClient {
46- if ( ! apiClient ) {
47- throw new Error ( `Missing apiClient. An apiClient is needed to perform token refresh.` ) ;
48- }
49- }
50-
5156/**
5257 * 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.
5358 * This heuristic should give us a reliable enough signal for browsers that support `Sec-Fetch-Dest` and for those that don't.
@@ -195,42 +200,75 @@ ${error.getFullMessage()}`,
195200 throw error ;
196201 }
197202
198- async function refreshToken ( authenticateContext : AuthenticateContext ) : Promise < string > {
203+ async function refreshToken (
204+ authenticateContext : AuthenticateContext ,
205+ ) : Promise < { data : string ; error : null } | { data : null ; error : any } > {
199206 // To perform a token refresh, apiClient must be defined.
200- assertApiClient ( options . apiClient ) ;
207+ if ( ! options . apiClient ) {
208+ return {
209+ data : null ,
210+ error : {
211+ message : 'An apiClient is needed to perform token refresh.' ,
212+ cause : { reason : RefreshTokenErrorReason . MissingApiClient } ,
213+ } ,
214+ } ;
215+ }
201216 const { sessionToken : expiredSessionToken , refreshTokenInCookie : refreshToken } = authenticateContext ;
202- if ( ! expiredSessionToken || ! refreshToken ) {
203- throw new Error ( 'Clerk: refreshTokenInCookie and sessionToken must be provided.' ) ;
217+ if ( ! expiredSessionToken ) {
218+ return {
219+ data : null ,
220+ error : {
221+ message : 'Session token must be provided.' ,
222+ cause : { reason : RefreshTokenErrorReason . MissingSessionToken } ,
223+ } ,
224+ } ;
225+ }
226+ if ( ! refreshToken ) {
227+ return {
228+ data : null ,
229+ error : {
230+ message : 'Refresh token must be provided.' ,
231+ cause : { reason : RefreshTokenErrorReason . MissingRefreshToken } ,
232+ } ,
233+ } ;
204234 }
205235 // The token refresh endpoint requires a sessionId, so we decode that from the expired token.
206236 const { data : decodeResult , errors : decodedErrors } = decodeJwt ( expiredSessionToken ) ;
207237 if ( ! decodeResult || decodedErrors ) {
208- throw new Error ( `Clerk: unable to decode session token.` ) ;
209- }
210- // Perform the actual token refresh.
211- const tokenResponse = await options . apiClient . sessions . refreshSession ( decodeResult . payload . sid , {
212- expired_token : expiredSessionToken || '' ,
213- refresh_token : refreshToken || '' ,
214- request_origin : authenticateContext . clerkUrl . origin ,
215- // The refresh endpoint expects headers as Record<string, string[]>, so we need to transform it.
216- request_headers : Object . fromEntries ( Array . from ( request . headers . entries ( ) ) . map ( ( [ k , v ] ) => [ k , [ v ] ] ) ) ,
217- } ) ;
218-
219- return tokenResponse . jwt ;
220- }
238+ return {
239+ data : null ,
240+ error : {
241+ message : 'Unable to decode session token.' ,
242+ cause : { reason : RefreshTokenErrorReason . SessionTokenDecodeFailed , errors : decodedErrors } ,
243+ } ,
244+ } ;
245+ }
221246
222- async function attemptRefresh (
223- authenticateContext : AuthenticateContext ,
224- ) : Promise < { data : { jwtPayload : JwtPayload ; sessionToken : string } ; error : null } | { data : null ; error : any } > {
225- let sessionToken : string ;
226247 try {
227- sessionToken = await refreshToken ( authenticateContext ) ;
248+ // Perform the actual token refresh.
249+ const tokenResponse = await options . apiClient . sessions . refreshSession ( decodeResult . payload . sid , {
250+ expired_token : expiredSessionToken || '' ,
251+ refresh_token : refreshToken || '' ,
252+ request_origin : authenticateContext . clerkUrl . origin ,
253+ // The refresh endpoint expects headers as Record<string, string[]>, so we need to transform it.
254+ request_headers : Object . fromEntries ( Array . from ( request . headers . entries ( ) ) . map ( ( [ k , v ] ) => [ k , [ v ] ] ) ) ,
255+ } ) ;
256+ return { data : tokenResponse . jwt , error : null } ;
228257 } catch ( err : any ) {
229258 if ( err ?. errors ?. length ) {
259+ if ( err . errors [ 0 ] . code === 'unexpected_error' ) {
260+ return {
261+ data : null ,
262+ error : {
263+ message : `Fetch unexpected error` ,
264+ cause : { reason : RefreshTokenErrorReason . FetchNetworkError , errors : err . errors } ,
265+ } ,
266+ } ;
267+ }
230268 return {
231269 data : null ,
232270 error : {
233- message : `Clerk: unable to refresh session token.` ,
271+ message : err . errors [ 0 ] . code ,
234272 cause : { reason : err . errors [ 0 ] . code , errors : err . errors } ,
235273 } ,
236274 } ;
@@ -241,14 +279,24 @@ ${error.getFullMessage()}`,
241279 } ;
242280 }
243281 }
282+ }
283+
284+ async function attemptRefresh (
285+ authenticateContext : AuthenticateContext ,
286+ ) : Promise < { data : { jwtPayload : JwtPayload ; sessionToken : string } ; error : null } | { data : null ; error : any } > {
287+ const { data : sessionToken , error } = await refreshToken ( authenticateContext ) ;
288+ if ( ! sessionToken ) {
289+ return { data : null , error } ;
290+ }
291+
244292 // Since we're going to return a signedIn response, we need to decode the data from the new sessionToken.
245293 const { data : jwtPayload , errors } = await verifyToken ( sessionToken , authenticateContext ) ;
246294 if ( errors ) {
247295 return {
248296 data : null ,
249297 error : {
250298 message : `Clerk: unable to verify refreshed session token.` ,
251- cause : { reason : 'invalid-session-token' , errors } ,
299+ cause : { reason : RefreshTokenErrorReason . InvalidSessionToken , errors } ,
252300 } ,
253301 } ;
254302 }
@@ -264,7 +312,11 @@ ${error.getFullMessage()}`,
264312 ) : SignedInState | SignedOutState | HandshakeState {
265313 if ( isRequestEligibleForHandshake ( authenticateContext ) ) {
266314 // If a refresh error is not passed in, we default to 'no-cookie' or 'non-eligible'.
267- refreshError = refreshError || ( authenticateContext . refreshTokenInCookie ? 'non-eligible' : 'no-cookie' ) ;
315+ refreshError =
316+ refreshError ||
317+ ( authenticateContext . refreshTokenInCookie
318+ ? RefreshTokenErrorReason . NonEligible
319+ : RefreshTokenErrorReason . NoCookie ) ;
268320
269321 // Right now the only usage of passing in different headers is for multi-domain sync, which redirects somewhere else.
270322 // In the future if we want to decorate the handshake redirect with additional headers per call we need to tweak this logic.
@@ -399,7 +451,9 @@ ${error.getFullMessage()}`,
399451 ) ;
400452 const authErrReason = AuthErrorReason . SatelliteCookieNeedsSyncing ;
401453 redirectURL . searchParams . append ( constants . QueryParameters . HandshakeReason , authErrReason ) ;
402- const refreshTokenError = authenticateContext . refreshTokenInCookie ? 'non-eligible' : 'no-cookie' ;
454+ const refreshTokenError = authenticateContext . refreshTokenInCookie
455+ ? RefreshTokenErrorReason . NonEligible
456+ : RefreshTokenErrorReason . NoCookie ;
403457 redirectURL . searchParams . append ( constants . QueryParameters . RefreshTokenError , refreshTokenError ) ;
404458
405459 const headers = new Headers ( { [ constants . Headers . Location ] : redirectURL . toString ( ) } ) ;
@@ -423,7 +477,9 @@ ${error.getFullMessage()}`,
423477 redirectBackToSatelliteUrl . searchParams . append ( constants . QueryParameters . ClerkSynced , 'true' ) ;
424478 const authErrReason = AuthErrorReason . PrimaryRespondsToSyncing ;
425479 redirectBackToSatelliteUrl . searchParams . append ( constants . QueryParameters . HandshakeReason , authErrReason ) ;
426- const refreshTokenError = authenticateContext . refreshTokenInCookie ? 'non-eligible' : 'no-cookie' ;
480+ const refreshTokenError = authenticateContext . refreshTokenInCookie
481+ ? RefreshTokenErrorReason . NonEligible
482+ : RefreshTokenErrorReason . NoCookie ;
427483 redirectBackToSatelliteUrl . searchParams . append ( constants . QueryParameters . RefreshTokenError , refreshTokenError ) ;
428484
429485 const headers = new Headers ( { [ constants . Headers . Location ] : redirectBackToSatelliteUrl . toString ( ) } ) ;
@@ -481,20 +537,22 @@ ${error.getFullMessage()}`,
481537 return signedOut ( authenticateContext , AuthErrorReason . UnexpectedError ) ;
482538 }
483539
484- let refreshError : string = authenticateContext . refreshTokenInCookie ? 'non-eligible' : 'no-cookie' ;
540+ let refreshError : string = authenticateContext . refreshTokenInCookie
541+ ? RefreshTokenErrorReason . NonEligible
542+ : RefreshTokenErrorReason . NoCookie ;
485543
486544 if ( isRequestEligibleForRefresh ( err , authenticateContext , request ) ) {
487545 const { data, error } = await attemptRefresh ( authenticateContext ) ;
488- if ( ! error ) {
489- return signedIn ( authenticateContext , data ! . jwtPayload , undefined , data ! . sessionToken ) ;
546+ if ( data ) {
547+ return signedIn ( authenticateContext , data . jwtPayload , undefined , data . sessionToken ) ;
490548 }
491549
492550 // If there's any error, simply fallback to the handshake flow.
493551 console . error ( 'Clerk: unable to refresh token:' , error ?. message || error ) ;
494552 if ( error ?. cause ?. reason ) {
495553 refreshError = error . cause . reason ;
496554 } else {
497- refreshError = 'unexpected-refresh-error' ;
555+ refreshError = RefreshTokenErrorReason . UnexpectedRefreshError ;
498556 }
499557 }
500558
0 commit comments