Token Management
UniSouk uses short-lived access tokens (15 minutes) and long-lived refresh tokens stored in HTTP-only cookies for secure authentication. This document explains how token management is implemented using Axios interceptors, including automatic token refresh and handling unauthorized (401) responses.
🎯 Goals
- Automatically refresh access tokens when they expire
- Prevent multiple simultaneous refresh requests
- Gracefully handle session expiry and redirect to login
⚙️ Axios Interceptor Setup
This setup ensures that every request carries the access token, and automatically refreshes it if the server returns a 401 Unauthorized due to token expiration.
import axios from "axios";
// in-memory storage for access token
let accessToken: string | null = null;
export const setAccessToken = (token: string | null) => {
accessToken = token;
};
// Singleton to avoid multiple refresh calls
let refreshTokenPromise: Promise<string> | null = null;
const refreshTokenSingleton = async () => {
if (refreshTokenPromise) return refreshTokenPromise;
refreshTokenPromise = axios
.get(`${BASE_URL}/auth/refresh/${storeId}`, { withCredentials: true })
.then((response) => {
const newAccessToken = response.data.data.accessToken;
setAccessToken(newAccessToken); // Store it in-memory, localStorage, etc.
return newAccessToken;
})
.finally(() => {
refreshTokenPromise = null;
});
return refreshTokenPromise;
};
🔄 Axios Instance with Interceptors
export const api = axios.create({
baseURL: API_URL,
headers: {
"Content-Type": "application/json",
"x-store-id": getStoreId(), // Fetch your current store ID
},
withCredentials: true,
});
Request Interceptor
Adds the access token to the Authorization header before every request.
api.interceptors.request.use(
(config) => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
Response Interceptor
Handles 401 responses by refreshing the token (once) and retrying the original request.
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const newAccessToken = await refreshTokenSingleton();
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return api(originalRequest);
} catch (refreshError) {
if (window.location.pathname.indexOf("/auth/") !== 0) {
removeAllCookies(); // Optional: clear session data
window.location.href = "/auth/login";
}
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
✅ Protected API Example
Here's an example of calling a protected API (e.g., creating a cart):
const createCart = async (customerId) => {
try {
const response = await api.post("/cart", { customerId });
return response.data;
} catch (error) {
console.error("Cart creation failed:", error);
throw error;
}
};
createCart("customer_123")
.then((data) => console.log("Cart created:", data))
.catch((error) => console.error("Error:", error));
📌 Important Notes
- The refresh token is stored in a HTTP-only cookie (set on login).
- Access tokens should be stored in memory or localStorage — whichever fits your use case best.
- This interceptor setup assumes the user is already logged in.
- If the refresh fails (e.g., session expired), the user is redirected to the login page.