diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index 0a9a50ff85..62d1f99f4c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -40,6 +40,7 @@ import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.context.DelegatingApplicationListener; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; @@ -72,6 +73,11 @@ public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAwa private ClassLoader beanClassLoader; + @Bean + public DelegatingApplicationListener delegatingApplicationListener() { + return new DelegatingApplicationListener(); + } + @Bean @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public SecurityExpressionHandler webSecurityExpressionHandler() { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index 78d2058d23..7098eb2d79 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -22,10 +22,15 @@ import java.util.List; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.GenericApplicationListenerAdapter; +import org.springframework.context.event.SmartApplicationListener; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.context.DelegatingApplicationListener; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; @@ -87,7 +92,7 @@ public final class SessionManagementConfigurer> private SessionAuthenticationStrategy sessionAuthenticationStrategy; private InvalidSessionStrategy invalidSessionStrategy; private List sessionAuthenticationStrategies = new ArrayList(); - private SessionRegistry sessionRegistry = new SessionRegistryImpl(); + private SessionRegistry sessionRegistry; private Integer maximumSessions; private String expiredUrl; private boolean maxSessionsPreventsLogin; @@ -367,14 +372,14 @@ public final class SessionManagementConfigurer> http.setSharedObject(RequestCache.class, new NullRequestCache()); } } - http.setSharedObject(SessionAuthenticationStrategy.class, getSessionAuthenticationStrategy()); + http.setSharedObject(SessionAuthenticationStrategy.class, getSessionAuthenticationStrategy(http)); http.setSharedObject(InvalidSessionStrategy.class, getInvalidSessionStrategy()); } @Override public void configure(H http) throws Exception { SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); - SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(securityContextRepository, getSessionAuthenticationStrategy()); + SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(securityContextRepository, getSessionAuthenticationStrategy(http)); if(sessionAuthenticationErrorUrl != null) { sessionManagementFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(sessionAuthenticationErrorUrl)); } @@ -389,7 +394,7 @@ public final class SessionManagementConfigurer> http.addFilter(sessionManagementFilter); if(isConcurrentSessionControlEnabled()) { - ConcurrentSessionFilter concurrentSessionFilter = new ConcurrentSessionFilter(sessionRegistry, expiredUrl); + ConcurrentSessionFilter concurrentSessionFilter = new ConcurrentSessionFilter(getSessionRegistry(http), expiredUrl); concurrentSessionFilter = postProcess(concurrentSessionFilter); http.addFilter(concurrentSessionFilter); } @@ -444,12 +449,13 @@ public final class SessionManagementConfigurer> * * @return the {@link SessionAuthenticationStrategy} to use */ - private SessionAuthenticationStrategy getSessionAuthenticationStrategy() { + private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) { if(sessionAuthenticationStrategy != null) { return sessionAuthenticationStrategy; } List delegateStrategies = sessionAuthenticationStrategies; if(isConcurrentSessionControlEnabled()) { + SessionRegistry sessionRegistry = getSessionRegistry(http); ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry); concurrentSessionControlStrategy.setMaximumSessions(maximumSessions); concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(maxSessionsPreventsLogin); @@ -466,6 +472,28 @@ public final class SessionManagementConfigurer> return sessionAuthenticationStrategy; } + private SessionRegistry getSessionRegistry(H http) { + if(sessionRegistry == null) { + SessionRegistryImpl sessionRegistry = new SessionRegistryImpl(); + registerDelegateApplicationListener(http, sessionRegistry); + this.sessionRegistry = sessionRegistry; + } + return sessionRegistry; + } + + private void registerDelegateApplicationListener(H http, ApplicationListener delegate) { + ApplicationContext context = http.getSharedObject(ApplicationContext.class); + if(context == null) { + return; + } + if(context.getBeansOfType(DelegatingApplicationListener.class).isEmpty()) { + return; + } + DelegatingApplicationListener delegating = context.getBean(DelegatingApplicationListener.class); + SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate); + delegating.addListener(smartListener); + } + /** * Returns true if the number of concurrent sessions per user should be restricted. * @return diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.groovy index 20fe5b8234..1d5d0b5ff4 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.groovy @@ -29,6 +29,7 @@ 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.configuration.WebSecurityConfigurerAdapter import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.core.session.SessionDestroyedEvent import org.springframework.security.web.access.ExceptionTranslationFilter import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy @@ -38,6 +39,7 @@ import org.springframework.security.web.context.SecurityContextPersistenceFilter import org.springframework.security.web.context.SecurityContextRepository import org.springframework.security.web.savedrequest.RequestCache import org.springframework.security.web.session.ConcurrentSessionFilter +import org.springframework.security.web.session.HttpSessionDestroyedEvent; import org.springframework.security.web.session.SessionManagementFilter /** @@ -154,12 +156,14 @@ class SessionManagementConfigurerTests extends BaseSpringSpec { def 'session fixation and enable concurrency control'() { setup: "context where session fixation is disabled and concurrency control is enabled" loadConfig(ConcurrencyControlConfig) + def authenticatedSession when: "authenticate successfully" request.servletPath = "/login" request.method = "POST" request.setParameter("username", "user"); request.setParameter("password","password") springSecurityFilterChain.doFilter(request, response, chain) + authenticatedSession = request.session then: "authentication is sucessful" response.status == HttpServletResponse.SC_MOVED_TEMPORARILY response.redirectedUrl == "/" @@ -173,6 +177,17 @@ class SessionManagementConfigurerTests extends BaseSpringSpec { then: response.status == HttpServletResponse.SC_MOVED_TEMPORARILY response.redirectedUrl == '/login?error' + when: 'SEC-2574: When Session Expires and authentication attempted' + context.publishEvent(new HttpSessionDestroyedEvent(authenticatedSession)) + super.setup() + request.servletPath = "/login" + request.method = "POST" + request.setParameter("username", "user"); + request.setParameter("password","password") + springSecurityFilterChain.doFilter(request, response, chain) + then: "authentication is successful" + response.status == HttpServletResponse.SC_MOVED_TEMPORARILY + response.redirectedUrl == "/" } @EnableWebSecurity diff --git a/core/src/main/java/org/springframework/security/context/DelegatingApplicationListener.java b/core/src/main/java/org/springframework/security/context/DelegatingApplicationListener.java new file mode 100644 index 0000000000..517808f4d2 --- /dev/null +++ b/core/src/main/java/org/springframework/security/context/DelegatingApplicationListener.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.context; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.SmartApplicationListener; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.List; + +/** + * Used for delegating to a number of SmartApplicationListener instances. This is useful when needing to register an + * SmartApplicationListener with the ApplicationContext programmatically. + * + * @author Rob Winch + */ +public final class DelegatingApplicationListener implements ApplicationListener { + private List listeners = new ArrayList(); + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if(event == null) { + return; + } + for(SmartApplicationListener listener : listeners) { + Object source = event.getSource(); + if(source != null && listener.supportsEventType(event.getClass()) && listener.supportsSourceType(source.getClass())) { + listener.onApplicationEvent(event); + } + } + } + + /** + * Adds a new SmartApplicationListener to use. + * + * @param smartApplicationListener the SmartApplicationListener to use. Cannot be null. + */ + public void addListener(SmartApplicationListener smartApplicationListener) { + Assert.notNull(smartApplicationListener, "smartApplicationListener cannot be null"); + listeners.add(smartApplicationListener); + } +} diff --git a/core/src/test/java/org/springframework/security/context/DelegatingApplicationListenerTests.java b/core/src/test/java/org/springframework/security/context/DelegatingApplicationListenerTests.java new file mode 100644 index 0000000000..df26e9314a --- /dev/null +++ b/core/src/test/java/org/springframework/security/context/DelegatingApplicationListenerTests.java @@ -0,0 +1,72 @@ +package org.springframework.security.context; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.SmartApplicationListener; +import org.springframework.security.core.session.SessionDestroyedEvent; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DelegatingApplicationListenerTests { + @Mock + SmartApplicationListener delegate; + + ApplicationEvent event; + + DelegatingApplicationListener listener; + + @Before + public void setup() { + event = new ApplicationEvent(this) {}; + listener = new DelegatingApplicationListener(); + listener.addListener(delegate); + } + + @Test + public void processEventNull() { + listener.onApplicationEvent(null); + + verify(delegate,never()).onApplicationEvent(any(ApplicationEvent.class)); + } + + @Test + public void processEventSuccess() { + when(delegate.supportsEventType(event.getClass())).thenReturn(true); + when(delegate.supportsSourceType(event.getSource().getClass())).thenReturn(true); + listener.onApplicationEvent(event); + + verify(delegate).onApplicationEvent(event); + } + + @Test + public void processEventEventTypeNotSupported() { + when(delegate.supportsSourceType(event.getSource().getClass())).thenReturn(true); + listener.onApplicationEvent(event); + + verify(delegate,never()).onApplicationEvent(any(ApplicationEvent.class)); + } + + @Test + public void processEventSourceTypeNotSupported() { + when(delegate.supportsEventType(event.getClass())).thenReturn(true); + listener.onApplicationEvent(event); + + verify(delegate,never()).onApplicationEvent(any(ApplicationEvent.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void addNull() { + listener.addListener(null); + } + +} \ No newline at end of file