springspring-bootauthenticationspring-securitycsrf

Managing CSRF and Authentication Tokens between Frontend like react and backend using Spring security with CSRF protection


In my Spring application, I need to use both a CSRF token and an authentication token for user login. However, I realized that the CSRF token is generated on the first GET request, while the authentication token is generated after a successful login, which is triggered by a POST request from the React frontend.

I'm confused about whether I need to manage two separate tokens: one for CSRF protection and one for authentication. If that's the case, how can I manage both tokens properly to ensure security?

Additionally, I have a concern: When a user first accesses the application, they land on the login page, but since the login process involves a POST request for authentication, it seems there won’t be a GET request to generate the CSRF token before that. How can I handle this scenario?

For CSRF setup I followed the spring csrf documentation for SPA https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa

Just for reference below is the SecurityConfig code with only CSRF token:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

//UserAuthSerivce is implementing the UserDetailsService
private UserAuthService userAuthService;

    public SecurityConfig(UserAuthService userAuthService) {
        this.userAuthService=userAuthService;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       return http.authorizeHttpRequests((request) -> request.
                requestMatchers("/login", "/register-me", "/logout", "/generate-token", "/csrf-token").permitAll()
                .anyRequest().authenticated())
                .cors(Customizer.withDefaults())
                .httpBasic(Customizer.withDefaults()).formLogin(Customizer.withDefaults()).csrf((csrf) -> csrf
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
//SpaCsrfTokenRequestHandler implementation I copied from the above mentioned documentation 
                        .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())            
        ).build();

    }

//Normal DaoAuthenticatorProvide, PasswordEncoder and AuthenticationManager 

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        
        CorsConfiguration cors=new CorsConfiguration();
        cors.setAllowedOrigins(Arrays.asList(
                "http://localhost:5173"
                ));
        cors.setAllowedHeaders(Arrays.asList(
                "Authorization","Content-Type", "X-XSRF-TOKEN", "XSRF-TOKEN"
                ));
        cors.setAllowedMethods(Arrays.asList(
                "GET","POST","PUT","DELETE"
                ));
        cors.setExposedHeaders(Arrays.asList("X-XSRF-TOKEN"));
        cors.setAllowCredentials(true);
        cors.setMaxAge(Duration.ofMinutes(2));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", cors);
        return source;
    }

}

//Under my Controller class, my csrf-token GET request to generate the CSRF token 
    @ResponseBody
    @GetMapping("/csrf-token")
    public ResponseEntity<Void> getCsrfToken(HttpServletRequest request) {
        
        return ResponseEntity.ok().build();
    }


Solution

  • You do need both tokens:

    1. The CSRF token ensures requests are coming from your trusted frontend.
    2. The authentication token ensures the user is authenticated.

    Ensure the frontend makes a GET request to fetch the CSRF token before the login request. In your React app, when the user lands on the login page, make a GET request to /csrf-token to fetch the CSRF token. Store this token (e.g., in memory or a state variable) and include it in the login POST request.Then, include the CSRF token in the login request. Your /csrf-token endpoint is fine. Spring Security's CookieCsrfTokenRepository automatically sets the CSRF token in a cookie (XSRF-TOKEN). For example:

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
    
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests((request) -> request
                .requestMatchers("/login", "/register-me", "/logout", "/generate-token", "/csrf-token").permitAll()
                .anyRequest().authenticated())
            .cors(Customizer.withDefaults())
            .csrf((csrf) -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
            )
            .build();
    }
    
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration cors = new CorsConfiguration();
        cors.setAllowedOrigins(Arrays.asList("http://localhost:5173"));
        cors.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-XSRF-TOKEN", "XSRF-TOKEN"));
        cors.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        cors.setExposedHeaders(Arrays.asList("X-XSRF-TOKEN"));
        cors.setAllowCredentials(true);
        cors.setMaxAge(Duration.ofMinutes(2));
    
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", cors);
        return source;
    }
    

    }

    and for React:

    useEffect(() => {
    fetch('/csrf-token', { credentials: 'include' })
        .then(response => {
            const csrfToken = document.cookie.replace(/(?: 
      (?:^|.*;\s*)XSRF-TOKEN\s*=\s*([^;]*).*$|^.*$/, '$1');
            setCsrfToken(csrfToken); // Store the token in state
        });
     }, []);
    
     const login = async (username, password) => {
     const response = await fetch('/login', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-XSRF-TOKEN': csrfToken, // Include the CSRF token
        },
        body: JSON.stringify({ username, password }),
        credentials: 'include',
    });
     // Handle response
    };