feat: enhance token refresh logic with request queue and improved error handling

This commit is contained in:
Rémi 2025-01-06 16:05:25 +01:00
parent b3c6ae2460
commit 845381e84d
2 changed files with 80 additions and 44 deletions

View File

@ -11,24 +11,42 @@ export const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL, baseURL: process.env.NEXT_PUBLIC_API_URL,
}); });
let isRefreshing = false;
let refreshQueue: Array<(token: string) => void> = [];
axiosInstance.interceptors.request.use(async (config) => { axiosInstance.interceptors.request.use(async (config) => {
// If the access token is still valid, use it
if (tokenExpirationAt && moment().isBefore(tokenExpirationAt)) { if (tokenExpirationAt && moment().isBefore(tokenExpirationAt)) {
config.headers.Authorization = `Bearer ${cachedAccessToken}`; config.headers.Authorization = `Bearer ${cachedAccessToken}`;
return config; return config;
} }
// Otherwise, get a new access token if (isRefreshing) {
// Add the request to the queue
return new Promise((resolve) => {
refreshQueue.push((token: string) => {
config.headers.Authorization = `Bearer ${token}`;
resolve(config);
});
});
}
isRefreshing = true;
try {
const session = await getSession(); const session = await getSession();
if (!session) { if (!session) {
throw new Error("User is not authenticated"); throw new Error("User is not authenticated");
} }
// Cache the new access token
cachedAccessToken = session.accessToken; cachedAccessToken = session.accessToken;
tokenExpirationAt = moment(session.accessTokenExpires); tokenExpirationAt = moment(session.accessTokenExpires);
// Use the new access token // Execute the queue
refreshQueue.forEach((cb) => cb(cachedAccessToken!));
refreshQueue = [];
config.headers.Authorization = `Bearer ${cachedAccessToken}`; config.headers.Authorization = `Bearer ${cachedAccessToken}`;
return config; return config;
} finally {
isRefreshing = false;
}
}); });

View File

@ -62,7 +62,11 @@ export const authOptions: AuthOptions = {
return token; return token;
} }
if (moment().isBefore(moment(token.accessTokenExpires))) { if (
moment().isBefore(
moment(token.accessTokenExpires).subtract(5, "s"),
)
) {
return token; return token;
} }
@ -85,14 +89,25 @@ export const authOptions: AuthOptions = {
}, },
}; };
let isRefreshing = false;
let refreshPromise: Promise<JWT> | null = null;
const refreshAccessToken = async (token: JWT): Promise<JWT> => { const refreshAccessToken = async (token: JWT): Promise<JWT> => {
// If another request is already refreshing the token, wait for it to finish
if (isRefreshing) {
return refreshPromise!;
}
isRefreshing = true;
refreshPromise = (async () => {
try {
const response = await axios.post<{ const response = await axios.post<{
access_token: string; access_token: string;
expires_in: number; expires_in: number;
refresh_token: string; refresh_token: string;
error_description?: string; error_description?: string;
}>( }>(
process.env.OAUTH_TOKEN_URL, process.env.OAUTH_TOKEN_URL!,
{ {
grant_type: "refresh_token", grant_type: "refresh_token",
refresh_token: token.refreshToken, refresh_token: token.refreshToken,
@ -106,11 +121,7 @@ const refreshAccessToken = async (token: JWT): Promise<JWT> => {
}, },
); );
if (response.status != 200) { if (response.status !== 200) throw response.data;
throw new Error(
response.data.error_description || "Failed to refresh access token",
);
}
return { return {
...token, ...token,
@ -120,6 +131,13 @@ const refreshAccessToken = async (token: JWT): Promise<JWT> => {
.subtract(5, "s"), .subtract(5, "s"),
refreshToken: response.data.refresh_token, refreshToken: response.data.refresh_token,
}; };
} finally {
isRefreshing = false;
refreshPromise = null;
}
})();
return refreshPromise;
}; };
export const getSession = cache(() => getServerSession()); export const getSession = cache(() => getServerSession());