Skip to content

Commit d09ccb1

Browse files
eleftheriaskostya05983
authored andcommitted
Allow configuration of logout through nested builder
Issue: spring-projectsgh-5557
1 parent fa18297 commit d09ccb1

File tree

3 files changed

+209
-2
lines changed

3 files changed

+209
-2
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,53 @@ public LogoutConfigurer<HttpSecurity> logout() throws Exception {
773773
return getOrApply(new LogoutConfigurer<>());
774774
}
775775

776+
/**
777+
* Provides logout support. This is automatically applied when using
778+
* {@link WebSecurityConfigurerAdapter}. The default is that accessing the URL
779+
* "/logout" will log the user out by invalidating the HTTP Session, cleaning up any
780+
* {@link #rememberMe()} authentication that was configured, clearing the
781+
* {@link SecurityContextHolder}, and then redirect to "/login?success".
782+
*
783+
* <h2>Example Custom Configuration</h2>
784+
*
785+
* The following customization to log out when the URL "/custom-logout" is invoked.
786+
* Log out will remove the cookie named "remove", not invalidate the HttpSession,
787+
* clear the SecurityContextHolder, and upon completion redirect to "/logout-success".
788+
*
789+
* <pre>
790+
* &#064;Configuration
791+
* &#064;EnableWebSecurity
792+
* public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter {
793+
*
794+
* &#064;Override
795+
* protected void configure(HttpSecurity http) throws Exception {
796+
* http
797+
* .authorizeRequests()
798+
* .antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
799+
* .and()
800+
* .formLogin()
801+
* .and()
802+
* // sample logout customization
803+
* .logout(logout ->
804+
* logout.deleteCookies(&quot;remove&quot;)
805+
* .invalidateHttpSession(false)
806+
* .logoutUrl(&quot;/custom-logout&quot;)
807+
* .logoutSuccessUrl(&quot;/logout-success&quot;)
808+
* );
809+
* }
810+
* }
811+
* </pre>
812+
*
813+
* @param logoutCustomizer the {@link Customizer} to provide more options for
814+
* the {@link LogoutConfigurer}
815+
* @return the {@link HttpSecurity} for further customizations
816+
* @throws Exception
817+
*/
818+
public HttpSecurity logout(Customizer<LogoutConfigurer<HttpSecurity>> logoutCustomizer) throws Exception {
819+
logoutCustomizer.customize(getOrApply(new LogoutConfigurer<>()));
820+
return HttpSecurity.this;
821+
}
822+
776823
/**
777824
* Allows configuring how an anonymous user is represented. This is automatically
778825
* applied when used in conjunction with {@link WebSecurityConfigurerAdapter}. By

config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@
3737

3838
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3939
import static org.mockito.ArgumentMatchers.any;
40-
import static org.mockito.Mockito.*;
40+
import static org.mockito.Mockito.mock;
41+
import static org.mockito.Mockito.spy;
42+
import static org.mockito.Mockito.verify;
4143
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
4244
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
43-
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
45+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
46+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
47+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
48+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
4449
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
4550
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
4651

@@ -77,6 +82,26 @@ protected void configure(HttpSecurity http) throws Exception {
7782
}
7883
}
7984

85+
@Test
86+
public void configureWhenDefaultLogoutSuccessHandlerForHasNullLogoutHandlerInLambdaThenException() {
87+
assertThatThrownBy(() -> this.spring.register(NullLogoutSuccessHandlerInLambdaConfig.class).autowire())
88+
.isInstanceOf(BeanCreationException.class)
89+
.hasRootCauseInstanceOf(IllegalArgumentException.class);
90+
}
91+
92+
@EnableWebSecurity
93+
static class NullLogoutSuccessHandlerInLambdaConfig extends WebSecurityConfigurerAdapter {
94+
@Override
95+
protected void configure(HttpSecurity http) throws Exception {
96+
// @formatter:off
97+
http
98+
.logout(logout ->
99+
logout.defaultLogoutSuccessHandlerFor(null, mock(RequestMatcher.class))
100+
);
101+
// @formatter:on
102+
}
103+
}
104+
80105
@Test
81106
public void configureWhenDefaultLogoutSuccessHandlerForHasNullMatcherThenException() {
82107
assertThatThrownBy(() -> this.spring.register(NullMatcherConfig.class).autowire())
@@ -96,6 +121,26 @@ protected void configure(HttpSecurity http) throws Exception {
96121
}
97122
}
98123

124+
@Test
125+
public void configureWhenDefaultLogoutSuccessHandlerForHasNullMatcherInLambdaThenException() {
126+
assertThatThrownBy(() -> this.spring.register(NullMatcherInLambdaConfig.class).autowire())
127+
.isInstanceOf(BeanCreationException.class)
128+
.hasRootCauseInstanceOf(IllegalArgumentException.class);
129+
}
130+
131+
@EnableWebSecurity
132+
static class NullMatcherInLambdaConfig extends WebSecurityConfigurerAdapter {
133+
@Override
134+
protected void configure(HttpSecurity http) throws Exception {
135+
// @formatter:off
136+
http
137+
.logout(logout ->
138+
logout.defaultLogoutSuccessHandlerFor(mock(LogoutSuccessHandler.class), null)
139+
);
140+
// @formatter:on
141+
}
142+
}
143+
99144
@Test
100145
public void configureWhenRegisteringObjectPostProcessorThenInvokedOnLogoutFilter() {
101146
this.spring.register(ObjectPostProcessorConfig.class).autowire();
@@ -263,6 +308,29 @@ protected void configure(HttpSecurity http) throws Exception {
263308
}
264309
}
265310

311+
@Test
312+
public void logoutWhenCustomLogoutUrlInLambdaThenRedirectsToLogin() throws Exception {
313+
this.spring.register(CsrfDisabledAndCustomLogoutInLambdaConfig.class).autowire();
314+
315+
this.mvc.perform(get("/custom/logout"))
316+
.andExpect(status().isFound())
317+
.andExpect(redirectedUrl("/login?logout"));
318+
}
319+
320+
@EnableWebSecurity
321+
static class CsrfDisabledAndCustomLogoutInLambdaConfig extends WebSecurityConfigurerAdapter {
322+
323+
@Override
324+
protected void configure(HttpSecurity http) throws Exception {
325+
// @formatter:off
326+
http
327+
.csrf()
328+
.disable()
329+
.logout(logout -> logout.logoutUrl("/custom/logout"));
330+
// @formatter:on
331+
}
332+
}
333+
266334
// SEC-3170
267335
@Test
268336
public void configureWhenLogoutHandlerNullThenException() {
@@ -283,6 +351,24 @@ protected void configure(HttpSecurity http) throws Exception {
283351
}
284352
}
285353

354+
@Test
355+
public void configureWhenLogoutHandlerNullInLambdaThenException() {
356+
assertThatThrownBy(() -> this.spring.register(NullLogoutHandlerInLambdaConfig.class).autowire())
357+
.isInstanceOf(BeanCreationException.class)
358+
.hasRootCauseInstanceOf(IllegalArgumentException.class);
359+
}
360+
361+
@EnableWebSecurity
362+
static class NullLogoutHandlerInLambdaConfig extends WebSecurityConfigurerAdapter {
363+
@Override
364+
protected void configure(HttpSecurity http) throws Exception {
365+
// @formatter:off
366+
http
367+
.logout(logout -> logout.addLogoutHandler(null));
368+
// @formatter:on
369+
}
370+
}
371+
286372
// SEC-3170
287373
@Test
288374
public void rememberMeWhenRememberMeServicesNotLogoutHandlerThenRedirectsToLogin() throws Exception {

config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@
4141

4242
import static org.assertj.core.api.Assertions.assertThat;
4343
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
44+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
4445
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
4546
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
4647
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
48+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
4749

4850
/**
4951
* Tests to verify that all the functionality of <logout> attributes is present
@@ -83,6 +85,23 @@ protected void configure(HttpSecurity http) throws Exception {
8385
}
8486
}
8587

88+
@Test
89+
@WithMockUser
90+
public void logoutWhenDisabledInLambdaThenRespondsWithNotFound() throws Exception {
91+
this.spring.register(HttpLogoutDisabledInLambdaConfig.class).autowire();
92+
93+
this.mvc.perform(post("/logout").with(csrf()).with(user("user")))
94+
.andExpect(status().isNotFound());
95+
}
96+
97+
@EnableWebSecurity
98+
static class HttpLogoutDisabledInLambdaConfig extends WebSecurityConfigurerAdapter {
99+
@Override
100+
protected void configure(HttpSecurity http) throws Exception {
101+
http.logout(AbstractHttpConfigurer::disable);
102+
}
103+
}
104+
86105
/**
87106
* http/logout custom
88107
*/
@@ -112,6 +131,35 @@ protected void configure(HttpSecurity http) throws Exception {
112131
}
113132
}
114133

134+
@Test
135+
@WithMockUser
136+
public void logoutWhenUsingVariousCustomizationsInLambdaThenMatchesNamespace() throws Exception {
137+
this.spring.register(CustomHttpLogoutInLambdaConfig.class).autowire();
138+
139+
this.mvc.perform(post("/custom-logout").with(csrf()))
140+
.andExpect(authenticated(false))
141+
.andExpect(redirectedUrl("/logout-success"))
142+
.andExpect(result -> assertThat(result.getResponse().getCookies()).hasSize(1))
143+
.andExpect(cookie().maxAge("remove", 0))
144+
.andExpect(session(Objects::nonNull));
145+
}
146+
147+
@EnableWebSecurity
148+
static class CustomHttpLogoutInLambdaConfig extends WebSecurityConfigurerAdapter {
149+
@Override
150+
protected void configure(HttpSecurity http) throws Exception {
151+
// @formatter:off
152+
http
153+
.logout(logout ->
154+
logout.deleteCookies("remove")
155+
.invalidateHttpSession(false)
156+
.logoutUrl("/custom-logout")
157+
.logoutSuccessUrl("/logout-success")
158+
);
159+
// @formatter:on
160+
}
161+
}
162+
115163
/**
116164
* http/logout@success-handler-ref
117165
*/
@@ -141,6 +189,32 @@ protected void configure(HttpSecurity http) throws Exception {
141189
}
142190
}
143191

192+
@Test
193+
@WithMockUser
194+
public void logoutWhenUsingSuccessHandlerRefInLambdaThenMatchesNamespace() throws Exception {
195+
this.spring.register(SuccessHandlerRefHttpLogoutInLambdaConfig.class).autowire();
196+
197+
this.mvc.perform(post("/logout").with(csrf()))
198+
.andExpect(authenticated(false))
199+
.andExpect(redirectedUrl("/SuccessHandlerRefHttpLogoutConfig"))
200+
.andExpect(noCookies())
201+
.andExpect(session(Objects::isNull));
202+
}
203+
204+
@EnableWebSecurity
205+
static class SuccessHandlerRefHttpLogoutInLambdaConfig extends WebSecurityConfigurerAdapter {
206+
@Override
207+
protected void configure(HttpSecurity http) throws Exception {
208+
SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
209+
logoutSuccessHandler.setDefaultTargetUrl("/SuccessHandlerRefHttpLogoutConfig");
210+
211+
// @formatter:off
212+
http
213+
.logout(logout -> logout.logoutSuccessHandler(logoutSuccessHandler));
214+
// @formatter:on
215+
}
216+
}
217+
144218
ResultMatcher authenticated(boolean authenticated) {
145219
return result -> assertThat(
146220
Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())

0 commit comments

Comments
 (0)