Loading…
Loading…
Both tokens stored in sessionStorage. Similar to localStorage but clears when tab/browser closes. Provides tab isolation but still vulnerable to XSS.
While sessionStorage provides tab isolation, it is still accessible to JavaScript. If an XSS attack occurs in the same tab, tokens can be stolen. The only advantage over localStorage is that tokens are limited to the current tab.
// localStorage vs sessionStorage
// localStorage: Persists forever
localStorage.setItem('key', 'value');
// ❌ Still there after browser restart
// ❌ Still there after tab close
// ✅ Shared across all tabs
// sessionStorage: Clears when tab closes
sessionStorage.setItem('key', 'value');
// ✅ Gone after tab/browser close
// ❌ Still there on page refresh (same tab)
// ❌ Separate per tab (no cross-tab sync)// Tab Isolation: Each tab has its own sessionStorage
// Tab 1: User logs in → token stored in Tab 1's sessionStorage
// Tab 2: Opened later → no token (separate storage)
// Tab 1: Refreshes → token still there
// Tab 1: Closes → token gone!
// Tab 2: Still no token
// This is DIFFERENT from localStorage:
// localStorage: All tabs share the same storage
// sessionStorage: Each tab has isolated storageUser submits credentials. Server returns both tokens in response body.
Access and refresh tokens stored in sessionStorage (current tab only).
Access token added to Authorization header from sessionStorage.
Tokens automatically cleared when tab or browser closes.
const STORAGE_KEYS = {
ACCESS_TOKEN: 'auth_access_token',
REFRESH_TOKEN: 'auth_refresh_token',
};
export const sessionStorageStrategy = {
setTokens: (accessToken: string, refreshToken?: string) => {
sessionStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
if (refreshToken) {
sessionStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
}
},
getAccessToken: () => sessionStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN),
getRefreshToken: () => sessionStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN),
clearTokens: () => {
sessionStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
sessionStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
},
};// src/lib/api-client.ts
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { sessionStorageStrategy } from './token-storage';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001',
headers: { 'Content-Type': 'application/json' },
});
apiClient.interceptors.request.use(
(config) => {
const token = sessionStorageStrategy.getAccessToken();
if (token) config.headers.Authorization = 'Bearer ' + token;
return config;
},
(error) => Promise.reject(error)
);
let isRefreshing = false;
let failedQueue: Array<{ resolve: (token: string) => void; reject: (error: any) => void; }> = [];
const processQueue = (error: any, token: string | null) => {
failedQueue.forEach((prom) => { error ? prom.reject(error) : prom.resolve(token!); });
failedQueue = [];
};
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: string };
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); })
.then((token) => { originalRequest.headers.Authorization = 'Bearer ' + token; return apiClient(originalRequest); });
}
originalRequest._retry = 'true'; isRefreshing = true;
try {
const refreshToken = sessionStorageStrategy.getRefreshToken();
if (!refreshToken) throw new Error('No refresh token');
const res = await axios.post(process.env.NEXT_PUBLIC_API_URL + '/auth/refresh', { refreshToken });
const { accessToken, refreshToken: newRefreshToken } = res.data;
sessionStorageStrategy.setTokens(accessToken, newRefreshToken);
processQueue(null, accessToken);
originalRequest.headers.Authorization = 'Bearer ' + accessToken;
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
sessionStorageStrategy.clearTokens();
window.location.href = '/auth/login';
return Promise.reject(refreshError);
} finally { isRefreshing = false; }
}
return Promise.reject(error);
}
);