diff --git a/src/main/java/org/fairdatateam/fairdatapoint/api/filter/CORSFilter.java b/src/main/java/org/fairdatateam/fairdatapoint/api/filter/CORSFilter.java deleted file mode 100644 index 53a0ebe99..000000000 --- a/src/main/java/org/fairdatateam/fairdatapoint/api/filter/CORSFilter.java +++ /dev/null @@ -1,68 +0,0 @@ -/** - * The MIT License - * Copyright © 2017 FAIR Data Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package org.fairdatateam.fairdatapoint.api.filter; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.http.HttpHeaders; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -import static java.lang.String.format; - -@Component -public class CORSFilter extends OncePerRequestFilter { - - @Override - public void doFilterInternal( - final HttpServletRequest request, - final HttpServletResponse response, - final FilterChain fc - ) throws IOException, ServletException { - final String allowedMtds = String.join(",", - RequestMethod.GET.name(), RequestMethod.POST.name(), - RequestMethod.PUT.name(), RequestMethod.PATCH.name(), - RequestMethod.DELETE.name()); - - response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, - format("%s,%s,%s,%s", HttpHeaders.ORIGIN, HttpHeaders.AUTHORIZATION, - HttpHeaders.ACCEPT, HttpHeaders.CONTENT_TYPE)); - response.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, - format("%s,%s", HttpHeaders.LOCATION, HttpHeaders.LINK)); - response.setHeader(HttpHeaders.ALLOW, allowedMtds); - response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, allowedMtds); - - fc.doFilter(request, response); - } -} diff --git a/src/main/java/org/fairdatateam/fairdatapoint/api/filter/FilterConfigurer.java b/src/main/java/org/fairdatateam/fairdatapoint/api/filter/FilterConfigurer.java index b80a88fa8..8c32e9eb1 100644 --- a/src/main/java/org/fairdatateam/fairdatapoint/api/filter/FilterConfigurer.java +++ b/src/main/java/org/fairdatateam/fairdatapoint/api/filter/FilterConfigurer.java @@ -36,17 +36,13 @@ public class FilterConfigurer extends @Autowired private JwtTokenFilter jwtTokenFilter; - @Autowired - private CORSFilter corsFilter; - @Autowired private LoggingFilter loggingFilter; @Override public void configure(HttpSecurity http) { http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); - http.addFilterBefore(corsFilter, JwtTokenFilter.class); - http.addFilterBefore(loggingFilter, CORSFilter.class); + http.addFilterBefore(loggingFilter, JwtTokenFilter.class); } } diff --git a/src/main/java/org/fairdatateam/fairdatapoint/config/SecurityConfig.java b/src/main/java/org/fairdatateam/fairdatapoint/config/SecurityConfig.java index e7fabe00d..bf923fd75 100644 --- a/src/main/java/org/fairdatateam/fairdatapoint/config/SecurityConfig.java +++ b/src/main/java/org/fairdatateam/fairdatapoint/config/SecurityConfig.java @@ -28,6 +28,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -35,6 +36,7 @@ import org.springframework.security.web.SecurityFilterChain; @Configuration +// Spring Security docs use @EnableWebSecurity, but we don't need it here because of Spring Boot autoconfiguration. public class SecurityConfig { @Bean @@ -67,6 +69,11 @@ public SecurityFilterChain filterChain(HttpSecurity http, FilterConfigurer filte .anyRequest().permitAll(); } ) + // Enable CORS for the security filter chain. + // "If Spring MVC is on classpath and no CorsConfigurationSource is provided, + // Spring Security will use CORS configuration provided to Spring MVC." + // https://docs.spring.io/spring-security/reference/servlet/integrations/cors.html#cors-spring-mvc-integration + .cors(Customizer.withDefaults()) .apply(filterConfigurer); return http.build(); } diff --git a/src/main/java/org/fairdatateam/fairdatapoint/config/WebMvcConfig.java b/src/main/java/org/fairdatateam/fairdatapoint/config/WebMvcConfig.java index 540c31b7f..7319dda16 100644 --- a/src/main/java/org/fairdatateam/fairdatapoint/config/WebMvcConfig.java +++ b/src/main/java/org/fairdatateam/fairdatapoint/config/WebMvcConfig.java @@ -30,11 +30,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.http.HttpHeaders; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.view.InternalResourceViewResolver; @@ -85,4 +87,24 @@ public ObjectMapper objectMapper() { public InternalResourceViewResolver defaultViewResolver() { return new InternalResourceViewResolver(); } + + /** + * Global Cross-Origin Resource Sharing (CORS) configuration based on + * + * Web MVC CORS example + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE") + .allowedHeaders( + HttpHeaders.ORIGIN, + HttpHeaders.AUTHORIZATION, + HttpHeaders.ACCEPT, + HttpHeaders.CONTENT_TYPE + ) + // If the response contains these headers, the browser will expose them in JavaScript + .exposedHeaders(HttpHeaders.LOCATION, HttpHeaders.LINK); + } } diff --git a/src/test/java/org/fairdatateam/fairdatapoint/acceptance/general/CorsTest.java b/src/test/java/org/fairdatateam/fairdatapoint/acceptance/general/CorsTest.java new file mode 100644 index 000000000..ca4ba5905 --- /dev/null +++ b/src/test/java/org/fairdatateam/fairdatapoint/acceptance/general/CorsTest.java @@ -0,0 +1,108 @@ +/** + * The MIT License + * Copyright © 2017 FAIR Data Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.fairdatateam.fairdatapoint.acceptance.general; + +import org.eclipse.jetty.http.HttpHeader; +import org.fairdatateam.fairdatapoint.Profiles; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.assertj.MockMvcTester; +import org.springframework.test.web.servlet.assertj.MvcTestResult; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies Cross-Origin Resource Sharing (CORS) configuration. For more information see + * spring security CORS + * and spring web mvc CORS. + */ +@ActiveProfiles(Profiles.TESTING) +@AutoConfigureMockMvc +@SpringBootTest +public class CorsTest { + + private final String otherOrigin = "https://other.origin"; + private final String uri = "/"; + + private final MockMvcTester mockMvc; + + // todo: do we need to enable the security filter chain as well, to test http.cors()? + // https://docs.spring.io/spring-security/reference/servlet/test/mockmvc/setup.html + + /** + * Constructor + */ + @Autowired + public CorsTest(MockMvcTester mockMvc) { + this.mockMvc = mockMvc; + } + + @Test + public void normalRequestYieldsNoAccessControlHeaders() { + // request without Origin header + MvcTestResult testResult = mockMvc.options().uri(uri).exchange(); + assertThat(testResult).hasStatusOk() + .doesNotContainHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN); + } + + @Test + public void corsPreflightRequestFailsIfAccessControlRequestMethodHeaderMissing() { + // request with Origin header, but without Access-Control-Request-Method header + MvcTestResult testResult = mockMvc.options() + .uri(uri) + .header(HttpHeaders.ORIGIN, otherOrigin) + .exchange(); + assertThat(testResult).hasStatus(HttpStatus.FORBIDDEN) + .hasBodyTextEqualTo("Invalid CORS request"); + } + + @Test + public void corsPreflightRequestYieldsExpectedAccessControlHeaders() { + // valid CORS request, with both required headers + MvcTestResult testResult = mockMvc.options() + .uri(uri) + // this preflight says: "I plan to make a GET request with this header from this origin." + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.GET) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, HttpHeaders.AUTHORIZATION) + .header(HttpHeaders.ORIGIN, otherOrigin) + .exchange(); + List.of( + // note that Access-Control-Allow-Headers is only returned if the request + // contains Access-Control-Request-Headers + HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, + HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, + HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, + HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS + ).forEach(header -> assertThat(testResult).hasStatusOk().containsHeader(header)); + + } + +}