Spring security CORS configuration not working after deploying to App Engine

144 views Asked by At

After configuring the CORS settings within my Spring Boot application to only permit requests coming from my website, I encountered an unexpected behavior when deploying the application on App Engine. Despite successful local testing where the configuration worked as intended, after deployment, the API remains accessible from any origin.

Below is my spring security configuration class :

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll())  // Allow all requests
                .httpBasic(Customizer.withDefaults())
                .cors(httpSecurityCorsConfigurer -> httpSecurityCorsConfigurer.configurationSource(corsConfigurationSource()))
                .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://mywebsite.com","https://mywebsite.com"));
        configuration.setAllowedHeaders(Arrays.asList("Access-Control-Allow-Origin", "Origin"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST","DELETE","PUT"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

And this is my controller:

@RestController
@RequestMapping("/cars")
public class CarController {

    private final CarService carService;

    public CarController(CarService carService) {
        this.carService = carService;
    }

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public Mono<Car> saveCar(@RequestBody Car car) {
        return carService.saveCar(car);
    }

    @DeleteMapping("/{carId}")
    public Mono<Void> deleteCar(@PathVariable String carId) {
        return carService.deleteCar(carId);
    }

    @GetMapping
    public Flux<Car> findAllCars() {
        return carService.findALlCars();
    }

    @GetMapping("/city/{city}")
    public Flux<Car> findCarsByCity(@PathVariable String city) {
        return carService.findCarsByCity(city);
    }
}

The app.yml file used by App Engine to deploy the API:

runtime: java17
instance_class: F1

I've found that I could configure CORS in my app.yml file, but I preferred to keep this concern for the spring security to handle it.

Additional logs:

  1. Request Headers: enter image description here

  2. Response Headers: enter image description here

1

There are 1 answers

3
VonC On BEST ANSWER

configuration.setAllowedHeaders(Arrays.asList("Access-Control-Allow-Origin", "Origin"));

You might need to adjust configuration.setAllowedHeaders(...). The header "Access-Control-Allow-Origin" is a response header, not a request header. It should not be in the list of allowed headers. Typically, you would allow headers like "Content-Type", "Authorization", "Cache-Control", etc.

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("http://mywebsite.com","https://mywebsite.com"));
    configuration.setAllowedHeaders(List.of("Content-Type", "Authorization", "Cache-Control"));
    configuration.setAllowedMethods(List.of("GET","POST","DELETE","PUT"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

Make sure that your SecurityConfiguration class is being picked up by Spring Boot. Sometimes, if the configuration is not in the correct package or is missing certain annotations, it will not be used. Your SecurityConfiguration class should be in a package that is either the same as or a sub-package of the main application class's package. The main application class is the one annotated with @SpringBootApplication.

Add logging to the SecurityConfiguration class to check whether it is being instantiated. You can add a log statement in a @PostConstruct method (the constructor might not be the best place to put a log message to test instantiation because Spring manages the creation of beans in a way that may not call the constructor as you might expect):

@Slf4j // If you are using Lombok, or define a Logger manually
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
    @PostConstruct
    public void postConstruct() {
        log.info("SecurityConfiguration has been instantiated and post-constructed");
    }

    @EventListener
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // This will log when the application context is refreshed 
        // (e.g., when the application is started or when a context is reloaded)
        log.info("SecurityConfiguration is fully loaded into the application context");
    }

    // rest of your configuration
}

If you have the Spring Boot Actuator dependency in your project, you can use the /beans endpoint to get a list of all the beans that have been instantiated in the Spring context, including your configuration:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    // other dependencies
}

Also, an app Engine might be overriding your CORS settings or providing its own defaults. It is possible that other middleware in your application is overriding the headers before they are sent back to the client. Check the logs in Google Cloud Console for your App Engine application to see if there are any messages that indicate manipulation of response headers or errors related to CORS.

As a test, you could temporarily disable your Spring Security configuration and deploy a simple controller that sets CORS headers manually in the response to see if App Engine is the one overriding them.

@GetMapping("/test-cors")
public ResponseEntity<String> testCors() {
    HttpHeaders headers = new HttpHeaders();
    headers.add("Access-Control-Allow-Origin", "http://mywebsite.com");
    return new ResponseEntity<>("CORS test", headers, HttpStatus.OK);
}

As noted by dur, the Origin header is missing from your request.

Use tools like curl to manually send requests with an Origin header to your App Engine deployment, and to see if the response indicates that CORS is being applied:

curl -H "Origin: http://mywebsite.com" -I https://your-app-engine-url.com/path