DEV Community

Aswani Nayak
Aswani Nayak

Posted on

Enabling CSRF in a JWT-Based React + Spring Boot Application (Enterprise-Ready Approach)

Modern applications often use:
• React SPA (Single Page Application)
• Spring Boot backend
• JWT-based authentication
• Stateless REST APIs

In this architecture, it’s common to disable CSRF:
Code
.csrf(AbstractHttpConfigurer::disable)
However, many enterprise security teams and tools (e.g., GitHub CodeQL) flag this as a high-severity issue.
This blog explains:
• ✅ Why CSRF is flagged
• ✅ Whether JWT APIs need CSRF
• ✅ How to enable CSRF without breaking your SPA
• ✅ A production-ready implementation

1️⃣ Understanding the Problem
What is CSRF?
CSRF (Cross-Site Request Forgery) is an attack where a malicious website tricks a browser into making an authenticated request to another site.
CSRF attacks rely on:
• Automatic cookie sending by the browser
• Session-based authentication
Spring Security enables CSRF by default for this reason.

Why JWT APIs Typically Disable CSRF
In a JWT-based system:
• Authentication is done via:
• Authorization: Bearer
• Browsers do NOT automatically attach Authorization headers.
• No session cookies are involved.
• The API is stateless.
Therefore, CSRF protection is not strictly required.
However...

Why Security Teams Still Want It Enabled
Enterprise AppSec teams often require:
• Defense-in-depth
• Uniform policy enforcement
• Future-proof protection (in case cookies are introduced later)
• Clean security scans without exceptions
Instead of disabling CSRF, we can implement it correctly for SPA.

2️⃣ Enterprise-Ready Solution
We will:
• ✅ Keep JWT authentication
• ✅ Keep backend stateless
• ✅ Enable CSRF
• ✅ Make it work with React SPA
• ✅ Pass security review
The key is:
Code
CookieCsrfTokenRepository

3️⃣ Spring Boot Implementation
Step 1: Security Configuration
Code

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
            // ✅ Enable CSRF with Cookie repository (SPA-friendly)
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )

            // ✅ Keep application stateless
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
            )

            // ✅ Add JWT filter
            .addFilterBefore(jwtAuthenticationFilter,
                    UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}
Enter fullscreen mode Exit fullscreen mode

What This Does
Spring Security will:

  1. Generate a CSRF token
  2. Send it in a cookie named: XSRF-TOKEN
  3. Expect the frontend to send it back in header: X-XSRF-TOKEN This pattern is SPA-friendly and secure.

4️⃣ React Implementation
Step 1: Enable Credentials
When using cookies, you must enable credentials:
Code

const api = axios.create({
  baseURL: "http://localhost:8080",
  withCredentials: true
});
Enter fullscreen mode Exit fullscreen mode

Step 2: Read CSRF Cookie
Helper function:
Code

function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Axios Interceptor
Code

api.interceptors.request.use((config) => {

  const jwt = localStorage.getItem("jwt");
  const csrfToken = getCookie("XSRF-TOKEN");

  if (jwt) {
    config.headers.Authorization = `Bearer ${jwt}`;
  }

  if (csrfToken) {
    config.headers["X-XSRF-TOKEN"] = csrfToken;
  }

  return config;
});
Enter fullscreen mode Exit fullscreen mode

Now every request sends:
• ✅ JWT token
• ✅ CSRF token

5️⃣ CORS Configuration (Critical)
When using cookies, you must allow credentials.
Code

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("http://localhost:3000"));
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    configuration.setAllowedHeaders(
        List.of("Authorization", "Content-Type", "X-XSRF-TOKEN")
    );
    configuration.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}
Enter fullscreen mode Exit fullscreen mode

And enable CORS:
Code
http.cors(Customizer.withDefaults());

6️⃣ Request Flow Explained
Initial Request
• Backend generates CSRF token
• Sends XSRF-TOKEN cookie
API Call
React sends:
Authorization: Bearer
X-XSRF-TOKEN:
Spring Validates
• JWT signature
• CSRF token match
If both valid → request succeeds.

7️⃣ Architecture Summary

8️⃣ Why This Is a Strong Design
Even though JWT Authorization header authentication does not require CSRF, enabling it provides:
• Defense-in-depth
• Protection against future cookie-based changes
• Compliance with security standards
• Cleaner audit results
This approach balances:
✅ Security
✅ Architecture purity
✅ Enterprise compliance

9️⃣ Final Thoughts
If you are building:
• React SPA
• Spring Boot backend
• JWT authentication
And your security team requires CSRF to be enabled:
✅ Use CookieCsrfTokenRepository
✅ Send CSRF token via header
✅ Keep the application stateless
This gives you:
• Modern architecture
• Strong security posture
• Enterprise-ready implementation

Top comments (0)