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,
});
let isRefreshing = false;
let refreshQueue: Array<(token: string) => void> = [];
axiosInstance.interceptors.request.use(async (config) => {
// If the access token is still valid, use it
if (tokenExpirationAt && moment().isBefore(tokenExpirationAt)) {
config.headers.Authorization = `Bearer ${cachedAccessToken}`;
return config;
}
// Otherwise, get a new access token
const session = await getSession();
if (!session) {
throw new Error("User is not authenticated");
if (isRefreshing) {
// Add the request to the queue
return new Promise((resolve) => {
refreshQueue.push((token: string) => {
config.headers.Authorization = `Bearer ${token}`;
resolve(config);
});
});
}
// Cache the new access token
cachedAccessToken = session.accessToken;
tokenExpirationAt = moment(session.accessTokenExpires);
isRefreshing = true;
// Use the new access token
config.headers.Authorization = `Bearer ${cachedAccessToken}`;
return config;
try {
const session = await getSession();
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;
}
if (moment().isBefore(moment(token.accessTokenExpires))) {
if (
moment().isBefore(
moment(token.accessTokenExpires).subtract(5, "s"),
)
) {
return token;
}
@ -85,41 +89,55 @@ export const authOptions: AuthOptions = {
},
};
const refreshAccessToken = async (token: JWT): Promise<JWT> => {
const response = await axios.post<{
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",
},
},
);
let isRefreshing = false;
let refreshPromise: Promise<JWT> | null = null;
if (response.status != 200) {
throw new Error(
response.data.error_description || "Failed to refresh access token",
);
const refreshAccessToken = async (token: JWT): Promise<JWT> => {
// If another request is already refreshing the token, wait for it to finish
if (isRefreshing) {
return refreshPromise!;
}
return {
...token,
accessToken: response.data.access_token,
accessTokenExpires: moment()
.add(response.data.expires_in, "seconds")
.subtract(5, "s"),
refreshToken: response.data.refresh_token,
};
isRefreshing = true;
refreshPromise = (async () => {
try {
const response = await axios.post<{
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) 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());