From 4199240662bfb993049cedb07647c4313ff37096 Mon Sep 17 00:00:00 2001 From: Robert Winch <362503+rwinch@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:56:46 -0500 Subject: [PATCH] Add Support for PreFlightRequestFilter Closes gh-18926 --- .../web/configurers/CorsConfigurer.java | 100 ++++++-- .../security/config/annotation/web/CorsDsl.kt | 7 +- .../web/configurers/CorsConfigurerTests.java | 214 ++++++++++++++++++ .../config/annotation/web/CorsDslTests.kt | 121 +++++++++- .../ROOT/pages/servlet/integrations/cors.adoc | 17 ++ docs/modules/ROOT/pages/whats-new.adoc | 1 + .../CorsPreFlightRequestHandlerExample.java | 49 ++++ .../CorsPreFlightRequestHandlerExample.kt | 53 +++++ 8 files changed, 538 insertions(+), 24 deletions(-) create mode 100644 docs/src/test/java/org/springframework/security/docs/servlet/integrations/corspreflightrequesthandler/CorsPreFlightRequestHandlerExample.java create mode 100644 docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/integrations/corspreflightrequesthandler/CorsPreFlightRequestHandlerExample.kt diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurer.java index 33e7171ee2..34c2b8a95c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurer.java @@ -21,15 +21,18 @@ import org.springframework.context.ApplicationContext; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.util.Assert; -import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.PreFlightRequestHandler; import org.springframework.web.filter.CorsFilter; +import org.springframework.web.filter.PreFlightRequestFilter; /** - * Adds {@link CorsFilter} to the Spring Security filter chain. If a bean by the name of - * corsFilter is provided, that {@link CorsFilter} is used. Else if - * corsConfigurationSource is defined, then that {@link CorsConfiguration} is used. + * Adds {@link CorsFilter} or {@link PreFlightRequestFilter} to the Spring Security filter + * chain. If a bean by the name of corsFilter is provided, that {@link CorsFilter} is + * used. Else if corsConfigurationSource is defined, then that + * {@link CorsConfigurationSource} is used. If a {@link PreFlightRequestHandler} is set on + * this configurer, {@link CorsFilter} is not used and {@link PreFlightRequestFilter} is + * registered instead. * * @param the builder to return. * @author Rob Winch @@ -43,6 +46,8 @@ public class CorsConfigurer> extends AbstractHt private CorsConfigurationSource configurationSource; + private PreFlightRequestHandler preFlightRequestHandler; + /** * Creates a new instance * @@ -56,30 +61,85 @@ public class CorsConfigurer> extends AbstractHt return this; } + /** + * Use the given {@link PreFlightRequestHandler} for CORS preflight requests. When + * set, {@link CorsFilter} is not used. Cannot be combined with + * {@link #configurationSource(CorsConfigurationSource)}. + * @param preFlightRequestHandler the handler to use + * @return the {@link CorsConfigurer} for additional configuration + */ + public CorsConfigurer preFlightRequestHandler(PreFlightRequestHandler preFlightRequestHandler) { + this.preFlightRequestHandler = preFlightRequestHandler; + return this; + } + @Override public void configure(H http) { ApplicationContext context = http.getSharedObject(ApplicationContext.class); + + if (this.configurationSource != null && this.preFlightRequestHandler != null) { + throw new IllegalStateException( + "Cannot configure both a CorsConfigurationSource and a PreFlightRequestHandler on CorsConfigurer"); + } + CorsFilter corsFilter = getCorsFilter(context); - Assert.state(corsFilter != null, () -> "Please configure either a " + CORS_FILTER_BEAN_NAME + " bean or a " - + CORS_CONFIGURATION_SOURCE_BEAN_NAME + "bean."); - http.addFilter(corsFilter); + if (corsFilter != null) { + http.addFilter(corsFilter); + return; + } + PreFlightRequestHandler preFlightRequestHandlerBean = getPreFlightRequestHandler(context); + if (preFlightRequestHandlerBean != null) { + http.addFilterBefore(new PreFlightRequestFilter(preFlightRequestHandlerBean), CorsFilter.class); + return; + } + throw new NoSuchBeanDefinitionException(CorsConfigurationSource.class, + "Failed to find a bean that implements `CorsConfigurationSource`. Please ensure that you are using " + + "`@EnableWebMvc`, are publishing a `WebMvcConfigurer`, or are publishing a `CorsConfigurationSource` bean."); + } + + private PreFlightRequestHandler getPreFlightRequestHandler(ApplicationContext context) { + if (this.configurationSource != null) { + return null; + } + if (this.preFlightRequestHandler != null) { + return this.preFlightRequestHandler; + } + if (context == null) { + return null; + } + if (context.getBeanNamesForType(PreFlightRequestHandler.class).length > 0) { + return context.getBean(PreFlightRequestHandler.class); + } + return null; + } + + private CorsConfigurationSource getCorsConfigurationSource(ApplicationContext context) { + if (context == null) { + return null; + } + boolean containsCorsSource = context.containsBeanDefinition(CORS_CONFIGURATION_SOURCE_BEAN_NAME); + if (containsCorsSource) { + return context.getBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME, CorsConfigurationSource.class); + } + return MvcCorsFilter.getMvcCorsConfigurationSource(context); } private CorsFilter getCorsFilter(ApplicationContext context) { + if (this.preFlightRequestHandler != null) { + return null; + } if (this.configurationSource != null) { return new CorsFilter(this.configurationSource); } - boolean containsCorsFilter = context.containsBeanDefinition(CORS_FILTER_BEAN_NAME); + boolean containsCorsFilter = context != null && context.containsBeanDefinition(CORS_FILTER_BEAN_NAME); if (containsCorsFilter) { return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class); } - boolean containsCorsSource = context.containsBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME); - if (containsCorsSource) { - CorsConfigurationSource configurationSource = context.getBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME, - CorsConfigurationSource.class); - return new CorsFilter(configurationSource); + CorsConfigurationSource corsConfigurationSource = getCorsConfigurationSource(context); + if (corsConfigurationSource != null) { + return new CorsFilter(corsConfigurationSource); } - return MvcCorsFilter.getMvcCorsFilter(context); + return null; } static class MvcCorsFilter { @@ -92,15 +152,11 @@ public class CorsConfigurer> extends AbstractHt * @param context * @return */ - private static CorsFilter getMvcCorsFilter(ApplicationContext context) { + private static CorsConfigurationSource getMvcCorsConfigurationSource(ApplicationContext context) { if (context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { - CorsConfigurationSource corsConfigurationSource = context - .getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, CorsConfigurationSource.class); - return new CorsFilter(corsConfigurationSource); + return context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, CorsConfigurationSource.class); } - throw new NoSuchBeanDefinitionException(CorsConfigurationSource.class, - "Failed to find a bean that implements `CorsConfigurationSource`. Please ensure that you are using " - + "`@EnableWebMvc`, are publishing a `WebMvcConfigurer`, or are publishing a `CorsConfigurationSource` bean."); + return null; } } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/CorsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/CorsDsl.kt index d4cd010d12..d813056864 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/CorsDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/CorsDsl.kt @@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.CorsConfigurer import org.springframework.web.cors.CorsConfigurationSource +import org.springframework.web.cors.PreFlightRequestHandler /** * A Kotlin DSL to configure [HttpSecurity] CORS using idiomatic Kotlin code. @@ -26,11 +27,14 @@ import org.springframework.web.cors.CorsConfigurationSource * @author Eleftheria Stein * @since 5.3 * @property configurationSource the [CorsConfigurationSource] to use. + * @property preFlightRequestHandler the [PreFlightRequestHandler] to use instead of [CorsFilter]. */ @SecurityMarker class CorsDsl { var configurationSource: CorsConfigurationSource? = null + var preFlightRequestHandler: PreFlightRequestHandler? = null + private var disabled = false /** @@ -42,7 +46,8 @@ class CorsDsl { internal fun get(): (CorsConfigurer) -> Unit { return { cors -> - configurationSource?.also { cors.configurationSource(configurationSource) } + configurationSource?.also { cors.configurationSource(it) } + preFlightRequestHandler?.also { cors.preFlightRequestHandler(it) } if (disabled) { cors.disable() } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java index adc698f287..c05100193c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java @@ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.PreFlightRequestHandler; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -196,6 +197,73 @@ public class CorsConfigurerTests { .andExpect(header().exists("X-Content-Type-Options")); } + @Test + public void optionsWhenPreFlightRequestHandlerBeanThenHandled() throws Exception { + this.spring.register(PreFlightRequestHandlerConfig.class).autowire(); + this.mvc + .perform(options("/") + .header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()) + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(status().isOk()) + .andExpect(header().exists("X-Pre-Flight")); + } + + @Test + public void optionsWhenNoPreFlightRequestHandlerBeanThenCorsFilterUsed() throws Exception { + this.spring.register(NoPreFlightRequestHandlerConfig.class).autowire(); + this.mvc + .perform(options("/") + .header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()) + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(status().isOk()) + .andExpect(header().exists("Access-Control-Allow-Origin")) + .andExpect(header().doesNotExist("X-Pre-Flight")); + } + + @Test + public void optionsWhenExplicitConfigurationSourceThenPreFlightRequestHandlerBeanIgnored() throws Exception { + this.spring.register(ExplicitConfigurationSourceWithPreFlightRequestHandlerBeanConfig.class).autowire(); + this.mvc + .perform(options("/") + .header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()) + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(status().isOk()) + .andExpect(header().exists("Access-Control-Allow-Origin")) + .andExpect(header().doesNotExist("X-Pre-Flight")); + } + + @Test + public void optionsWhenPreFlightRequestHandlerMemberThenHandled() throws Exception { + this.spring.register(PreFlightRequestHandlerMemberConfig.class).autowire(); + this.mvc + .perform(options("/") + .header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()) + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(status().isOk()) + .andExpect(header().exists("X-Pre-Flight")); + } + + @Test + public void configureWhenBothConfigurationSourceAndPreFlightRequestHandlerMemberThenIllegalState() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.spring.register(BothCorsConfigurerMembersConfig.class).autowire()) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessageContaining("Cannot configure both"); + } + + @Test + public void optionsWhenPreFlightRequestHandlerMemberThenCorsFilterBeanIgnored() throws Exception { + this.spring.register(PreFlightRequestHandlerMemberWithCorsFilterBeanConfig.class).autowire(); + this.mvc + .perform(options("/") + .header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()) + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(status().isOk()) + .andExpect(header().exists("X-Pre-Flight")) + .andExpect(header().doesNotExist("Access-Control-Allow-Origin")); + } + @Configuration @EnableWebSecurity static class DefaultCorsConfig { @@ -382,4 +450,150 @@ public class CorsConfigurerTests { } + @Configuration + @EnableWebSecurity + static class PreFlightRequestHandlerConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated()) + .cors(withDefaults()); + return http.build(); + // @formatter:on + } + + @Bean + PreFlightRequestHandler preFlightRequestHandler() { + return (request, response) -> response.addHeader("X-Pre-Flight", "Handled"); + } + + } + + @Configuration + @EnableWebSecurity + static class NoPreFlightRequestHandlerConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated()) + .cors(withDefaults()); + return http.build(); + // @formatter:on + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedOrigins(Collections.singletonList("*")); + corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name())); + source.registerCorsConfiguration("/**", corsConfiguration); + return source; + } + + } + + @Configuration + @EnableWebSecurity + static class ExplicitConfigurationSourceWithPreFlightRequestHandlerBeanConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedOrigins(Collections.singletonList("*")); + corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name())); + source.registerCorsConfiguration("/**", corsConfiguration); + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated()) + .cors((cors) -> cors.configurationSource(source)); + return http.build(); + // @formatter:on + } + + @Bean + PreFlightRequestHandler preFlightRequestHandler() { + return (request, response) -> response.addHeader("X-Pre-Flight", "Handled"); + } + + } + + @Configuration + @EnableWebSecurity + static class PreFlightRequestHandlerMemberConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated()) + .cors((cors) -> cors.preFlightRequestHandler( + (request, response) -> response.addHeader("X-Pre-Flight", "Member"))); + return http.build(); + // @formatter:on + } + + } + + @Configuration + @EnableWebSecurity + static class BothCorsConfigurerMembersConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedOrigins(Collections.singletonList("*")); + corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name())); + source.registerCorsConfiguration("/**", corsConfiguration); + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated()) + .cors((cors) -> cors + .configurationSource(source) + .preFlightRequestHandler((request, response) -> response.addHeader("X-Pre-Flight", "Handled"))); + return http.build(); + // @formatter:on + } + + } + + @Configuration + @EnableWebSecurity + static class PreFlightRequestHandlerMemberWithCorsFilterBeanConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated()) + .cors((cors) -> cors.preFlightRequestHandler( + (request, response) -> response.addHeader("X-Pre-Flight", "Member"))); + return http.build(); + // @formatter:on + } + + @Bean + CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedOrigins(Collections.singletonList("*")); + corsConfiguration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name())); + source.registerCorsConfiguration("/**", corsConfiguration); + return new CorsFilter(source); + } + + } + } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/CorsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/CorsDslTests.kt index cdaeebb997..9821f3878b 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/CorsDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/CorsDslTests.kt @@ -16,7 +16,10 @@ package org.springframework.security.config.annotation.web +import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy +import org.assertj.core.api.Assertions.catchThrowable +import org.assertj.core.util.Throwables import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.BeanCreationException @@ -35,9 +38,14 @@ import org.springframework.test.web.servlet.get import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMethod import org.springframework.web.bind.annotation.RestController +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.cors.CorsConfiguration import org.springframework.web.cors.CorsConfigurationSource +import org.springframework.web.cors.PreFlightRequestHandler import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import org.springframework.web.filter.CorsFilter import org.springframework.web.servlet.config.annotation.EnableWebMvc /** @@ -153,7 +161,7 @@ class CorsDslTests { @Test fun `CORS when CORS configuration source dsl then responds with CORS header`() { - this.spring.register(CorsCrossOriginBeanConfig::class.java, HomeController::class.java).autowire() + this.spring.register(CorsCrossOriginSourceConfig::class.java, HomeController::class.java).autowire() this.mockMvc.get("/") { @@ -185,6 +193,117 @@ class CorsDslTests { } } + @Test + fun `CORS when preFlight request handler dsl then OPTIONS uses handler`() { + this.spring.register(PreFlightRequestHandlerDslConfig::class.java).autowire() + + this.mockMvc.perform(options("/") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, RequestMethod.POST.name) + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(status().isOk) + .andExpect(header().exists("X-Pre-Flight")) + } + + @Configuration + @EnableWebSecurity + open class PreFlightRequestHandlerDslConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + cors { + preFlightRequestHandler = PreFlightRequestHandler { _, response -> + response.addHeader("X-Pre-Flight", "Dsl") + } + } + } + return http.build() + } + } + + @Test + fun `CORS when configuration source and preFlight handler dsl then illegal state`() { + val thrown = catchThrowable { + this.spring.register(BothCorsDslMembersConfig::class.java).autowire() + } + assertThat(thrown).isInstanceOf(BeanCreationException::class.java) + assertThat(Throwables.getRootCause(thrown)) + .isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("Cannot configure both") + } + + @Configuration + @EnableWebSecurity + open class BothCorsDslMembersConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + val source = UrlBasedCorsConfigurationSource() + val corsConfiguration = CorsConfiguration() + corsConfiguration.allowedOrigins = listOf("*") + corsConfiguration.allowedMethods = listOf( + RequestMethod.GET.name, + RequestMethod.POST.name) + source.registerCorsConfiguration("/**", corsConfiguration) + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + cors { + configurationSource = source + preFlightRequestHandler = PreFlightRequestHandler { _, response -> + response.addHeader("X-Pre-Flight", "Dsl") + } + } + } + return http.build() + } + } + + @Test + fun `CORS when preFlight handler dsl then CorsFilter bean ignored on OPTIONS`() { + this.spring.register(PreFlightRequestHandlerDslWithCorsFilterBeanConfig::class.java).autowire() + + this.mockMvc.perform(options("/") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, RequestMethod.POST.name) + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(status().isOk) + .andExpect(header().exists("X-Pre-Flight")) + .andExpect(header().doesNotExist("Access-Control-Allow-Origin")) + } + + @Configuration + @EnableWebSecurity + open class PreFlightRequestHandlerDslWithCorsFilterBeanConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + cors { + preFlightRequestHandler = PreFlightRequestHandler { _, response -> + response.addHeader("X-Pre-Flight", "Dsl") + } + } + } + return http.build() + } + + @Bean + open fun corsFilter(): CorsFilter { + val source = UrlBasedCorsConfigurationSource() + val corsConfiguration = CorsConfiguration() + corsConfiguration.allowedOrigins = listOf("*") + corsConfiguration.allowedMethods = listOf( + RequestMethod.GET.name, + RequestMethod.POST.name) + source.registerCorsConfiguration("/**", corsConfiguration) + return CorsFilter(source) + } + } + @RestController private class HomeController { @GetMapping("/") diff --git a/docs/modules/ROOT/pages/servlet/integrations/cors.adoc b/docs/modules/ROOT/pages/servlet/integrations/cors.adoc index 5878b20a6d..099389ec65 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/cors.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/cors.adoc @@ -184,6 +184,23 @@ fun corsConfigurationSource(): UrlBasedCorsConfigurationSource { ---- ====== +[[cors-preflight-request-handler]] +== `PreFlightRequestHandler` and `PreFlightRequestFilter` + +Spring Framework defines {spring-framework-api-url}org/springframework/web/cors/PreFlightRequestHandler.html[`PreFlightRequestHandler`] for applications that need to handle CORS preflight (`OPTIONS`) requests outside of `CorsFilter`. +When Spring Security selects a `PreFlightRequestHandler` for a filter chain, it registers {spring-framework-api-url}org/springframework/web/filter/PreFlightRequestFilter.html[`PreFlightRequestFilter`] in the security filter chain (before `CorsFilter`) so preflight can be handled early in the request lifecycle. + +You can supply a handler in either of these ways: + +* Pass a handler directly with the `preFlightRequestHandler` attribute. +* Register a `PreFlightRequestHandler` bean when cors is enabled and when no `CorsConfigurationSource` or `CorsFilter` is chosen for that chain. + +You must not configure both `configurationSource` and `preFlightRequestHandler` on the same `CorsConfigurer`; doing so results in an error at startup. + +The following example explicitly registers a `PreFlightRequestHandler` using the `preFlightRequestHandler`: + +include-code::./CorsPreFlightRequestHandlerExample[tag=preflightRequestHandler,indent=0] + [WARNING] ==== CORS is a browser-based security feature. diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index b0c2feed25..50e5baa9d8 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -8,6 +8,7 @@ * Added xref:servlet/authorization/architecture.adoc#authz-conditional-authorization-manager[ConditionalAuthorizationManager] * Added `when` and `withWhen` conditions to `AuthorizationManagerFactories.multiFactor()` for xref:servlet/authentication/mfa.adoc#programmatic-mfa[Programmatic MFA] * Added `MultiFactorCondition.WEBAUTHN_REGISTERED` to `@EnableMultiFactorAuthentication(when = ...)` for xref:servlet/authentication/mfa.adoc#mfa-when-webauthn-registered[conditionally requiring MFA for WebAuthn Users] +* https://github.com/spring-projects/spring-security/issues/18926[gh-18926] - xref:servlet/integrations/cors.adoc[Add `PreFlightRequestFilter` Support] == OAuth 2.0 diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/integrations/corspreflightrequesthandler/CorsPreFlightRequestHandlerExample.java b/docs/src/test/java/org/springframework/security/docs/servlet/integrations/corspreflightrequesthandler/CorsPreFlightRequestHandlerExample.java new file mode 100644 index 0000000000..3022eafbd7 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/integrations/corspreflightrequesthandler/CorsPreFlightRequestHandlerExample.java @@ -0,0 +1,49 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.integrations.corspreflightrequesthandler; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.PreFlightRequestHandler; + +@Configuration +@EnableWebSecurity +class CorsPreFlightRequestHandlerExample { + + @Bean + PreFlightRequestHandler preFlightRequestHandler() { + return (request, response) -> { + // custom preflight handling (for example, write CORS headers or complete the response) + }; + } + + @Bean + SecurityFilterChain springSecurity(HttpSecurity http, PreFlightRequestHandler preFlightRequestHandler) { + // tag::preflightRequestHandler[] + http + // .. + .cors((cors) -> cors + .preFlightRequestHandler(preFlightRequestHandler) + ); + return http.build(); + // end::preflightRequestHandler[] + } + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/integrations/corspreflightrequesthandler/CorsPreFlightRequestHandlerExample.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/integrations/corspreflightrequesthandler/CorsPreFlightRequestHandlerExample.kt new file mode 100644 index 0000000000..96511caab9 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/integrations/corspreflightrequesthandler/CorsPreFlightRequestHandlerExample.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.servlet.integrations.corspreflightrequesthandler + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.web.SecurityFilterChain +import org.springframework.web.cors.PreFlightRequestHandler + +@Configuration +@EnableWebSecurity +class CorsPreFlightRequestHandlerExample { + + @Bean + fun preFlightRequestHandler(): PreFlightRequestHandler { + return PreFlightRequestHandler { _, _ -> + // custom preflight handling (for example, write CORS headers or complete the response) + } + } + + // tag::preflightRequestHandler[] + @Bean + fun springSecurity(http: HttpSecurity, preFlightRequestHandler: PreFlightRequestHandler): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + cors { + this.preFlightRequestHandler = preFlightRequestHandler + } + } + return http.build() + } + // end::preflightRequestHandler[] + +}