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 f3411352dd..0a16993345 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 @@ -2033,7 +2033,9 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder matchers = new ArrayList<>(); - PathPatternRequestMatcher.Builder builder = getSharedObject(PathPatternRequestMatcher.Builder.class); + ApplicationContext context = getSharedObject(ApplicationContext.class); + PathPatternRequestMatcher.Builder builder = context.getBeanProvider(PathPatternRequestMatcher.Builder.class) + .getIfUnique(() -> getSharedObject(PathPatternRequestMatcher.Builder.class)); for (String pattern : patterns) { matchers.add(builder.matcher(pattern)); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 619a7d4e7d..81e6c8f034 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -239,9 +239,7 @@ class HttpSecurityConfiguration { Map, Object> sharedObjects = new HashMap<>(); sharedObjects.put(ApplicationContext.class, this.context); sharedObjects.put(ContentNegotiationStrategy.class, this.contentNegotiationStrategy); - sharedObjects.put(PathPatternRequestMatcher.Builder.class, - this.context.getBeanProvider(PathPatternRequestMatcher.Builder.class) - .getIfUnique(() -> constructRequestMatcherBuilder(this.context))); + sharedObjects.put(PathPatternRequestMatcher.Builder.class, constructRequestMatcherBuilder(this.context)); return sharedObjects; } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java index 418f5e181f..34520754ef 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java @@ -37,6 +37,7 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @@ -110,6 +111,21 @@ public class WebSecurityTests { assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST); } + // gh-19128 + @Test + public void ignoringWhenBuilderBeanWithBasePathThenHonorsBasePath() throws Exception { + loadConfig(IgnoringBuilderBeanConfig.class); + this.request.setServletPath("/spring"); + this.request.setRequestURI("/spring/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + setup(); + this.request.setServletPath(""); + this.request.setRequestURI("/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); @@ -201,6 +217,52 @@ public class WebSecurityTests { } + // gh-19128 + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class IgnoringBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().requestMatchers("/path"); + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic(withDefaults()) + .authorizeHttpRequests((requests) -> requests + .anyRequest().denyAll()); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(PasswordEncodedUser.user()); + } + + @RestController + static class PathController { + + @RequestMapping("/path") + String path() { + return "path"; + } + + } + + } + @Configuration @EnableWebSecurity static class RequestRejectedHandlerConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java index 3bee5bc0d4..d355c82693 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java @@ -40,6 +40,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.users.AuthenticationTestConfiguration; +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; import org.springframework.security.core.authority.FactorGrantedAuthority; import org.springframework.security.core.context.SecurityContextChangedListener; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -153,6 +154,17 @@ public class FormLoginConfigurerTests { // @formatter:on } + // gh-19128 + @Test + public void loginWhenBuilderBeanWithBasePathThenLoginProcessingUrlIgnoresBasePath() throws Exception { + this.spring.register(FormLoginBuilderBeanConfig.class).autowire(); + // @formatter:off + this.mockMvc.perform(formLogin().user("invalid")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?error")); + // @formatter:on + } + @Test public void loginWhenFormLoginConfiguredThenHasDefaultSuccessUrl() throws Exception { this.spring.register(FormLoginConfig.class).autowire(); @@ -519,6 +531,37 @@ public class FormLoginConfigurerTests { } + // gh-19128 + @Configuration + @EnableWebSecurity + @EnableWebMvc + static class FormLoginBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated()) + .formLogin(withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(PasswordEncodedUser.user()); + } + + } + @Configuration @EnableWebSecurity static class FormLoginInLambdaConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java index 79cd3b0459..7c1087af4b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java @@ -35,6 +35,7 @@ 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.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.UserDetailsService; @@ -127,6 +128,17 @@ public class LogoutConfigurerTests { // @formatter:on } + // gh-19128 + @Test + public void logoutWhenBuilderBeanWithBasePathThenLogoutUrlIgnoresBasePath() throws Exception { + this.spring.register(LogoutBuilderBeanConfig.class).autowire(); + // @formatter:off + this.mvc.perform(post("/logout").with(csrf())) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?logout")); + // @formatter:on + } + // SEC-2311 @Test public void logoutWhenGetRequestAndCsrfDisabledThenRedirectsToLogin() throws Exception { @@ -524,6 +536,29 @@ public class LogoutConfigurerTests { } + // gh-19128 + @Configuration + @EnableWebSecurity + static class LogoutBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout(withDefaults()); + // @formatter:on + return http.build(); + } + + } + @Configuration @EnableWebSecurity static class CsrfDisabledConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurerTests.java index 0b65e42d4d..f90626fa80 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurerTests.java @@ -26,6 +26,7 @@ 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.SecurityFilterChain; import org.springframework.test.web.servlet.MockMvc; @@ -66,6 +67,16 @@ public class PasswordManagementConfigurerTests { .andExpect(redirectedUrl("/custom-change-password-page")); } + // gh-19128 + @Test + public void changePasswordWhenBuilderBeanWithBasePathThenChangePasswordUrlIgnoresBasePath() throws Exception { + this.spring.register(PasswordManagementBuilderBeanConfig.class).autowire(); + + this.mvc.perform(get("/.well-known/change-password")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/change-password")); + } + @Test public void whenSettingNullChangePasswordPage() { PasswordManagementConfigurer configurer = new PasswordManagementConfigurer(); @@ -102,6 +113,29 @@ public class PasswordManagementConfigurerTests { } + // gh-19128 + @Configuration + @EnableWebSecurity + static class PasswordManagementBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .passwordManagement(withDefaults()) + .build(); + // @formatter:on + } + + } + @Configuration @EnableWebSecurity static class PasswordManagementWithCustomChangePasswordPageConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java index f4d4849467..cce83b88b5 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java @@ -34,6 +34,7 @@ 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.core.userdetails.User; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.test.web.servlet.RequestCacheResultMatcher; @@ -185,6 +186,21 @@ public class RequestCacheConfigurerTests { this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/")); } + // gh-19128 + @Test + public void getWhenBuilderBeanWithBasePathThenSavedRequestMatcherIgnoresBasePath() throws Exception { + this.spring.register(RequestCacheBuilderBeanConfig.class, DefaultSecurityConfig.class).autowire(); + MockHttpServletRequestBuilder request = get("/messages").header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); + // @formatter:off + MockHttpSession session = (MockHttpSession) this.mvc.perform(request) + .andExpect(redirectedUrl("/login")) + .andReturn() + .getRequest() + .getSession(); + // @formatter:on + this.mvc.perform(formLogin(session)).andExpect(RequestCacheResultMatcher.redirectToCachedRequest()); + } + @Test public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemembers() throws Exception { this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire(); @@ -401,6 +417,31 @@ public class RequestCacheConfigurerTests { } + // gh-19128 + @Configuration + @EnableWebSecurity + static class RequestCacheBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated()) + .formLogin(withDefaults()); + // @formatter:on + return http.build(); + } + + } + @Configuration @EnableWebSecurity static class RequestCacheDisabledConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index 2bb49d775c..5aadbdcb2b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -56,6 +56,7 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; 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.context.DelegatingApplicationListener; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -440,6 +441,18 @@ public class OAuth2LoginConfigurerTests { then(redirectStrategy).should().sendRedirect(any(), any(), anyString()); } + // gh-19128 + @Test + public void oauth2LoginWhenBuilderBeanWithBasePathThenLoginProcessingUrlIgnoresBasePath() throws Exception { + loadConfig(OAuth2LoginBuilderBeanConfig.class); + String requestUri = "/login/oauth2/code/google"; + this.request = get(requestUri).build(); + this.request.setParameter("code", "code123"); + this.request.setParameter("state", "state123"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + assertThat(this.response.getRedirectedUrl()).endsWith("/login?error"); + } + // gh-5347 @Test public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() throws Exception { @@ -788,6 +801,31 @@ public class OAuth2LoginConfigurerTests { } + // gh-19128 + @Configuration + @EnableWebSecurity + static class OAuth2LoginBuilderBeanConfig extends CommonSecurityFilterChainConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .oauth2Login((login) -> login + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION))); + // @formatter:on + return super.configureFilterChain(http); + } + + } + @Configuration @EnableWebSecurity static class OAuth2LoginConfigFormLogin extends CommonSecurityFilterChainConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java index e5dd999525..e87d82037e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java @@ -40,6 +40,7 @@ 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.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @@ -98,6 +99,19 @@ public class OneTimeTokenLoginConfigurerTests { .andExpectAll(status().isFound(), redirectedUrl("/"), authenticated()); } + // gh-19128 + @Test + void oneTimeTokenWhenBuilderBeanWithBasePathThenGenerateAndLoginUrlsIgnoreBasePath() throws Exception { + this.spring.register(OneTimeTokenBuilderBeanConfig.class).autowire(); + this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/login/ott")); + + String token = getLastToken().getTokenValue(); + + this.mvc.perform(post("/login/ott").param("token", token).with(csrf())) + .andExpectAll(status().isFound(), redirectedUrl("/"), authenticated()); + } + @Test void oneTimeTokenWhenDifferentAuthenticationUrlsThenCanAuthenticate() throws Exception { this.spring.register(OneTimeTokenDifferentUrlsConfig.class).autowire(); @@ -296,6 +310,41 @@ public class OneTimeTokenLoginConfigurerTests { } + // gh-19128 + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + @Import(UserDetailsServiceConfig.class) + static class OneTimeTokenBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http, + OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + .oneTimeTokenLogin((ott) -> ott + .tokenGenerationSuccessHandler(ottSuccessHandler) + ); + // @formatter:on + return http.build(); + } + + @Bean + TestOneTimeTokenGenerationSuccessHandler ottSuccessHandler() { + return new TestOneTimeTokenGenerationSuccessHandler(); + } + + } + @Configuration(proxyBeanMethods = false) @EnableWebSecurity @Import(UserDetailsServiceConfig.class) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index 5289bc9140..36be1a2ea5 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -57,6 +57,7 @@ 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.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -244,6 +245,14 @@ public class Saml2LoginConfigurerTests { performSaml2Login("ROLE_AUTH_MANAGER"); } + // gh-19128 + @Test + public void saml2LoginWhenBuilderBeanWithBasePathThenAuthenticateUriIgnoresBasePath() throws Exception { + this.spring.register(Saml2LoginBuilderBeanConfig.class).autowire(); + MvcResult result = this.mvc.perform(get("/saml2/authenticate/registration-id")).andReturn(); + assertThat(result.getResponse().getRedirectedUrl()).contains("SAMLRequest"); + } + @Test public void authenticationRequestWhenAuthenticationRequestResolverBeanThenUses() throws Exception { this.spring.register(CustomAuthenticationRequestResolverBean.class).autowire(); @@ -522,6 +531,33 @@ public class Saml2LoginConfigurerTests { } + // gh-19128 + @Configuration + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class Saml2LoginBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authz) -> authz + .anyRequest().authenticated() + ) + .saml2Login(Customizer.withDefaults()); + // @formatter:on + return http.build(); + } + + } + @Configuration @EnableWebSecurity @Import(Saml2LoginConfigBeans.class) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java index 69ab0be567..11450950a1 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java @@ -49,6 +49,7 @@ 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.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.saml2.core.Saml2Utils; @@ -196,6 +197,17 @@ public class Saml2LogoutConfigurerTests { verify(logoutHandler).logout(any(), any(), any()); } + // gh-19128 + @Test + public void saml2LogoutWhenBuilderBeanWithBasePathThenLogoutUrlIgnoresBasePath() throws Exception { + this.spring.register(Saml2LogoutBuilderBeanConfig.class).autowire(); + MvcResult result = this.mvc.perform(post("/logout").with(authentication(this.user)).with(csrf())) + .andExpect(status().isFound()) + .andReturn(); + String location = result.getResponse().getHeader("Location"); + assertThat(location).startsWith("https://ap.example.org/logout/saml2/request"); + } + @Test public void saml2LogoutWhenUnauthenticatedThenEntryPoint() throws Exception { this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); @@ -579,6 +591,32 @@ public class Saml2LogoutConfigurerTests { } + // gh-19128 + @Configuration + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class Saml2LogoutBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) + .saml2Login(withDefaults()) + .saml2Logout(withDefaults()); + // @formatter:on + return http.build(); + } + + } + @Configuration @EnableWebSecurity @Import(Saml2LoginConfigBeans.class) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java index 470e99aaea..ea59a1b9ff 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java @@ -30,6 +30,7 @@ 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.saml2.provider.service.metadata.OpenSaml5MetadataResolver; import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse; @@ -124,6 +125,15 @@ public class Saml2MetadataConfigurerTests { this.mvc.perform(get("/saml2/metadata")).andExpect(status().isForbidden()); } + // gh-19128 + @Test + void saml2MetadataWhenBuilderBeanWithBasePathThenMetadataUrlIgnoresBasePath() throws Exception { + this.spring.register(MetadataBuilderBeanConfig.class).autowire(); + this.mvc.perform(get("/saml/metadata")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("md:EntityDescriptor"))); + } + @EnableWebSecurity @Configuration @Import(RelyingPartyRegistrationConfig.class) @@ -141,6 +151,31 @@ public class Saml2MetadataConfigurerTests { } + // gh-19128 + @EnableWebSecurity + @Configuration + @Import(RelyingPartyRegistrationConfig.class) + static class MetadataBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain filters(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) + .saml2Metadata((saml2) -> saml2.metadataUrl("/saml/metadata")); + return http.build(); + // @formatter:on + } + + } + @EnableWebSecurity @Configuration @Import(RelyingPartyRegistrationConfig.class) diff --git a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc index 80c09ca9cd..c9e51882d0 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc @@ -81,6 +81,12 @@ Xml:: and no authorization changes are necessary since it simply adjusts the `LogoutFilter`. +[NOTE] +==== +The URI passed to `logoutUrl` (and to `logoutSuccessUrl`) is matched and redirected to literally. +If you want either URI to live under your application servlet's base path, include that prefix explicitly — for example, `logoutUrl("/api/logout")`. +==== + [[permit-logout-endpoints]] However, if you stand up your own logout success endpoint (or in a rare case, <>), say using {spring-framework-reference-url}web.html#spring-web[Spring MVC], you will need to permit it in Spring Security. This is because Spring MVC processes your request after Spring Security does. diff --git a/docs/modules/ROOT/pages/servlet/authentication/onetimetoken.adoc b/docs/modules/ROOT/pages/servlet/authentication/onetimetoken.adoc index d401a821b5..f78f60a8a2 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/onetimetoken.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/onetimetoken.adoc @@ -252,6 +252,13 @@ class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSucc ---- ====== +[NOTE] +==== +The URI passed to `generateTokenUrl` is matched literally. +If you want it to live under your application servlet's base path, include that prefix explicitly — for example, `generateTokenUrl("/api/ott/generate")`. +The same applies to `loginProcessingUrl` and `tokenGeneratingUrl` elsewhere on this page. +==== + [[changing-submit-page-url]] == Changing the Default Submit Page URL diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc index 6b096c38fd..51b84d81c5 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc @@ -155,6 +155,15 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain { ---- ====== +[NOTE] +==== +The URI passed to `loginPage` (and to `loginProcessingUrl`, if set separately) is matched and redirected to literally. +If your custom login page is served by an application servlet mapped under a base path, include that prefix in the URI you pass to the DSL. +For example, if your dispatcher is at `/api/*`: + +include-code::./FormLoginServletPathConfiguration[tag=loginPage,indent=0] +==== + [[servlet-authentication-form-custom-html]] When the login page is specified in the Spring Security configuration, you are responsible for rendering the page. // FIXME: default login page rendered by Spring Security diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc index e76ad48219..e00978baf6 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc @@ -642,6 +642,8 @@ This is because Spring Security requires all URIs to be absolute (minus the cont There are several other components that create request matchers for you like {spring-boot-api-url}org/springframework/boot/security/autoconfigure/web/servlet/PathRequest.html[`PathRequest#toStaticResources#atCommonLocations`] ===== +If most of your authorization rules sit under the same servlet path, you can xref:servlet/integrations/mvc.adoc#mvc-requestmatcher[publish a `PathPatternRequestMatcher.Builder` bean] with that base path; Spring Security then applies it to the string overloads of `authorizeHttpRequests((authorize) -> authorize.requestMatchers(...))`, `HttpSecurity#securityMatcher(...)` / `#securityMatchers(...)`, and `WebSecurityCustomizer#ignoring().requestMatchers(...)`. + [[match-by-custom]] === Using a Custom Matcher diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/passwords/formloginservletpath/FormLoginServletPathConfiguration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/passwords/formloginservletpath/FormLoginServletPathConfiguration.java new file mode 100644 index 0000000000..aa074fba46 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/passwords/formloginservletpath/FormLoginServletPathConfiguration.java @@ -0,0 +1,46 @@ +/* + * 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.authentication.passwords.formloginservletpath; + +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; + +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +public class FormLoginServletPathConfiguration { + + // tag::loginPage[] + @Bean + SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + .formLogin((form) -> form + .loginPage("/api/login") + .loginProcessingUrl("/api/login") + ); + // @formatter:on + return http.build(); + } + // end::loginPage[] + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/passwords/formloginservletpath/FormLoginServletPathConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/passwords/formloginservletpath/FormLoginServletPathConfiguration.kt new file mode 100644 index 0000000000..b3ce2447c3 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/passwords/formloginservletpath/FormLoginServletPathConfiguration.kt @@ -0,0 +1,32 @@ +package org.springframework.security.kt.docs.servlet.authentication.passwords.formloginservletpath + +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 + +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +class FormLoginServletPathConfiguration { + + // tag::loginPage[] + @Bean + fun springSecurity(http: HttpSecurity): SecurityFilterChain { + // @formatter:off + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + formLogin { + loginPage = "/api/login" + loginProcessingUrl = "/api/login" + } + } + return http.build() + // @formatter:on + } + // end::loginPage[] + +}