From 92314b0956295a42123b5b6d7e0cd6a7124ff6c8 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Wed, 19 Jun 2019 12:55:26 -0400 Subject: [PATCH] Allow configuration of logout through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 47 ++++++++++ .../configurers/LogoutConfigurerTests.java | 90 ++++++++++++++++++- .../configurers/NamespaceHttpLogoutTests.java | 74 +++++++++++++++ 3 files changed, 209 insertions(+), 2 deletions(-) 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 e5e588a40a..565acac30b 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 @@ -773,6 +773,53 @@ public final class HttpSecurity extends return getOrApply(new LogoutConfigurer<>()); } + /** + * Provides logout support. This is automatically applied when using + * {@link WebSecurityConfigurerAdapter}. The default is that accessing the URL + * "/logout" will log the user out by invalidating the HTTP Session, cleaning up any + * {@link #rememberMe()} authentication that was configured, clearing the + * {@link SecurityContextHolder}, and then redirect to "/login?success". + * + *

Example Custom Configuration

+ * + * The following customization to log out when the URL "/custom-logout" is invoked. + * Log out will remove the cookie named "remove", not invalidate the HttpSession, + * clear the SecurityContextHolder, and upon completion redirect to "/logout-success". + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers("/**").hasRole("USER")
+	 * 				.and()
+	 * 			.formLogin()
+	 * 				.and()
+	 * 			// sample logout customization
+	 * 			.logout(logout ->
+	 * 				logout.deleteCookies("remove")
+	 * 					.invalidateHttpSession(false)
+	 * 					.logoutUrl("/custom-logout")
+	 * 					.logoutSuccessUrl("/logout-success")
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * @param logoutCustomizer the {@link Customizer} to provide more options for + * the {@link LogoutConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity logout(Customizer> logoutCustomizer) throws Exception { + logoutCustomizer.customize(getOrApply(new LogoutConfigurer<>())); + return HttpSecurity.this; + } + /** * Allows configuring how an anonymous user is represented. This is automatically * applied when used in conjunction with {@link WebSecurityConfigurerAdapter}. By 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 3ef62bf4a8..86bf87a6be 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 @@ -37,10 +37,15 @@ import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -77,6 +82,26 @@ public class LogoutConfigurerTests { } } + @Test + public void configureWhenDefaultLogoutSuccessHandlerForHasNullLogoutHandlerInLambdaThenException() { + assertThatThrownBy(() -> this.spring.register(NullLogoutSuccessHandlerInLambdaConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseInstanceOf(IllegalArgumentException.class); + } + + @EnableWebSecurity + static class NullLogoutSuccessHandlerInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout(logout -> + logout.defaultLogoutSuccessHandlerFor(null, mock(RequestMatcher.class)) + ); + // @formatter:on + } + } + @Test public void configureWhenDefaultLogoutSuccessHandlerForHasNullMatcherThenException() { assertThatThrownBy(() -> this.spring.register(NullMatcherConfig.class).autowire()) @@ -96,6 +121,26 @@ public class LogoutConfigurerTests { } } + @Test + public void configureWhenDefaultLogoutSuccessHandlerForHasNullMatcherInLambdaThenException() { + assertThatThrownBy(() -> this.spring.register(NullMatcherInLambdaConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseInstanceOf(IllegalArgumentException.class); + } + + @EnableWebSecurity + static class NullMatcherInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout(logout -> + logout.defaultLogoutSuccessHandlerFor(mock(LogoutSuccessHandler.class), null) + ); + // @formatter:on + } + } + @Test public void configureWhenRegisteringObjectPostProcessorThenInvokedOnLogoutFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); @@ -263,6 +308,29 @@ public class LogoutConfigurerTests { } } + @Test + public void logoutWhenCustomLogoutUrlInLambdaThenRedirectsToLogin() throws Exception { + this.spring.register(CsrfDisabledAndCustomLogoutInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/custom/logout")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?logout")); + } + + @EnableWebSecurity + static class CsrfDisabledAndCustomLogoutInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf() + .disable() + .logout(logout -> logout.logoutUrl("/custom/logout")); + // @formatter:on + } + } + // SEC-3170 @Test public void configureWhenLogoutHandlerNullThenException() { @@ -283,6 +351,24 @@ public class LogoutConfigurerTests { } } + @Test + public void configureWhenLogoutHandlerNullInLambdaThenException() { + assertThatThrownBy(() -> this.spring.register(NullLogoutHandlerInLambdaConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseInstanceOf(IllegalArgumentException.class); + } + + @EnableWebSecurity + static class NullLogoutHandlerInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout(logout -> logout.addLogoutHandler(null)); + // @formatter:on + } + } + // SEC-3170 @Test public void rememberMeWhenRememberMeServicesNotLogoutHandlerThenRedirectsToLogin() throws Exception { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.java index cd0bf72dec..a1ad03748d 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.java @@ -41,9 +41,11 @@ import org.springframework.test.web.servlet.ResultMatcher; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Tests to verify that all the functionality of attributes is present @@ -83,6 +85,23 @@ public class NamespaceHttpLogoutTests { } } + @Test + @WithMockUser + public void logoutWhenDisabledInLambdaThenRespondsWithNotFound() throws Exception { + this.spring.register(HttpLogoutDisabledInLambdaConfig.class).autowire(); + + this.mvc.perform(post("/logout").with(csrf()).with(user("user"))) + .andExpect(status().isNotFound()); + } + + @EnableWebSecurity + static class HttpLogoutDisabledInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.logout(AbstractHttpConfigurer::disable); + } + } + /** * http/logout custom */ @@ -112,6 +131,35 @@ public class NamespaceHttpLogoutTests { } } + @Test + @WithMockUser + public void logoutWhenUsingVariousCustomizationsInLambdaThenMatchesNamespace() throws Exception { + this.spring.register(CustomHttpLogoutInLambdaConfig.class).autowire(); + + this.mvc.perform(post("/custom-logout").with(csrf())) + .andExpect(authenticated(false)) + .andExpect(redirectedUrl("/logout-success")) + .andExpect(result -> assertThat(result.getResponse().getCookies()).hasSize(1)) + .andExpect(cookie().maxAge("remove", 0)) + .andExpect(session(Objects::nonNull)); + } + + @EnableWebSecurity + static class CustomHttpLogoutInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout(logout -> + logout.deleteCookies("remove") + .invalidateHttpSession(false) + .logoutUrl("/custom-logout") + .logoutSuccessUrl("/logout-success") + ); + // @formatter:on + } + } + /** * http/logout@success-handler-ref */ @@ -141,6 +189,32 @@ public class NamespaceHttpLogoutTests { } } + @Test + @WithMockUser + public void logoutWhenUsingSuccessHandlerRefInLambdaThenMatchesNamespace() throws Exception { + this.spring.register(SuccessHandlerRefHttpLogoutInLambdaConfig.class).autowire(); + + this.mvc.perform(post("/logout").with(csrf())) + .andExpect(authenticated(false)) + .andExpect(redirectedUrl("/SuccessHandlerRefHttpLogoutConfig")) + .andExpect(noCookies()) + .andExpect(session(Objects::isNull)); + } + + @EnableWebSecurity + static class SuccessHandlerRefHttpLogoutInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler(); + logoutSuccessHandler.setDefaultTargetUrl("/SuccessHandlerRefHttpLogoutConfig"); + + // @formatter:off + http + .logout(logout -> logout.logoutSuccessHandler(logoutSuccessHandler)); + // @formatter:on + } + } + ResultMatcher authenticated(boolean authenticated) { return result -> assertThat( Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())