Skip to content

Commit 03cb24e

Browse files
committed
handle jwt autorefresh
1 parent 8aac48a commit 03cb24e

1 file changed

Lines changed: 201 additions & 21 deletions

File tree

apps/client/src/components/auth0.tsx

Lines changed: 201 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)