Skip to content

Commit 6d1e68a

Browse files
committed
Introduce LogoutSuccessEvent
LogoutSuccessEvent is a simple AbstractAuthenticationEvent implementation which indicates successful logout. By default, LogoutConfigurer will add a new LogoutHandler called LogoutSuccessEventPublishingLogoutHandler to publish this event. This PR will also fix ConcurrentSessionFilter's composite logoutHandler, now will get LogoutHandler instances from LogoutConfigurer for consistency. Fixes gh-2900
1 parent 052256d commit 6d1e68a

File tree

10 files changed

+291
-13
lines changed

10 files changed

+291
-13
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@
2929
import org.springframework.security.web.authentication.logout.DelegatingLogoutSuccessHandler;
3030
import org.springframework.security.web.authentication.logout.LogoutFilter;
3131
import org.springframework.security.web.authentication.logout.LogoutHandler;
32+
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
3233
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
3334
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
3435
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
@@ -60,6 +61,7 @@
6061
* No shared objects are used.
6162
*
6263
* @author Rob Winch
64+
* @author Onur Kagan Ozcan
6365
* @since 3.2
6466
* @see RememberMeConfigurer
6567
*/
@@ -85,8 +87,9 @@ public LogoutConfigurer() {
8587
}
8688

8789
/**
88-
* Adds a {@link LogoutHandler}. The {@link SecurityContextLogoutHandler} is added as
89-
* the last {@link LogoutHandler} by default.
90+
* Adds a {@link LogoutHandler}.
91+
* {@link SecurityContextLogoutHandler} and {@link LogoutSuccessEventPublishingLogoutHandler} are added as
92+
* last {@link LogoutHandler} instances by default.
9093
*
9194
* @param logoutHandler the {@link LogoutHandler} to add
9295
* @return the {@link LogoutConfigurer} for further customization
@@ -329,6 +332,7 @@ List<LogoutHandler> getLogoutHandlers() {
329332
*/
330333
private LogoutFilter createLogoutFilter(H http) throws Exception {
331334
logoutHandlers.add(contextLogoutHandler);
335+
logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));
332336
LogoutHandler[] handlers = logoutHandlers
333337
.toArray(new LogoutHandler[0]);
334338
LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers);

config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@
3535
import org.springframework.security.core.session.SessionRegistryImpl;
3636
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
3737
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
38+
import org.springframework.security.web.authentication.logout.LogoutHandler;
3839
import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy;
3940
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
4041
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
@@ -54,6 +55,7 @@
5455
import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
5556
import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;
5657
import org.springframework.util.Assert;
58+
import org.springframework.util.CollectionUtils;
5759

5860
/**
5961
* Allows configuring session management.
@@ -88,6 +90,7 @@
8890
* </ul>
8991
*
9092
* @author Rob Winch
93+
* @author Onur Kagan Ozcan
9194
* @since 3.2
9295
* @see SessionManagementFilter
9396
* @see ConcurrentSessionFilter
@@ -505,21 +508,30 @@ public void configure(H http) throws Exception {
505508

506509
http.addFilter(sessionManagementFilter);
507510
if (isConcurrentSessionControlEnabled()) {
508-
ConcurrentSessionFilter concurrentSessionFilter = createConccurencyFilter(http);
511+
ConcurrentSessionFilter concurrentSessionFilter = createConcurrencyFilter(http);
509512

510513
concurrentSessionFilter = postProcess(concurrentSessionFilter);
511514
http.addFilter(concurrentSessionFilter);
512515
}
513516
}
514517

515-
private ConcurrentSessionFilter createConccurencyFilter(H http) {
518+
private ConcurrentSessionFilter createConcurrencyFilter(H http) {
516519
SessionInformationExpiredStrategy expireStrategy = getExpiredSessionStrategy();
517520
SessionRegistry sessionRegistry = getSessionRegistry(http);
521+
ConcurrentSessionFilter concurrentSessionFilter;
518522
if (expireStrategy == null) {
519-
return new ConcurrentSessionFilter(sessionRegistry);
523+
concurrentSessionFilter = new ConcurrentSessionFilter(sessionRegistry);
524+
} else {
525+
concurrentSessionFilter = new ConcurrentSessionFilter(sessionRegistry, expireStrategy);
526+
}
527+
LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
528+
if (logoutConfigurer != null) {
529+
List<LogoutHandler> logoutHandlers = logoutConfigurer.getLogoutHandlers();
530+
if (!CollectionUtils.isEmpty(logoutHandlers)) {
531+
concurrentSessionFilter.setLogoutHandlers(logoutHandlers);
532+
}
520533
}
521-
522-
return new ConcurrentSessionFilter(sessionRegistry, expireStrategy);
534+
return concurrentSessionFilter;
523535
}
524536

525537
/**

config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,13 +25,15 @@
2525
import org.springframework.beans.factory.xml.ParserContext;
2626
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler;
2727
import org.springframework.security.web.authentication.logout.LogoutFilter;
28+
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
2829
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
2930
import org.springframework.util.StringUtils;
3031
import org.w3c.dom.Element;
3132

3233
/**
3334
* @author Luke Taylor
3435
* @author Ben Alex
36+
* @author Onur Kagan Ozcan
3537
*/
3638
class LogoutBeanDefinitionParser implements BeanDefinitionParser {
3739
static final String ATT_LOGOUT_SUCCESS_URL = "logout-success-url";
@@ -120,6 +122,8 @@ public BeanDefinition parse(Element element, ParserContext pc) {
120122
logoutHandlers.add(cookieDeleter);
121123
}
122124

125+
logoutHandlers.add(new RootBeanDefinition(LogoutSuccessEventPublishingLogoutHandler.class));
126+
123127
builder.addConstructorArgValue(logoutHandlers);
124128

125129
return builder.getBeanDefinition();

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.security.config.annotation.web.configurers;
1818

19+
import java.util.List;
20+
1921
import org.junit.Rule;
2022
import org.junit.Test;
2123
import org.springframework.beans.factory.annotation.Autowired;
@@ -33,12 +35,19 @@
3335
import org.springframework.security.core.AuthenticationException;
3436
import org.springframework.security.core.authority.AuthorityUtils;
3537
import org.springframework.security.core.userdetails.PasswordEncodedUser;
38+
import org.springframework.security.util.FieldUtils;
3639
import org.springframework.security.web.AuthenticationEntryPoint;
40+
import org.springframework.security.web.FilterChainProxy;
41+
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
42+
import org.springframework.security.web.authentication.logout.LogoutFilter;
43+
import org.springframework.security.web.authentication.logout.LogoutHandler;
44+
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
3745
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
3846
import org.springframework.test.web.servlet.MockMvc;
3947
import org.springframework.web.bind.annotation.GetMapping;
4048
import org.springframework.web.bind.annotation.RestController;
4149

50+
import javax.servlet.Filter;
4251
import javax.servlet.http.HttpServletRequest;
4352
import javax.servlet.http.HttpServletResponse;
4453

@@ -59,6 +68,7 @@
5968
*
6069
* @author Rob Winch
6170
* @author Eleftheria Stein
71+
* @author Onur Kagan Ozcan
6272
*/
6373
public class ServletApiConfigurerTests {
6474
@Rule
@@ -287,4 +297,56 @@ public void admin(HttpServletRequest request) {
287297
}
288298
}
289299
}
300+
301+
@Test
302+
public void checkSecurityContextAwareAndLogoutFilterHasSameSizeAndHasLogoutSuccessEventPublishingLogoutHandler() {
303+
this.spring.register(ServletApiWithLogoutConfig.class);
304+
305+
SecurityContextHolderAwareRequestFilter scaFilter = getFilter(SecurityContextHolderAwareRequestFilter.class);
306+
LogoutFilter logoutFilter = getFilter(LogoutFilter.class);
307+
308+
LogoutHandler lfLogoutHandler = getFieldValue(logoutFilter, "handler");
309+
assertThat(lfLogoutHandler).isInstanceOf(CompositeLogoutHandler.class);
310+
311+
List<LogoutHandler> scaLogoutHandlers = getFieldValue(scaFilter, "logoutHandlers");
312+
List<LogoutHandler> lfLogoutHandlers = getFieldValue(lfLogoutHandler, "logoutHandlers");
313+
314+
assertThat(scaLogoutHandlers).hasSameSizeAs(lfLogoutHandlers);
315+
316+
assertThat(scaLogoutHandlers).hasAtLeastOneElementOfType(LogoutSuccessEventPublishingLogoutHandler.class);
317+
assertThat(lfLogoutHandlers).hasAtLeastOneElementOfType(LogoutSuccessEventPublishingLogoutHandler.class);
318+
}
319+
320+
@EnableWebSecurity
321+
static class ServletApiWithLogoutConfig extends WebSecurityConfigurerAdapter {
322+
@Override
323+
protected void configure(HttpSecurity http) throws Exception {
324+
// @formatter:off
325+
http
326+
.servletApi().and()
327+
.logout();
328+
// @formatter:on
329+
}
330+
}
331+
332+
private <T extends Filter> T getFilter(Class<T> filterClass) {
333+
return (T) getFilters().stream()
334+
.filter(filterClass::isInstance)
335+
.findFirst()
336+
.orElse(null);
337+
}
338+
339+
private List<Filter> getFilters() {
340+
FilterChainProxy proxy = this.spring.getContext().getBean(FilterChainProxy.class);
341+
return proxy.getFilters("/");
342+
}
343+
344+
private <T> T getFieldValue(Object target, String fieldName) {
345+
try {
346+
return (T) FieldUtils.getFieldValue(target, fieldName);
347+
} catch (Exception e) {
348+
throw new RuntimeException(e);
349+
}
350+
}
351+
290352
}

config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@
4141
import org.springframework.security.web.FilterChainProxy;
4242
import org.springframework.security.web.authentication.RememberMeServices;
4343
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
44+
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
45+
import org.springframework.security.web.authentication.logout.LogoutFilter;
4446
import org.springframework.security.web.authentication.logout.LogoutHandler;
47+
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
4548
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
4649
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
4750
import org.springframework.security.web.session.ConcurrentSessionFilter;
@@ -71,6 +74,7 @@
7174
* @author Luke Taylor
7275
* @author Rob Winch
7376
* @author Josh Cummings
77+
* @author Onur Kagan Ozcan
7478
*/
7579
public class SessionManagementConfigTests {
7680
private static final String CONFIG_LOCATION_PREFIX =
@@ -455,6 +459,32 @@ public void requestWhenSessionFixationProtectionIsNoneAndInvalidSessionUrlIsSetT
455459
.andExpect(redirectedUrl("/timeoutUrl"));
456460
}
457461

462+
/**
463+
* SEC-2680
464+
*/
465+
@Test
466+
public void checkConcurrencyAndLogoutFilterHasSameSizeAndHasLogoutSuccessEventPublishingLogoutHandler() {
467+
468+
this.spring.configLocations(this.xml("ConcurrencyControlLogoutAndRememberMeHandlers")).autowire();
469+
470+
ConcurrentSessionFilter concurrentSessionFilter = getFilter(ConcurrentSessionFilter.class);
471+
LogoutFilter logoutFilter = getFilter(LogoutFilter.class);
472+
473+
LogoutHandler csfLogoutHandler = getFieldValue(concurrentSessionFilter, "handlers");
474+
LogoutHandler lfLogoutHandler = getFieldValue(logoutFilter, "handler");
475+
476+
assertThat(csfLogoutHandler).isInstanceOf(CompositeLogoutHandler.class);
477+
assertThat(lfLogoutHandler).isInstanceOf(CompositeLogoutHandler.class);
478+
479+
List<LogoutHandler> csfLogoutHandlers = getFieldValue(csfLogoutHandler, "logoutHandlers");
480+
List<LogoutHandler> lfLogoutHandlers = getFieldValue(lfLogoutHandler, "logoutHandlers");
481+
482+
assertThat(csfLogoutHandlers).hasSameSizeAs(lfLogoutHandlers);
483+
484+
assertThat(csfLogoutHandlers).hasAtLeastOneElementOfType(LogoutSuccessEventPublishingLogoutHandler.class);
485+
assertThat(lfLogoutHandlers).hasAtLeastOneElementOfType(LogoutSuccessEventPublishingLogoutHandler.class);
486+
}
487+
458488
static class TeapotSessionAuthenticationStrategy implements SessionAuthenticationStrategy {
459489

460490
@Override
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.authentication.event;
18+
19+
import org.springframework.security.core.Authentication;
20+
21+
/**
22+
* Application event which indicates successful logout
23+
*
24+
* @author Onur Kagan Ozcan
25+
* @since 5.2.0
26+
*/
27+
public class LogoutSuccessEvent extends AbstractAuthenticationEvent {
28+
29+
public LogoutSuccessEvent(Authentication authentication) {
30+
super(authentication);
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.authentication.logout;
18+
19+
import javax.servlet.http.HttpServletRequest;
20+
import javax.servlet.http.HttpServletResponse;
21+
import org.springframework.context.ApplicationEventPublisher;
22+
import org.springframework.context.ApplicationEventPublisherAware;
23+
import org.springframework.security.authentication.event.LogoutSuccessEvent;
24+
import org.springframework.security.core.Authentication;
25+
26+
/**
27+
* A logout handler which publishes {@link LogoutSuccessEvent}
28+
*
29+
* @author Onur Kagan Ozcan
30+
* @since 5.2.0
31+
*/
32+
public final class LogoutSuccessEventPublishingLogoutHandler implements LogoutHandler, ApplicationEventPublisherAware {
33+
34+
private ApplicationEventPublisher eventPublisher;
35+
36+
@Override
37+
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
38+
if (eventPublisher == null) {
39+
return;
40+
}
41+
if (authentication == null) {
42+
return;
43+
}
44+
eventPublisher.publishEvent(new LogoutSuccessEvent(authentication));
45+
}
46+
47+
@Override
48+
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
49+
this.eventPublisher = applicationEventPublisher;
50+
}
51+
52+
}

0 commit comments

Comments
 (0)