@@ -22,6 +22,13 @@ export type TypedResponse<T> = Response & {
22
22
json ( ) : Promise < T > ;
23
23
} ;
24
24
25
+ export class SessionRefreshError extends Error {
26
+ constructor ( cause : unknown ) {
27
+ super ( 'Session refresh error' , { cause } ) ;
28
+ this . name = 'SessionRefreshError' ;
29
+ }
30
+ }
31
+
25
32
/**
26
33
* This function is used to refresh the session by using the refresh token.
27
34
* It will authenticate the user with the refresh token and return a new session object.
@@ -89,7 +96,7 @@ export async function refreshSession(request: Request, { organizationId }: { org
89
96
90
97
async function updateSession ( request : Request , debug : boolean ) {
91
98
const session = await getSessionFromCookie ( request . headers . get ( 'Cookie' ) as string ) ;
92
- const { commitSession, getSession, destroySession } = await getSessionStorage ( ) ;
99
+ const { commitSession, getSession } = await getSessionStorage ( ) ;
93
100
94
101
// If no session, just continue
95
102
if ( ! session ) {
@@ -138,13 +145,7 @@ async function updateSession(request: Request, debug: boolean) {
138
145
// istanbul ignore next
139
146
if ( debug ) console . log ( 'Failed to refresh. Deleting cookie and redirecting.' , e ) ;
140
147
141
- const cookieSession = await getSession ( request . headers . get ( 'Cookie' ) ) ;
142
-
143
- throw redirect ( '/' , {
144
- headers : {
145
- 'Set-Cookie' : await destroySession ( cookieSession ) ,
146
- } ,
147
- } ) ;
148
+ throw new SessionRefreshError ( e ) ;
148
149
}
149
150
}
150
151
@@ -287,6 +288,8 @@ export async function authkitLoader<Data = unknown>(
287
288
const {
288
289
ensureSignedIn = false ,
289
290
debug = false ,
291
+ onSessionRefreshSuccess,
292
+ onSessionRefreshError,
290
293
storage,
291
294
cookie,
292
295
} = typeof loaderOrOptions === 'object' ? loaderOrOptions : options ;
@@ -295,62 +298,107 @@ export async function authkitLoader<Data = unknown>(
295
298
const { getSession, destroySession } = await configureSessionStorage ( { storage, cookieName } ) ;
296
299
297
300
const { request } = loaderArgs ;
298
- const session = await updateSession ( request , debug ) ;
299
301
300
- if ( ! session ) {
301
- if ( ensureSignedIn ) {
302
- const returnPathname = getReturnPathname ( request . url ) ;
302
+ try {
303
+ // Try to get session, this might throw SessionRefreshError
304
+ const session = await updateSession ( request , debug ) ;
305
+
306
+ if ( ! session ) {
307
+ // No session found case (not authenticated)
308
+ if ( ensureSignedIn ) {
309
+ const returnPathname = getReturnPathname ( request . url ) ;
310
+ const cookieSession = await getSession ( request . headers . get ( 'Cookie' ) ) ;
311
+
312
+ throw redirect ( await getAuthorizationUrl ( { returnPathname } ) , {
313
+ headers : {
314
+ 'Set-Cookie' : await destroySession ( cookieSession ) ,
315
+ } ,
316
+ } ) ;
317
+ }
318
+
319
+ const auth : UnauthorizedData = {
320
+ user : null ,
321
+ accessToken : null ,
322
+ impersonator : null ,
323
+ organizationId : null ,
324
+ permissions : null ,
325
+ entitlements : null ,
326
+ role : null ,
327
+ sessionId : null ,
328
+ sealedSession : null ,
329
+ } ;
330
+
331
+ return await handleAuthLoader ( loader , loaderArgs , auth ) ;
332
+ }
333
+
334
+ // Session found and valid (or refreshed successfully)
335
+ const {
336
+ sessionId,
337
+ organizationId = null ,
338
+ role = null ,
339
+ permissions = [ ] ,
340
+ entitlements = [ ] ,
341
+ } = getClaimsFromAccessToken ( session . accessToken ) ;
342
+
343
+ const cookieSession = await getSession ( request . headers . get ( 'Cookie' ) ) ;
344
+ const { impersonator = null } = session ;
345
+
346
+ // checking for 'headers' in session determines if the session was refreshed or not
347
+ if ( onSessionRefreshSuccess && 'headers' in session ) {
348
+ await onSessionRefreshSuccess ( {
349
+ accessToken : session . accessToken ,
350
+ user : session . user ,
351
+ impersonator,
352
+ organizationId,
353
+ } ) ;
354
+ }
355
+
356
+ const auth : AuthorizedData = {
357
+ user : session . user ,
358
+ sessionId,
359
+ accessToken : session . accessToken ,
360
+ organizationId,
361
+ role,
362
+ permissions,
363
+ entitlements,
364
+ impersonator,
365
+ sealedSession : cookieSession . get ( 'jwt' ) ,
366
+ } ;
367
+
368
+ return await handleAuthLoader ( loader , loaderArgs , auth , session ) ;
369
+ } catch ( error ) {
370
+ if ( error instanceof SessionRefreshError ) {
303
371
const cookieSession = await getSession ( request . headers . get ( 'Cookie' ) ) ;
304
372
305
- throw redirect ( await getAuthorizationUrl ( { returnPathname } ) , {
373
+ if ( onSessionRefreshError ) {
374
+ try {
375
+ const result = await onSessionRefreshError ( {
376
+ error : error . cause ,
377
+ request,
378
+ sessionData : cookieSession ,
379
+ } ) ;
380
+
381
+ if ( result instanceof Response ) {
382
+ return result ;
383
+ }
384
+ } catch ( callbackError ) {
385
+ // If callback throws a Response (like redirect), propagate it
386
+ if ( callbackError instanceof Response ) {
387
+ throw callbackError ;
388
+ }
389
+ }
390
+ }
391
+
392
+ throw redirect ( '/' , {
306
393
headers : {
307
394
'Set-Cookie' : await destroySession ( cookieSession ) ,
308
395
} ,
309
396
} ) ;
310
397
}
311
398
312
- const auth : UnauthorizedData = {
313
- user : null ,
314
- accessToken : null ,
315
- impersonator : null ,
316
- organizationId : null ,
317
- permissions : null ,
318
- entitlements : null ,
319
- role : null ,
320
- sessionId : null ,
321
- sealedSession : null ,
322
- } ;
323
-
324
- return await handleAuthLoader ( loader , loaderArgs , auth ) ;
399
+ // Propagate other errors
400
+ throw error ;
325
401
}
326
-
327
- // istanbul ignore next
328
- const {
329
- sessionId,
330
- organizationId = null ,
331
- role = null ,
332
- permissions = [ ] ,
333
- entitlements = [ ] ,
334
- } = getClaimsFromAccessToken ( session . accessToken ) ;
335
-
336
- const cookieSession = await getSession ( request . headers . get ( 'Cookie' ) ) ;
337
-
338
- // istanbul ignore next
339
- const { impersonator = null } = session ;
340
-
341
- const auth : AuthorizedData = {
342
- user : session . user ,
343
- sessionId,
344
- accessToken : session . accessToken ,
345
- organizationId,
346
- role,
347
- permissions,
348
- entitlements,
349
- impersonator,
350
- sealedSession : cookieSession . get ( 'jwt' ) ,
351
- } ;
352
-
353
- return await handleAuthLoader ( loader , loaderArgs , auth , session ) ;
354
402
}
355
403
356
404
async function handleAuthLoader (
0 commit comments