diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index 92b7073e..6a88f332 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -3,6 +3,7 @@ import axios from "axios"; import { useNavigate, Link } from "react-router-dom"; import { ThemeContext } from "../../context/ThemeContext"; import type { ThemeContextType } from "../../context/ThemeContext"; +import { csrfHeaders } from "../../utils/csrf"; const backendUrl = import.meta.env.VITE_BACKEND_URL; @@ -30,7 +31,9 @@ const Login: React.FC = () => { setIsLoading(true); try { - const response = await axios.post(`${backendUrl}/api/auth/login`, formData); + const response = await axios.post(`${backendUrl}/api/auth/login`, formData, { + headers: csrfHeaders(), + }); setMessage(response.data.message); if (response.data.message === 'Login successful') { diff --git a/src/utils/csrf.ts b/src/utils/csrf.ts new file mode 100644 index 00000000..a0d8da26 --- /dev/null +++ b/src/utils/csrf.ts @@ -0,0 +1,40 @@ +/** + * Client-side CSRF token utilities. + * + * Generates a per-session random token stored in sessionStorage and provides + * a helper that attaches it as the X-CSRF-Token header on state-mutating + * requests. The backend must validate this header against the value it set + * in the session. Without such a header, a cross-site form submission carries + * no distinguishing marker and cannot be rejected. + */ + +const CSRF_TOKEN_KEY = 'csrf_token'; + +/** + * Return the existing session CSRF token, creating one if absent. + * The token is a 32-byte hex string generated via Web Crypto. + */ +export function getCsrfToken(): string { + const existing = sessionStorage.getItem(CSRF_TOKEN_KEY); + if (existing) return existing; + + const bytes = new Uint8Array(32); + window.crypto.getRandomValues(bytes); + const token = Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + + sessionStorage.setItem(CSRF_TOKEN_KEY, token); + return token; +} + +/** + * Return an axios-compatible headers object that includes the CSRF token. + * Spread this into the headers of any POST / PUT / PATCH / DELETE request. + * + * @example + * axios.post(url, body, { headers: csrfHeaders() }); + */ +export function csrfHeaders(): Record { + return { 'X-CSRF-Token': getCsrfToken() }; +}