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.
The following code is stored in the constant.ts file:
// constant.ts
import axios from "axios";
export const BASE_URL = `https://dev-sfapi.unisouk.com`;
export const STORE_ID = `<STORE_ID>`; // the STORE_ID will be provided by your admin in Unisouk .
// 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/${STORE_ID}`, {
withCredentials: true,
headers: {
"Content-Type": "application/json",
"x-store-id": STORE_ID, // Fetch your current store ID
},
})
.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
// constant.ts
export const api = axios.create({
baseURL: BASE_URL,
headers: {
"Content-Type": "application/json",
"x-store-id": STORE_ID, // Fetch your current store ID
},
withCredentials: true,
});
Request Interceptor
Adds the access token to the Authorization header before every request.
// constant.ts
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.
// constant.ts
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) {
window.location.href = "/auth/login"; // or whichever your login path would be
}
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):
// create-cart.ts
import { api } from "../constant";
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.