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();
}
You do need both tokens:
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
};