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())