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) {
const session = await getSession(); // Add the request to the queue
if (!session) { return new Promise((resolve) => {
throw new Error("User is not authenticated"); refreshQueue.push((token: string) => {
config.headers.Authorization = `Bearer ${token}`;
resolve(config);
});
});
} }
// Cache the new access token isRefreshing = true;
cachedAccessToken = session.accessToken;
tokenExpirationAt = moment(session.accessTokenExpires);
// Use the new access token try {
config.headers.Authorization = `Bearer ${cachedAccessToken}`; const session = await getSession();
return config; if (!session) {
throw new Error("User is not authenticated");
}
cachedAccessToken = session.accessToken;
tokenExpirationAt = moment(session.accessTokenExpires);
// Execute the queue
refreshQueue.forEach((cb) => cb(cachedAccessToken!));
refreshQueue = [];
config.headers.Authorization = `Bearer ${cachedAccessToken}`;
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,41 +89,55 @@ export const authOptions: AuthOptions = {
}, },
}; };
const refreshAccessToken = async (token: JWT): Promise<JWT> => { let isRefreshing = false;
const response = await axios.post<{ let refreshPromise: Promise<JWT> | null = null;
access_token: string;
expires_in: number;
refresh_token: string;
error_description?: string;
}>(
process.env.OAUTH_TOKEN_URL,
{
grant_type: "refresh_token",
refresh_token: token.refreshToken,
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
);
if (response.status != 200) { const refreshAccessToken = async (token: JWT): Promise<JWT> => {
throw new Error( // If another request is already refreshing the token, wait for it to finish
response.data.error_description || "Failed to refresh access token", if (isRefreshing) {
); return refreshPromise!;
} }
return { isRefreshing = true;
...token, refreshPromise = (async () => {
accessToken: response.data.access_token, try {
accessTokenExpires: moment() const response = await axios.post<{
.add(response.data.expires_in, "seconds") access_token: string;
.subtract(5, "s"), expires_in: number;
refreshToken: response.data.refresh_token, refresh_token: string;
}; error_description?: string;
}>(
process.env.OAUTH_TOKEN_URL!,
{
grant_type: "refresh_token",
refresh_token: token.refreshToken,
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
);
if (response.status !== 200) throw response.data;
return {
...token,
accessToken: response.data.access_token,
accessTokenExpires: moment()
.add(response.data.expires_in, "seconds")
.subtract(5, "s"),
refreshToken: response.data.refresh_token,
};
} finally {
isRefreshing = false;
refreshPromise = null;
}
})();
return refreshPromise;
}; };
export const getSession = cache(() => getServerSession()); export const getSession = cache(() => getServerSession());