diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java index 23f7150461..82ff4312c0 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java @@ -54,6 +54,7 @@ import org.springframework.security.web.session.ConcurrentSessionFilter; import org.springframework.security.web.session.DisableEncodeUrlFilter; import org.springframework.security.web.session.ForceEagerSessionCreationFilter; import org.springframework.security.web.session.SessionManagementFilter; +import org.springframework.security.web.transport.HttpsRedirectFilter; import org.springframework.web.filter.CorsFilter; /** @@ -78,6 +79,7 @@ final class FilterOrderRegistration { put(DisableEncodeUrlFilter.class, order.next()); put(ForceEagerSessionCreationFilter.class, order.next()); put(ChannelProcessingFilter.class, order.next()); + put(HttpsRedirectFilter.class, order.next()); order.next(); // gh-8105 put(WebAsyncManagerIntegrationFilter.class, order.next()); put(SecurityContextHolderFilter.class, order.next()); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 778a1243ad..482bc29af7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -59,6 +59,7 @@ import org.springframework.security.config.annotation.web.configurers.Expression import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; +import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer; import org.springframework.security.config.annotation.web.configurers.JeeConfigurer; import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; import org.springframework.security.config.annotation.web.configurers.PasswordManagementConfigurer; @@ -3145,6 +3146,53 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderExample Configuration + * + * The example below demonstrates how to require HTTPS for every request. Only + * requiring HTTPS for some requests is supported, for example if you need to + * differentiate between local and production deployments. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequireHttpsConfig {
+	 *
+	 * 	@Bean
+	 * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeHttpRequests((authorize) -> authorize
+	 * 				anyRequest().authenticated()
+	 * 			)
+	 * 			.formLogin(withDefaults())
+	 * 			.redirectToHttps(withDefaults());
+	 * 		return http.build();
+	 * 	}
+	 *
+	 * 	@Bean
+	 * 	public UserDetailsService userDetailsService() {
+	 * 		UserDetails user = User.withDefaultPasswordEncoder()
+	 * 			.username("user")
+	 * 			.password("password")
+	 * 			.roles("USER")
+	 * 			.build();
+	 * 		return new InMemoryUserDetailsManager(user);
+	 * 	}
+	 * }
+	 * 
+ * @param httpsRedirectConfigurerCustomizer the {@link Customizer} to provide more + * options for the {@link HttpsRedirectConfigurer} + * @return the {@link HttpSecurity} for further customizations + */ + public HttpSecurity redirectToHttps( + Customizer> httpsRedirectConfigurerCustomizer) throws Exception { + httpsRedirectConfigurerCustomizer.customize(getOrApply(new HttpsRedirectConfigurer<>())); + return HttpSecurity.this; + } + /** * Configures HTTP Basic authentication. * diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpsRedirectConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpsRedirectConfigurer.java new file mode 100644 index 0000000000..c5b168d1a6 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpsRedirectConfigurer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2025 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.config.annotation.web.configurers; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.web.PortMapper; +import org.springframework.security.web.transport.HttpsRedirectFilter; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +/** + * Specifies for what requests the application should redirect to HTTPS. When this + * configurer is added, it redirects all HTTP requests by default to HTTPS. + * + *

Security Filters

+ * + * The following Filters are populated + * + * + * + *

Shared Objects Created

+ * + * No shared objects are created. + * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + * + * + * @param the type of {@link HttpSecurityBuilder} that is being configured + * @author Josh Cummings + * @since 6.5 + */ +public final class HttpsRedirectConfigurer> + extends AbstractHttpConfigurer, H> { + + private RequestMatcher requestMatcher; + + public HttpsRedirectConfigurer requestMatchers(RequestMatcher... matchers) { + this.requestMatcher = new OrRequestMatcher(matchers); + return this; + } + + @Override + public void configure(H http) throws Exception { + HttpsRedirectFilter filter = new HttpsRedirectFilter(); + if (this.requestMatcher != null) { + filter.setRequestMatcher(this.requestMatcher); + } + PortMapper mapper = http.getSharedObject(PortMapper.class); + if (mapper != null) { + filter.setPortMapper(mapper); + } + http.addFilter(filter); + } + +} diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt index 528f8021e9..bd1b6e0bc0 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt @@ -533,6 +533,39 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu this.http.requiresChannel(requiresChannelCustomizer) } + /** + * Configures channel security. In order for this configuration to be useful at least + * one mapping to a required channel must be provided. + * + * Example: + * + * The example below demonstrates how to require HTTPS for every request. Only + * requiring HTTPS for some requests is supported, for example if you need to differentiate + * between local and production deployments. + * + * ``` + * @Configuration + * @EnableWebSecurity + * class RequireHttpsConfig { + * + * @Bean + * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + * http { + * redirectToHttps { } + * } + * return http.build(); + * } + * } + * ``` + * @param httpsRedirectConfiguration custom configuration to apply to HTTPS redirect rules + * @see [HttpsRedirectDsl] + * @since 6.5 + */ + fun redirectToHttps(httpsRedirectConfiguration: HttpsRedirectDsl.() -> Unit) { + val httpsRedirectCustomizer = HttpsRedirectDsl().apply(httpsRedirectConfiguration).get() + this.http.redirectToHttps(httpsRedirectCustomizer) + } + /** * Adds X509 based pre authentication to an application * diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpsRedirectDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpsRedirectDsl.kt new file mode 100644 index 0000000000..160b08e732 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpsRedirectDsl.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2020 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.config.annotation.web + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer +import org.springframework.security.web.PortMapper +import org.springframework.security.web.util.matcher.RequestMatcher + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] HTTPS redirection rules using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property portMapper the [PortMapper] that specifies a custom HTTPS port to redirect to. + */ +@SecurityMarker +class HttpsRedirectDsl { + var requestMatchers: Array? = null + + internal fun get(): (HttpsRedirectConfigurer) -> Unit { + return { https -> + requestMatchers?.also { https.requestMatchers(*requestMatchers!!) } + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpsRedirectConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpsRedirectConfigurerTests.java new file mode 100644 index 0000000000..944f21203a --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpsRedirectConfigurerTests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2019 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.config.annotation.web.configurers; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +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.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; +import org.springframework.security.web.PortMapper; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link HttpsRedirectConfigurerTests} + * + * @author Josh Cummings + */ +@ExtendWith(SpringTestContextExtension.class) +public class HttpsRedirectConfigurerTests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + MockMvc mvc; + + @Test + public void getWhenSecureThenDoesNotRedirect() throws Exception { + this.spring.register(RedirectToHttpConfig.class).autowire(); + // @formatter:off + this.mvc.perform(get("https://localhost")) + .andExpect(status().isNotFound()); + // @formatter:on + } + + @Test + public void getWhenInsecureThenRespondsWithRedirectToSecure() throws Exception { + this.spring.register(RedirectToHttpConfig.class).autowire(); + // @formatter:off + this.mvc.perform(get("http://localhost")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("https://localhost")); + // @formatter:on + } + + @Test + public void getWhenInsecureAndPathRequiresTransportSecurityThenRedirects() throws Exception { + this.spring.register(SometimesRedirectToHttpsConfig.class, UsePathPatternConfig.class).autowire(); + // @formatter:off + this.mvc.perform(get("http://localhost:8080")) + .andExpect(status().isNotFound()); + this.mvc.perform(get("http://localhost:8080/secure")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("https://localhost:8443/secure")); + // @formatter:on + } + + @Test + public void getWhenInsecureAndUsingCustomPortMapperThenRespondsWithRedirectToSecurePort() throws Exception { + this.spring.register(RedirectToHttpsViaCustomPortsConfig.class).autowire(); + PortMapper portMapper = this.spring.getContext().getBean(PortMapper.class); + given(portMapper.lookupHttpsPort(4080)).willReturn(4443); + // @formatter:off + this.mvc.perform(get("http://localhost:4080")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("https://localhost:4443")); + // @formatter:on + } + + @Configuration + @EnableWebMvc + @EnableWebSecurity + static class RedirectToHttpConfig { + + @Bean + SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + // @formatter:off + http + .redirectToHttps(withDefaults()); + // @formatter:on + return http.build(); + } + + } + + @Configuration + @EnableWebMvc + @EnableWebSecurity + static class SometimesRedirectToHttpsConfig { + + @Bean + SecurityFilterChain springSecurity(HttpSecurity http, PathPatternRequestMatcher.Builder path) throws Exception { + // @formatter:off + http + .redirectToHttps((https) -> https.requestMatchers(path.matcher("/secure"))); + // @formatter:on + return http.build(); + } + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + + } + + @Configuration + @EnableWebMvc + @EnableWebSecurity + static class RedirectToHttpsViaCustomPortsConfig { + + @Bean + SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + // @formatter:off + http + .portMapper((p) -> p.portMapper(portMapper())) + .redirectToHttps(withDefaults()); + + // @formatter:on + return http.build(); + } + + @Bean + PortMapper portMapper() { + return mock(PortMapper.class); + } + + } + + @Configuration + static class UsePathPatternConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + + } + +} diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpsRedirectDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpsRedirectDslTests.kt new file mode 100644 index 0000000000..cedb9930c5 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpsRedirectDslTests.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2020 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.config.annotation.web + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean +import org.springframework.security.web.PortMapperImpl +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.web.servlet.config.annotation.EnableWebMvc +import java.net.URI +import java.util.* + +/** + * Tests for [HttpsRedirectDsl] + * + * @author Eleftheria Stein + */ +@ExtendWith(SpringTestContextExtension::class) +class HttpsRedirectDslTests { + @JvmField + val spring = SpringTestContext(this) + + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `request when matches redirect to HTTPS matcher then redirects to HTTPS`() { + this.spring.register(HttpRedirectMatcherConfig::class.java, UsePathPatternConfig::class.java).autowire() + + val result = this.mockMvc.get("/secure") + .andExpect { + status { is3xxRedirection() } + }.andReturn() + + val location = result.response.getHeader(HttpHeaders.LOCATION)?.let { URI.create(it) } + assertThat(location?.scheme).isEqualTo("https") + } + + @Test + fun `request when does not match redirect to HTTPS matcher then does not redirect`() { + this.spring.register(HttpRedirectMatcherConfig::class.java, UsePathPatternConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + status { isNotFound() } + } + } + + @Configuration + @EnableWebSecurity + @EnableWebMvc + open class HttpRedirectMatcherConfig { + @Bean + open fun springFilterChain(http: HttpSecurity, path: PathPatternRequestMatcher.Builder): SecurityFilterChain { + http { + redirectToHttps { + requestMatchers = arrayOf(path.matcher("/secure")) + } + } + return http.build() + } + } + + @Configuration + open class UsePathPatternConfig { + @Bean + open fun requestMatcherBuilder() = PathPatternRequestMatcherBuilderFactoryBean() + } + + @Test + fun `request when port mapper configured then redirected to HTTPS port`() { + this.spring.register(PortMapperConfig::class.java).autowire() + + val result = this.mockMvc.get("http://localhost:543") + .andExpect { + status { + is3xxRedirection() + } + }.andReturn() + + val location = result.response.getHeader(HttpHeaders.LOCATION)?.let { URI.create(it) } + assertThat(location?.scheme).isEqualTo("https") + assertThat(location?.port).isEqualTo(123) + } + + @Configuration + @EnableWebSecurity + @EnableWebMvc + open class PortMapperConfig { + @Bean + open fun springFilterChain(http: HttpSecurity): SecurityFilterChain { + val customPortMapper = PortMapperImpl() + customPortMapper.setPortMappings(Collections.singletonMap("543", "123")) + http { + portMapper { + portMapper = customPortMapper + } + redirectToHttps { } + } + return http.build() + } + } +} diff --git a/docs/modules/ROOT/pages/servlet/exploits/http.adoc b/docs/modules/ROOT/pages/servlet/exploits/http.adoc index 4ea9215879..fcf96bf02a 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/http.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/http.adoc @@ -27,9 +27,7 @@ public class WebSecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... - .requiresChannel(channel -> channel - .anyRequest().requiresSecure() - ); + .redirectToHttps(withDefaults()); return http.build(); } } @@ -47,9 +45,7 @@ class SecurityConfig { open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { // ... - requiresChannel { - secure(AnyRequestMatcher.INSTANCE, "REQUIRES_SECURE_CHANNEL") - } + redirectToHttps { } } return http.build() }