feat: enhance token refresh logic with request queue and improved error handling
This commit is contained in:
parent
b3c6ae2460
commit
845381e84d
@ -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;
|
||||
}
|
||||
});
|
||||
|
@ -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());
|
||||
|
Loading…
Reference in New Issue
Block a user