I have a backend in Spring boot 3.4.2 and a frontend Next JS 15.0.4. I have authentication on backend side with google oauth2 and it works. My cooki setup locally works with localhost:3000 and localhost:8080. I use csrf protection and cors config on backend side and do not want to turn off, so my question is, how to make it work on production.
My prod setup: I have a domain, lets call it: example. I created a subdomain like api.example for the backend with domain records and it works, so the namecheap doman setup in theory is good.
My Nextjs app is on vercel. My backend app is on render. The domain and subdomain have been configured on both platfroms.
THE PROBLEM:
when i click on frontend to the login button, that redirects to the backend. Backend manages to authenticate the user, and it's great. But after the successful auth, when the handler redirects to the next js app to a protected page, the request (next js server side request towards backend) fails for getting any protected data, so it redirects to login, and this whole flow leds to TOO_MANY_REDIRECTIONS
I am pretty sure, my cookie setup is messed somehow with the domain and subdomain on prod env.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.exceptionHandling(exceptionHandlingConfigurer -> {
exceptionHandlingConfigurer.authenticationEntryPoint(
new Oauth2AuthenticationEntrypoint());
})
.oauth2Login(customizer -> {
customizer
.successHandler(new Oauth2LoginSuccessHandler(userService, appConfig));
})
.logout(logout -> logout
.logoutUrl("/logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "XSRF-TOKEN")
.logoutSuccessHandler((request, response, authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
})
);
http.addFilterBefore(debuglogFilter, UsernamePasswordAuthenticationFilter.class);
http.cors().configurationSource(corsConfigurationSource());
http.csrf(csrf -> csrf
.requireCsrfProtectionMatcher(new AntPathRequestMatcher("/api/v1/books/process-images"))
.disable());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(appConfig.getAllowedOrigins()); // Your frontend URL
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // Allow all HTTP methods, including OPTIONS
configuration.setAllowedHeaders(Arrays.asList("X-XSRF-TOKEN", "Content-Type", "Authorization", "Cookie"));
configuration.setAllowCredentials(true); // Allow credentials (cookies)
// Create and return the CorsConfigurationSource
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // Apply to all endpoints
return source;
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(appConfig.getAllowedOrigins().get(0)) // Allow your frontend URL
.allowedMethods("GET", "POST", "PUT", "DELETE") // Allowed HTTP methods
.allowedHeaders("X-XSRF-TOKEN", "Content-Type", "Authorization", "Cookie")
.allowCredentials(true); // Allow credentials (cookies, etc.)
}
@Slf4j
@RequiredArgsConstructor
public class Oauth2LoginSuccessHandler implements AuthenticationSuccessHandler {
private final CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository(); // Use session-based CSRF storage
private final UserService userService;
private final AppConfig appConfig;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
log.debug("Authentication: {}", authentication);
// Convert OAuth2 user to your app user
DefaultOAuth2User oidcUser = (DefaultOAuth2User) authentication.getPrincipal();
AppUser appUser = AppUser.fromGoogleUser(oidcUser);
userService.findOrCreateUser(appUser);
AppAuthenticationToken token = new AppAuthenticationToken(appUser);
SecurityContextHolder.getContext().setAuthentication(token);
CsrfToken csrfToken = csrfTokenRepository.generateToken(request);
csrfTokenRepository.saveToken(csrfToken, request, response);
// Cookie csrfCookie = new Cookie("XSRF-TOKEN", csrfToken.getToken());
// csrfCookie.setPath("/");
// csrfCookie.setSecure(false); // Set to true in production with HTTPS
// csrfCookie.setHttpOnly(false); // Allow JS to read it
Cookie csrfCookie = new Cookie("XSRF-TOKEN", csrfToken.getToken());
csrfCookie.setPath("/");
csrfCookie.setSecure(true); // MUST be true in production with HTTPS
csrfCookie.setHttpOnly(false); // JavaScript should access it
// csrfCookie.setDomain("example"); // Ensure it's accessible from frontend
csrfCookie.setAttribute("SameSite", "None"); // Enable cross-site requests
// response.addCookie(csrfCookie);
response.addCookie(csrfCookie);
log.debug("Set CSRF token: {}", csrfToken.getToken());
//