@@ -316,6 +316,45 @@ export type GetAccessTokenFunction = {
316316 ( options : GetTokenSilentlyOptions ) : Promise < any > ;
317317} ;
318318
319+ const parseJwtPayload = ( token : string ) : JWTPayload | null => {
320+ try {
321+ const [ , payload ] = token . split ( "." ) ;
322+ if ( ! payload ) return null ;
323+
324+ // base64url decoding
325+ const base64 = payload . replace ( / - / g, "+" ) . replace ( / _ / g, "/" ) ;
326+ const json = atob ( base64 ) ;
327+ return JSON . parse ( json ) as JWTPayload ;
328+ } catch {
329+ return null ;
330+ }
331+ } ;
332+
333+ const getAccessTokenWithAutoRefresh = async (
334+ getAccessTokenSilently : GetAccessTokenFunction ,
335+ options : GetTokenSilentlyOptions ,
336+ ) => {
337+ const accessToken = await getAccessTokenSilently ( options ) ;
338+ const payload = parseJwtPayload ( accessToken ) ;
339+
340+ // If we cannot parse an expiry, just return what we got
341+ if ( ! payload ?. exp ) {
342+ return accessToken ;
343+ }
344+
345+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
346+ const secondsLeft = payload . exp - now ;
347+
348+ // If the token is expired (or about to), force refresh via ignoreCache.
349+ // A small buffer helps avoid using a token right before it expires.
350+ if ( secondsLeft < 10 ) {
351+ const refreshOptions = { ...options , ignoreCache : true } as any ;
352+ return getAccessTokenSilently ( refreshOptions ) ;
353+ }
354+
355+ return accessToken ;
356+ } ;
357+
319358/**
320359 * Fetches JSON data from a secured API endpoint using Auth0 token authentication.
321360 * Handles the token acquisition and authorization header setup automatically.
@@ -334,19 +373,42 @@ export const getJsonFromSecuredApi = async (
334373 url : string ,
335374 getAccessTokenFunction : GetAccessTokenFunction ,
336375) => {
337- try {
338- const accessToken = await getAccessTokenFunction ( {
376+ const getToken = ( ) =>
377+ getAccessTokenWithAutoRefresh ( getAccessTokenFunction , {
339378 authorizationParams : {
340379 audience : import . meta. env . AUTH0_AUDIENCE ,
341380 scope : import . meta. env . AUTH0_SCOPE ,
342381 } ,
343382 } ) ;
383+
384+ try {
385+ const accessToken = await getToken ( ) ;
386+
344387 const apiResponse = await fetch ( url , {
345388 headers : {
346389 Authorization : `Bearer ${ accessToken } ` ,
347390 } ,
348391 } ) ;
349392
393+ // If server rejects, try to refresh and retry once
394+ if ( apiResponse . status === 401 ) {
395+ const refreshedToken = await getAccessTokenFunction ( {
396+ authorizationParams : {
397+ audience : import . meta. env . AUTH0_AUDIENCE ,
398+ scope : import . meta. env . AUTH0_SCOPE ,
399+ } ,
400+ ignoreCache : true ,
401+ } as any ) ;
402+
403+ const retryResponse = await fetch ( url , {
404+ headers : {
405+ Authorization : `Bearer ${ refreshedToken } ` ,
406+ } ,
407+ } ) ;
408+
409+ return await retryResponse . json ( ) ;
410+ }
411+
350412 return await apiResponse . json ( ) ;
351413 } catch ( error ) {
352414 // eslint-disable-next-line no-console
@@ -368,13 +430,17 @@ export const postJsonToSecuredApi = async (
368430 data : any ,
369431 getAccessTokenFunction : GetAccessTokenFunction ,
370432) => {
371- try {
372- const accessToken = await getAccessTokenFunction ( {
433+ const getToken = ( ) =>
434+ getAccessTokenWithAutoRefresh ( getAccessTokenFunction , {
373435 authorizationParams : {
374436 audience : import . meta. env . AUTH0_AUDIENCE ,
375437 scope : import . meta. env . AUTH0_SCOPE ,
376438 } ,
377439 } ) ;
440+
441+ try {
442+ const accessToken = await getToken ( ) ;
443+
378444 const apiResponse = await fetch ( url , {
379445 method : "POST" ,
380446 headers : {
@@ -384,6 +450,27 @@ export const postJsonToSecuredApi = async (
384450 body : JSON . stringify ( data ) ,
385451 } ) ;
386452
453+ if ( apiResponse . status === 401 ) {
454+ const refreshedToken = await getAccessTokenFunction ( {
455+ authorizationParams : {
456+ audience : import . meta. env . AUTH0_AUDIENCE ,
457+ scope : import . meta. env . AUTH0_SCOPE ,
458+ } ,
459+ ignoreCache : true ,
460+ } as any ) ;
461+
462+ const retryResponse = await fetch ( url , {
463+ method : "POST" ,
464+ headers : {
465+ Authorization : `Bearer ${ refreshedToken } ` ,
466+ "Content-Type" : "application/json" ,
467+ } ,
468+ body : JSON . stringify ( data ) ,
469+ } ) ;
470+
471+ return await retryResponse . json ( ) ;
472+ }
473+
387474 return await apiResponse . json ( ) ;
388475 } catch ( error ) {
389476 // eslint-disable-next-line no-console
@@ -403,13 +490,17 @@ export const deleteJsonFromSecuredApi = async (
403490 url : string ,
404491 getAccessTokenFunction : GetAccessTokenFunction ,
405492) => {
406- try {
407- const accessToken = await getAccessTokenFunction ( {
493+ const getToken = ( ) =>
494+ getAccessTokenWithAutoRefresh ( getAccessTokenFunction , {
408495 authorizationParams : {
409496 audience : import . meta. env . AUTH0_AUDIENCE ,
410497 scope : import . meta. env . AUTH0_SCOPE ,
411498 } ,
412499 } ) ;
500+
501+ try {
502+ const accessToken = await getToken ( ) ;
503+
413504 const apiResponse = await fetch ( url , {
414505 method : "DELETE" ,
415506 headers : {
@@ -418,6 +509,26 @@ export const deleteJsonFromSecuredApi = async (
418509 } ,
419510 } ) ;
420511
512+ if ( apiResponse . status === 401 ) {
513+ const refreshedToken = await getAccessTokenFunction ( {
514+ authorizationParams : {
515+ audience : import . meta. env . AUTH0_AUDIENCE ,
516+ scope : import . meta. env . AUTH0_SCOPE ,
517+ } ,
518+ ignoreCache : true ,
519+ } as any ) ;
520+
521+ const retryResponse = await fetch ( url , {
522+ method : "DELETE" ,
523+ headers : {
524+ Authorization : `Bearer ${ refreshedToken } ` ,
525+ "Content-Type" : "application/json" ,
526+ } ,
527+ } ) ;
528+
529+ return await retryResponse . json ( ) ;
530+ }
531+
421532 return await apiResponse . json ( ) ;
422533 } catch ( error ) {
423534 // eslint-disable-next-line no-console
@@ -443,19 +554,40 @@ export const useSecuredApi = () => {
443554 const getJson = useCallback (
444555 async ( url : string ) => {
445556 try {
446- const accessToken = await getAccessTokenSilently ( {
447- authorizationParams : {
448- audience : import . meta. env . AUTH0_AUDIENCE ,
449- scope : import . meta. env . AUTH0_SCOPE ,
557+ const accessToken = await getAccessTokenWithAutoRefresh (
558+ getAccessTokenSilently ,
559+ {
560+ authorizationParams : {
561+ audience : import . meta. env . AUTH0_AUDIENCE ,
562+ scope : import . meta. env . AUTH0_SCOPE ,
563+ } ,
450564 } ,
451- } ) ;
565+ ) ;
452566
453567 const apiResponse = await fetch ( url , {
454568 headers : {
455569 Authorization : `Bearer ${ accessToken } ` ,
456570 } ,
457571 } ) ;
458572
573+ if ( apiResponse . status === 401 ) {
574+ const refreshedToken = await getAccessTokenSilently ( {
575+ authorizationParams : {
576+ audience : import . meta. env . AUTH0_AUDIENCE ,
577+ scope : import . meta. env . AUTH0_SCOPE ,
578+ } ,
579+ ignoreCache : true ,
580+ } as any ) ;
581+
582+ const retryResponse = await fetch ( url , {
583+ headers : {
584+ Authorization : `Bearer ${ refreshedToken } ` ,
585+ } ,
586+ } ) ;
587+
588+ return await retryResponse . json ( ) ;
589+ }
590+
459591 return await apiResponse . json ( ) ;
460592 } catch ( error ) {
461593 // eslint-disable-next-line no-console
@@ -469,12 +601,15 @@ export const useSecuredApi = () => {
469601 const postJson = useCallback (
470602 async ( url : string , data : any ) => {
471603 try {
472- const accessToken = await getAccessTokenSilently ( {
473- authorizationParams : {
474- audience : import . meta. env . AUTH0_AUDIENCE ,
475- scope : import . meta. env . AUTH0_SCOPE ,
604+ const accessToken = await getAccessTokenWithAutoRefresh (
605+ getAccessTokenSilently ,
606+ {
607+ authorizationParams : {
608+ audience : import . meta. env . AUTH0_AUDIENCE ,
609+ scope : import . meta. env . AUTH0_SCOPE ,
610+ } ,
476611 } ,
477- } ) ;
612+ ) ;
478613
479614 const apiResponse = await fetch ( url , {
480615 method : "POST" ,
@@ -485,6 +620,27 @@ export const useSecuredApi = () => {
485620 body : JSON . stringify ( data ) ,
486621 } ) ;
487622
623+ if ( apiResponse . status === 401 ) {
624+ const refreshedToken = await getAccessTokenSilently ( {
625+ authorizationParams : {
626+ audience : import . meta. env . AUTH0_AUDIENCE ,
627+ scope : import . meta. env . AUTH0_SCOPE ,
628+ } ,
629+ ignoreCache : true ,
630+ } as any ) ;
631+
632+ const retryResponse = await fetch ( url , {
633+ method : "POST" ,
634+ headers : {
635+ Authorization : `Bearer ${ refreshedToken } ` ,
636+ "Content-Type" : "application/json" ,
637+ } ,
638+ body : JSON . stringify ( data ) ,
639+ } ) ;
640+
641+ return await retryResponse . json ( ) ;
642+ }
643+
488644 return await apiResponse . json ( ) ;
489645 } catch ( error ) {
490646 // eslint-disable-next-line no-console
@@ -498,12 +654,15 @@ export const useSecuredApi = () => {
498654 const deleteJson = useCallback (
499655 async ( url : string , data ?: any ) => {
500656 try {
501- const accessToken = await getAccessTokenSilently ( {
502- authorizationParams : {
503- audience : import . meta. env . AUTH0_AUDIENCE ,
504- scope : import . meta. env . AUTH0_SCOPE ,
657+ const accessToken = await getAccessTokenWithAutoRefresh (
658+ getAccessTokenSilently ,
659+ {
660+ authorizationParams : {
661+ audience : import . meta. env . AUTH0_AUDIENCE ,
662+ scope : import . meta. env . AUTH0_SCOPE ,
663+ } ,
505664 } ,
506- } ) ;
665+ ) ;
507666
508667 const apiResponse = await fetch ( url , {
509668 method : "DELETE" ,
@@ -514,6 +673,27 @@ export const useSecuredApi = () => {
514673 body : data ? JSON . stringify ( data ) : undefined ,
515674 } ) ;
516675
676+ if ( apiResponse . status === 401 ) {
677+ const refreshedToken = await getAccessTokenSilently ( {
678+ authorizationParams : {
679+ audience : import . meta. env . AUTH0_AUDIENCE ,
680+ scope : import . meta. env . AUTH0_SCOPE ,
681+ } ,
682+ ignoreCache : true ,
683+ } as any ) ;
684+
685+ const retryResponse = await fetch ( url , {
686+ method : "DELETE" ,
687+ headers : {
688+ Authorization : `Bearer ${ refreshedToken } ` ,
689+ "Content-Type" : "application/json" ,
690+ } ,
691+ body : data ? JSON . stringify ( data ) : undefined ,
692+ } ) ;
693+
694+ return await retryResponse . json ( ) ;
695+ }
696+
517697 return await apiResponse . json ( ) ;
518698 } catch ( error ) {
519699 // eslint-disable-next-line no-console
0 commit comments