Skip to main content

Version: v1

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.