more secure token handling

This commit is contained in:
jackiettran
2025-09-17 18:37:07 -04:00
parent a9fa579b6d
commit cf6dd9be90
10 changed files with 807 additions and 231 deletions

View File

@@ -1,37 +1,160 @@
import axios from "axios";
import axios, { AxiosError, AxiosRequestConfig } from "axios";
const API_BASE_URL = process.env.REACT_APP_API_URL;
// CSRF token management
let csrfToken: string | null = null;
// Token refresh state
let isRefreshing = false;
let failedQueue: Array<{
resolve: (value?: any) => void;
reject: (reason?: any) => void;
config: AxiosRequestConfig;
}> = [];
const processQueue = (error: AxiosError | null, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(prom.config);
}
});
failedQueue = [];
};
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
"Content-Type": "application/json",
},
withCredentials: true, // Enable cookies
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
// Fetch CSRF token
export const fetchCSRFToken = async (): Promise<string> => {
try {
const response = await api.get("/auth/csrf-token");
csrfToken = response.data.csrfToken || "";
return csrfToken || "";
} catch (error) {
console.error("Failed to fetch CSRF token:", error);
return "";
}
};
// Reset CSRF token (call on logout)
export const resetCSRFToken = () => {
csrfToken = null;
};
// Check if authentication cookie exists
export const hasAuthCookie = (): boolean => {
return document.cookie
.split('; ')
.some(cookie => cookie.startsWith('accessToken='));
};
// Check if user has any auth indicators
export const hasAuthIndicators = (): boolean => {
return hasAuthCookie() || !!localStorage.getItem('token');
};
api.interceptors.request.use(async (config) => {
// Add CSRF token to headers for state-changing requests
const method = config.method?.toUpperCase() || "";
if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
// If we don't have a CSRF token yet, try to fetch it
if (!csrfToken) {
// Skip fetching for auth endpoints to avoid circular dependency
const isAuthEndpoint = config.url?.includes("/auth/") && !config.url?.includes("/auth/refresh");
if (!isAuthEndpoint) {
await fetchCSRFToken();
}
}
// Add the token if we have it
if (csrfToken) {
config.headers["X-CSRF-Token"] = csrfToken;
}
}
return config;
});
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Only redirect to login if we have a token (user was logged in)
const token = localStorage.getItem("token");
async (error: AxiosError) => {
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean, _csrfRetry?: boolean };
if (token) {
// User was logged in but token expired/invalid
localStorage.removeItem("token");
// Handle CSRF token errors
if (error.response?.status === 403) {
const errorData = error.response?.data as any;
if (errorData?.code === "CSRF_TOKEN_MISMATCH" && !originalRequest._csrfRetry) {
originalRequest._csrfRetry = true;
// Try to fetch a new CSRF token and retry
try {
await fetchCSRFToken();
// Retry the original request with new token
return api(originalRequest);
} catch (csrfError) {
console.error("Failed to refresh CSRF token:", csrfError);
}
}
}
// Handle token expiration and authentication errors
if (error.response?.status === 401) {
const errorData = error.response?.data as any;
// Don't redirect for NO_TOKEN on public endpoints
if (errorData?.code === "NO_TOKEN") {
// Let the app handle this - user simply isn't logged in
return Promise.reject(error);
}
// If token is expired, try to refresh
if (errorData?.code === "TOKEN_EXPIRED" && !originalRequest._retry) {
if (isRefreshing) {
// If already refreshing, queue the request
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject, config: originalRequest });
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
// Try to refresh the token
await api.post("/auth/refresh");
isRefreshing = false;
processQueue(null);
// Also refresh CSRF token after auth refresh
await fetchCSRFToken();
// Retry the original request
return api(originalRequest);
} catch (refreshError) {
isRefreshing = false;
processQueue(refreshError as AxiosError);
// Refresh failed, redirect to login
window.location.href = "/login";
return Promise.reject(refreshError);
}
}
// For other 401 errors, check if we should redirect
// Only redirect if this is not a login/register request
const isAuthEndpoint = originalRequest.url?.includes("/auth/");
if (!isAuthEndpoint && errorData?.error !== "Access token required") {
window.location.href = "/login";
}
// For non-authenticated users, just reject the error without redirecting
// Let individual components handle 401 errors as needed
}
return Promise.reject(error);
}
);
@@ -40,6 +163,9 @@ export const authAPI = {
register: (data: any) => api.post("/auth/register", data),
login: (data: any) => api.post("/auth/login", data),
googleLogin: (data: any) => api.post("/auth/google", data),
logout: () => api.post("/auth/logout"),
refresh: () => api.post("/auth/refresh"),
getCSRFToken: () => api.get("/auth/csrf-token"),
};
export const userAPI = {