From df822df0d10951a0de66ad7d3bed5aa9c3701a94 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Nov 2025 04:46:58 +0000 Subject: [PATCH] fix(ui): add CSRF token handling to API client The frontend API client was not sending the X-CSRF-Token header on POST/PUT/DELETE/PATCH requests, causing CSRF validation to fail with 403 "CSRF tokens do not match" errors. Changes: - Add csrfToken variable to store token from responses - Capture CSRF token from X-CSRF-Token response header - Send CSRF token in X-CSRF-Token header for state-changing requests --- ui/src/lib/api.ts | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index c0cdeded..16eb9885 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -4,6 +4,9 @@ import { toast } from './toast'; // API Base URL - uses Vite proxy in development, direct URL in production const API_BASE_URL = import.meta.env.VITE_API_URL || '/api/v1'; +// CSRF token storage - captured from response headers and sent on state-changing requests +let csrfToken: string | null = null; + // API Error Response Type export interface APIErrorResponse { error: string; @@ -867,7 +870,7 @@ class APIClient { withCredentials: true, // Enable cookies for SAML session }); - // Request interceptor for adding auth tokens + // Request interceptor for adding auth tokens and CSRF tokens this.client.interceptors.request.use( (config) => { // BUG FIX: Use Zustand persisted store as single source of truth for token @@ -884,14 +887,30 @@ class APIClient { console.error('Failed to parse auth state:', e); } } + + // Add CSRF token for state-changing requests (POST, PUT, DELETE, PATCH) + // The server validates this token against the csrf_token cookie + const method = config.method?.toUpperCase(); + if (csrfToken && (method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH')) { + config.headers['X-CSRF-Token'] = csrfToken; + } + return config; }, (error) => Promise.reject(error) ); - // Response interceptor for error handling + // Response interceptor for CSRF token capture and error handling this.client.interceptors.response.use( - (response) => response, + (response) => { + // Capture CSRF token from response headers + // Server sends this on GET/HEAD/OPTIONS requests + const newCsrfToken = response.headers['x-csrf-token']; + if (newCsrfToken) { + csrfToken = newCsrfToken; + } + return response; + }, (error: AxiosError) => { // Handle network errors if (!error.response) {