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:
{
"timestamp": "2024-03-15T10:23:45.123+00:00",
"status": 403,
"error": "Forbidden",
"message": "Invalid CSRF token found.",
"path": "/api/users"
}Server-side log:
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/usersOr with stateless API:
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-TOKENheader - 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-TOKENheader is not in the allowed headers list - Cookie not sent with credentials:
SameSite=StrictorwithCredentials: falseprevents 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
allowedHeadersconfiguration - 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-TOKENcookie and sendsX-XSRF-TOKENheader