Introduction

Spring Security enables CSRF protection by default, requiring a valid CSRF token on every state-changing HTTP request (POST, PUT, DELETE, PATCH). When a web application makes AJAX requests without including the CSRF token, or the token does not match the one stored in the session, Spring returns HTTP 403 Forbidden with the message "Invalid CSRF Token". This error is common in single-page applications (SPAs), mobile apps, and API clients where the CSRF token must be explicitly extracted from a cookie or meta tag and sent as a request header.

Symptoms

HTTP 403 response:

json
{
  "timestamp": "2024-03-15T10:23:45.123+00:00",
  "status": 403,
  "error": "Forbidden",
  "message": "Invalid CSRF token found.",
  "path": "/api/users"
}

Server-side log:

bash
2024-03-15 10:23:45.123 DEBUG 12345 --- [nio-8080-exec-3] o.s.s.w.csrf.CsrfFilter: Invalid CSRF token found for http://localhost:8080/api/users

Or with stateless API:

bash
org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:127)

Common Causes

  • AJAX request does not send CSRF token: Frontend code sends POST/PUT without the X-CSRF-TOKEN header
  • Token stored in session but session expired: The CSRF token is lost when the HTTP session times out
  • Stateless API with CSRF enabled: REST APIs using JWT authentication do not need CSRF protection
  • CORS preflight blocks CSRF header: The X-CSRF-TOKEN header is not in the allowed headers list
  • Cookie not sent with credentials: SameSite=Strict or withCredentials: false prevents the CSRF cookie from being sent
  • Multiple tabs sharing session: Opening the app in multiple tabs causes token mismatch

Step-by-Step Fix

Step 1: Configure CSRF token in cookie for SPA access

```java @Configuration @EnableWebSecurity public class SecurityConfig {

@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) ); return http.build(); } } ```

withHttpOnlyFalse() allows JavaScript to read the cookie. Then in your SPA:

```javascript // Read the CSRF token from the cookie function getCsrfToken() { const cookies = document.cookie.split(';'); for (const cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'XSRF-TOKEN') { return decodeURIComponent(value); } } return null; }

// Include in every state-changing request async function apiPost(url, data) { const csrfToken = getCsrfToken(); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-XSRF-TOKEN': csrfToken, }, credentials: 'include', body: JSON.stringify(data), }); return response.json(); } ```

Step 2: Disable CSRF for stateless API endpoints

```java @Configuration @EnableWebSecurity public class SecurityConfig {

@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

return http.build(); } } ```

Step 3: Configure CORS to allow CSRF headers

```java @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("https://myapp.example.com")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH")); config.setAllowedHeaders(List.of( "Content-Type", "Authorization", "X-XSRF-TOKEN", // Must include CSRF header "X-CSRF-TOKEN" )); config.setAllowCredentials(true); config.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", config); return source; } ```

Prevention

  • Use CookieCsrfTokenRepository.withHttpOnlyFalse() for SPA applications
  • Disable CSRF for stateless API endpoints that use token-based authentication
  • Always include CSRF headers in the CORS allowedHeaders configuration
  • Add credentials: 'include' to fetch/AJAX calls when using cookie-based CSRF
  • Test CSRF protection by sending a POST request without the token -- it should return 403
  • Use Angular's built-in CSRF support: Angular automatically reads XSRF-TOKEN cookie and sends X-XSRF-TOKEN header