diff --git a/src/auth/index.js b/src/auth/index.js index 7ad7c48d..1ffbf688 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -14,6 +14,7 @@ import {useNavigate} from "react-router-dom"; import {useDispatch} from "react-redux"; import { LOGOUT, REFRESH_TOKEN } from 'utils/ApiPathConst'; import { getMessage } from 'utils/getI18nMessage'; +import { beginRefresh } from 'auth/refreshGate'; // Guard so we only register interceptors once (ThemeRoutes re-renders add duplicate handlers otherwise) let axiosInterceptorsSetup = false; @@ -192,10 +193,10 @@ export const SetupAxiosInterceptors = () => { return Promise.reject(error); } isRefreshToken = true; - return axios + return beginRefresh(() => axios .post(`${apiPath}${REFRESH_TOKEN}`, { - refreshToken: refreshToken // Replace with your refresh token - }) + refreshToken: refreshToken + })) .then((response) => { if (response.status === 200) { const newAccessToken = response.data.accessToken; @@ -261,36 +262,17 @@ export const SetupAxiosInterceptors = () => { } export const handleRefreshTokenFunction = () => { - // const token = localStorage.getItem('accessToken'); - let isRefresh = false; - - if (!isRefresh) { - const refreshToken = localStorage.getItem('refreshToken'); - isRefresh = true; - axios.post(`${apiPath}${REFRESH_TOKEN}`, { - refreshToken: refreshToken - }).then((response) => { - if (response.status === 200) { - const newAccessToken = response.data.accessToken; - const newRefreshToken = response.data.refreshToken; - localStorage.setItem('accessToken', newAccessToken); - localStorage.setItem('refreshToken', newRefreshToken); - // token = newAccessToken; - isRefresh = false; - } else { - // token = null; - isRefresh = false; - } - }) - .catch((refreshError) => { - console.log('Failed to refresh token'); - if (refreshError != undefined){ - console.log(refreshError) - } - // token = null - isRefresh = false; - }); + const refreshToken = localStorage.getItem('refreshToken'); + if (!refreshToken) { + return false; } - - return isRefresh; + beginRefresh(() => axios.post(`${apiPath}${REFRESH_TOKEN}`, { + refreshToken: refreshToken + })).catch((refreshError) => { + console.log('Failed to refresh token'); + if (refreshError != undefined) { + console.log(refreshError) + } + }); + return true; } diff --git a/src/auth/jwt/jwtService.js b/src/auth/jwt/jwtService.js index 430717a0..0e48f9ec 100644 --- a/src/auth/jwt/jwtService.js +++ b/src/auth/jwt/jwtService.js @@ -1,5 +1,6 @@ import axios from 'axios' import jwtDefaultConfig from './jwtDefaultConfig' +import { beginRefresh } from '../refreshGate' export default class JwtService { // ** jwtConfig <= Will be used by this service @@ -126,8 +127,8 @@ export default class JwtService { } refreshToken() { - return axios.post(this.jwtConfig.refreshEndpoint, { + return beginRefresh(() => axios.post(this.jwtConfig.refreshEndpoint, { refreshToken: this.getRefreshToken() - }) + })) } } diff --git a/src/auth/refreshGate.js b/src/auth/refreshGate.js new file mode 100644 index 00000000..b2cbbf6a --- /dev/null +++ b/src/auth/refreshGate.js @@ -0,0 +1,71 @@ +const LOCK_KEY = 'pnspsRefreshLock'; +const LOCK_MS = 15000; + +let inFlight = null; + +function waitForStoredToken(previousRefreshToken) { + return new Promise((resolve, reject) => { + const started = Date.now(); + const timer = setInterval(() => { + const refreshToken = localStorage.getItem('refreshToken'); + const accessToken = localStorage.getItem('accessToken'); + if (refreshToken && accessToken && refreshToken !== previousRefreshToken) { + clearInterval(timer); + resolve({ + status: 200, + data: { accessToken, refreshToken } + }); + return; + } + const lockGone = !localStorage.getItem(LOCK_KEY); + if ((lockGone && Date.now() - started > 500) || Date.now() - started > LOCK_MS) { + clearInterval(timer); + reject(new Error('refresh-lock-released')); + } + }, 200); + }); +} + +export function storeRefreshedTokens(accessToken, refreshToken) { + if (accessToken) { + localStorage.setItem('accessToken', accessToken); + localStorage.setItem('axiosToken', 'Bearer ' + accessToken); + } + if (refreshToken) { + localStorage.setItem('refreshToken', refreshToken); + } +} + +/** + * One refresh at a time in this tab, and a short lock so a second tab waits + * for the new token instead of posting the same refresh token again. + */ +export function beginRefresh(run) { + if (inFlight) { + return inFlight; + } + const previousRefreshToken = localStorage.getItem('refreshToken'); + inFlight = (async () => { + const lockedAt = Number(localStorage.getItem(LOCK_KEY) || 0); + if (lockedAt && Date.now() - lockedAt < LOCK_MS) { + try { + return await waitForStoredToken(previousRefreshToken); + } catch (e) { + // The other tab did not store a new token. Refresh from here. + } + } + localStorage.setItem(LOCK_KEY, String(Date.now())); + try { + const response = await run(); + if (response?.status === 200 && response.data) { + storeRefreshedTokens(response.data.accessToken, response.data.refreshToken); + } + return response; + } finally { + localStorage.removeItem(LOCK_KEY); + } + })().finally(() => { + inFlight = null; + }); + return inFlight; +} diff --git a/src/components/RefreshTokenProvider.js b/src/components/RefreshTokenProvider.js index c1485a3e..98a1bfd4 100644 --- a/src/components/RefreshTokenProvider.js +++ b/src/components/RefreshTokenProvider.js @@ -2,6 +2,7 @@ import { createContext, useEffect, useRef, useCallback } from 'react'; import { apiPath } from 'auth/utils'; import { REFRESH_TOKEN } from 'utils/ApiPathConst'; import axios from 'axios'; +import { beginRefresh } from 'auth/refreshGate'; import { isUserLoggedIn, } from "utils/Utils"; @@ -16,10 +17,10 @@ const RefreshTokenProvider = ({ children }) => { if (!isRefresh.current) { const refreshToken = localStorage.getItem('refreshToken'); isRefresh.current = true; - axios + beginRefresh(() => axios .post(`${apiPath}${REFRESH_TOKEN}`, { refreshToken: refreshToken - }) + })) .then((response) => { if (response.status === 200) { const newAccessToken = response.data.accessToken;