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

@@ -9,11 +9,16 @@
name="description"
content="CommunityRentals.App - Rent gym equipment, tools, and musical instruments from your neighbors"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>CommunityRentals.App - Equipment & Tool Rental Marketplace</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"
/>
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body>

View File

@@ -233,15 +233,6 @@ const AuthModal: React.FC<AuthModalProps> = ({
Sign in with Google
</button>
<button
className="btn btn-outline-dark w-100 mb-3 py-3 d-flex align-items-center justify-content-center"
onClick={() => handleSocialLogin("apple")}
disabled={loading}
>
<i className="bi bi-apple me-2"></i>
Sign in with Apple
</button>
<div className="text-center mt-3">
<small className="text-muted">
{mode === "login"

View File

@@ -6,7 +6,7 @@ import React, {
ReactNode,
} from "react";
import { User } from "../types";
import { authAPI, userAPI } from "../services/api";
import { authAPI, userAPI, fetchCSRFToken, resetCSRFToken, hasAuthIndicators } from "../services/api";
interface AuthContextType {
user: User | null;
@@ -14,8 +14,9 @@ interface AuthContextType {
login: (email: string, password: string) => Promise<void>;
register: (data: any) => Promise<void>;
googleLogin: (idToken: string) => Promise<void>;
logout: () => void;
logout: () => Promise<void>;
updateUser: (user: User) => void;
checkAuth: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -36,46 +37,86 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem("token");
if (token) {
userAPI
.getProfile()
.then((response) => {
setUser(response.data);
})
.catch((error) => {
localStorage.removeItem("token");
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
const checkAuth = async () => {
try {
const response = await userAPI.getProfile();
setUser(response.data);
} catch (error: any) {
// Only log actual errors, not "user not logged in"
if (error.response?.data?.code !== "NO_TOKEN") {
console.error("Auth check failed:", error);
}
setUser(null);
}
};
useEffect(() => {
// Initialize authentication
const initializeAuth = async () => {
try {
// Check if we have any auth indicators before making API call
if (hasAuthIndicators()) {
// Only check auth if we have some indication of being logged in
// This avoids unnecessary 401 errors in the console
await checkAuth();
} else {
// No auth indicators - skip the API call
setUser(null);
}
// Always fetch CSRF token for subsequent requests
await fetchCSRFToken();
} catch (error) {
console.error("Failed to initialize auth:", error);
// Even on error, try to get CSRF token for non-authenticated requests
try {
await fetchCSRFToken();
} catch (csrfError) {
console.error("Failed to fetch CSRF token:", csrfError);
}
} finally {
setLoading(false);
}
};
initializeAuth();
}, []);
const login = async (email: string, password: string) => {
const response = await authAPI.login({ email, password });
localStorage.setItem("token", response.data.token);
setUser(response.data.user);
// Fetch new CSRF token after login
await fetchCSRFToken();
};
const register = async (data: any) => {
const response = await authAPI.register(data);
localStorage.setItem("token", response.data.token);
setUser(response.data.user);
// Fetch new CSRF token after registration
await fetchCSRFToken();
};
const googleLogin = async (idToken: string) => {
const response = await authAPI.googleLogin({ idToken });
localStorage.setItem("token", response.data.token);
setUser(response.data.user);
// Fetch new CSRF token after Google login
await fetchCSRFToken();
};
const logout = () => {
localStorage.removeItem("token");
setUser(null);
const logout = async () => {
try {
await authAPI.logout();
setUser(null);
// Reset CSRF token on logout
resetCSRFToken();
// Redirect to home page after logout
window.location.href = "/";
} catch (error) {
console.error("Logout failed:", error);
// Even if logout fails, clear local state and CSRF token
setUser(null);
resetCSRFToken();
}
};
const updateUser = (user: User) => {
@@ -84,7 +125,16 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
return (
<AuthContext.Provider
value={{ user, loading, login, register, googleLogin, logout, updateUser }}
value={{
user,
loading,
login,
register,
googleLogin,
logout,
updateUser,
checkAuth,
}}
>
{children}
</AuthContext.Provider>

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 = {