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 f5f6955560..097e34dfed 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 @@ -15,7 +15,11 @@ */ package org.springframework.security.config.annotation.web.configurers; +import java.util.Arrays; +import java.util.List; + import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -23,7 +27,10 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; -import org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy; +import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy; +import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy; +import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy; +import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; @@ -70,7 +77,8 @@ import org.springframework.util.Assert; * @see ConcurrentSessionFilter */ public final class SessionManagementConfigurer> extends AbstractHttpConfigurer { - private SessionAuthenticationStrategy sessionAuthenticationStrategy = new SessionFixationProtectionStrategy(); + private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = new SessionFixationProtectionStrategy(); + private SessionAuthenticationStrategy sessionAuthenticationStrategy; private SessionRegistry sessionRegistry = new SessionRegistryImpl(); private Integer maximumSessions; private String expiredUrl; @@ -149,17 +157,25 @@ public final class SessionManagementConfigurer> /** * Allows explicitly specifying the {@link SessionAuthenticationStrategy}. * The default is to use {@link SessionFixationProtectionStrategy}. If - * restricting the maximum number of sessions is configured, - * {@link ConcurrentSessionControlStrategy} will be used. + * restricting the maximum number of sessions is configured, then + * {@link CompositeSessionAuthenticationStrategy} delegating to + * {@link ConcurrentSessionControlAuthenticationStrategy}, + * {@link SessionFixationProtectionStrategy} (optional), and + * {@link RegisterSessionAuthenticationStrategy} will be used. * * @param sessionAuthenticationStrategy - * @return the {@link SessionManagementConfigurer} for further customizations + * @return the {@link SessionManagementConfigurer} for further + * customizations */ public SessionManagementConfigurer sessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) { - this.sessionAuthenticationStrategy = sessionAuthenticationStrategy; + this.sessionFixationAuthenticationStrategy = sessionAuthenticationStrategy; return this; } + public SessionFixationConfigurer sessionFixation() { + return new SessionFixationConfigurer(); + } + /** * Controls the maximum number of sessions for a user. The default is to allow any number of users. * @param maximumSessions the maximum number of sessions for a user @@ -167,10 +183,57 @@ public final class SessionManagementConfigurer> */ public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) { this.maximumSessions = maximumSessions; - this.sessionAuthenticationStrategy = null; return new ConcurrencyControlConfigurer(); } + /** + * Allows configuring SessionFixation protection + * + * @author Rob Winch + */ + public final class SessionFixationConfigurer { + /** + * Specifies that a new session should be created, but the session + * attributes from the original {@link HttpSession} should not be + * retained. + * + * @return the {@link SessionManagementConfigurer} for further customizations + */ + public SessionManagementConfigurer newSession() { + SessionFixationProtectionStrategy sessionFixationProtectionStrategy = new SessionFixationProtectionStrategy(); + sessionFixationProtectionStrategy.setMigrateSessionAttributes(false); + SessionManagementConfigurer.this.sessionFixationAuthenticationStrategy = sessionFixationProtectionStrategy; + return SessionManagementConfigurer.this; + } + + /** + * Specifies that a new session should be created and the session + * attributes from the original {@link HttpSession} should be + * retained. + * + * @return the {@link SessionManagementConfigurer} for further customizations + */ + public SessionManagementConfigurer migrateSession() { + SessionManagementConfigurer.this.sessionFixationAuthenticationStrategy = new SessionFixationProtectionStrategy(); + return SessionManagementConfigurer.this; + } + + /** + * Specifies that no session fixation protection should be enabled. This + * may be useful when utilizing other mechanisms for protecting against + * session fixation. For example, if application container session + * fixation protection is already in use. Otherwise, this option is not + * recommended. + * + * @return the {@link SessionManagementConfigurer} for further + * customizations + */ + public SessionManagementConfigurer none() { + SessionManagementConfigurer.this.sessionFixationAuthenticationStrategy = new NullAuthenticatedSessionStrategy(); + return SessionManagementConfigurer.this; + } + } + /** * Allows configuring controlling of multiple sessions. * @@ -314,10 +377,18 @@ public final class SessionManagementConfigurer> return sessionAuthenticationStrategy; } if(isConcurrentSessionControlEnabled()) { - ConcurrentSessionControlStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlStrategy(sessionRegistry); + ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry); concurrentSessionControlStrategy.setMaximumSessions(maximumSessions); concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(maxSessionsPreventsLogin); - sessionAuthenticationStrategy = concurrentSessionControlStrategy; + concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy); + + RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(sessionRegistry); + registerSessionStrategy = postProcess(registerSessionStrategy); + + List delegateStrategies = Arrays.asList(concurrentSessionControlStrategy, sessionFixationAuthenticationStrategy, registerSessionStrategy); + sessionAuthenticationStrategy = postProcess(new CompositeSessionAuthenticationStrategy(delegateStrategies)); + } else { + sessionAuthenticationStrategy = sessionFixationAuthenticationStrategy; } return sessionAuthenticationStrategy; } diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index def9f38b57..6bfe96b928 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -18,14 +18,17 @@ package org.springframework.security.config.http; import static org.springframework.security.config.http.HttpSecurityBeanDefinitionParser.*; import static org.springframework.security.config.http.SecurityFilters.*; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.BeanReferenceFactoryBean; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; @@ -51,7 +54,10 @@ import org.springframework.security.web.access.expression.WebExpressionVoter; import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; -import org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy; +import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy; +import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy; +import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy; +import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.NullSecurityContextRepository; @@ -66,6 +72,7 @@ import org.springframework.security.web.session.ConcurrentSessionFilter; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; @@ -83,6 +90,7 @@ class HttpConfigurationBuilder { private static final String ATT_SESSION_FIXATION_PROTECTION = "session-fixation-protection"; private static final String OPT_SESSION_FIXATION_NO_PROTECTION = "none"; private static final String OPT_SESSION_FIXATION_MIGRATE_SESSION = "migrateSession"; + private static final String OPT_CHANGE_SESSION_ID = "changeSessionId"; private static final String ATT_INVALID_SESSION_URL = "invalid-session-url"; private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref"; @@ -247,6 +255,7 @@ class HttpConfigurationBuilder { String sessionAuthStratRef = null; String errorUrl = null; + boolean sessionControlEnabled = false; if (sessionMgmtElt != null) { if (sessionPolicy == SessionCreationPolicy.STATELESS) { pc.getReaderContext().error(Elements.SESSION_MANAGEMENT + " cannot be used" + @@ -258,8 +267,9 @@ class HttpConfigurationBuilder { sessionAuthStratRef = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_STRATEGY_REF); errorUrl = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_ERROR_URL); sessionCtrlElt = DomUtils.getChildElementByTagName(sessionMgmtElt, Elements.CONCURRENT_SESSIONS); + sessionControlEnabled = sessionCtrlElt != null; - if (sessionCtrlElt != null) { + if (sessionControlEnabled) { if (StringUtils.hasText(sessionAuthStratRef)) { pc.getReaderContext().error(ATT_SESSION_AUTH_STRATEGY_REF + " attribute cannot be used" + " in combination with <" + Elements.CONCURRENT_SESSIONS + ">", pc.extractSource(sessionCtrlElt)); @@ -269,7 +279,8 @@ class HttpConfigurationBuilder { } if (!StringUtils.hasText(sessionFixationAttribute)) { - sessionFixationAttribute = OPT_SESSION_FIXATION_MIGRATE_SESSION; + Method changeSessionIdMethod = ReflectionUtils.findMethod(HttpServletRequest.class, "changeSessionId"); + sessionFixationAttribute = changeSessionIdMethod == null ? OPT_SESSION_FIXATION_MIGRATE_SESSION : OPT_CHANGE_SESSION_ID; } else if (StringUtils.hasText(sessionAuthStratRef)) { pc.getReaderContext().error(ATT_SESSION_FIXATION_PROTECTION + " attribute cannot be used" + " in combination with " + ATT_SESSION_AUTH_STRATEGY_REF, pc.extractSource(sessionMgmtElt)); @@ -282,28 +293,50 @@ class HttpConfigurationBuilder { boolean sessionFixationProtectionRequired = !sessionFixationAttribute.equals(OPT_SESSION_FIXATION_NO_PROTECTION); - BeanDefinitionBuilder sessionStrategy; + ManagedList delegateSessionStrategies = new ManagedList(); + BeanDefinitionBuilder concurrentSessionStrategy; + BeanDefinitionBuilder sessionFixationStrategy = null; + BeanDefinitionBuilder registerSessionStrategy; - if (sessionCtrlElt != null) { + if (sessionControlEnabled) { assert sessionRegistryRef != null; - sessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(ConcurrentSessionControlStrategy.class); - sessionStrategy.addConstructorArgValue(sessionRegistryRef); + concurrentSessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(ConcurrentSessionControlAuthenticationStrategy.class); + concurrentSessionStrategy.addConstructorArgValue(sessionRegistryRef); String maxSessions = sessionCtrlElt.getAttribute("max-sessions"); if (StringUtils.hasText(maxSessions)) { - sessionStrategy.addPropertyValue("maximumSessions", maxSessions); + concurrentSessionStrategy.addPropertyValue("maximumSessions", maxSessions); } String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded"); if (StringUtils.hasText(exceptionIfMaximumExceeded)) { - sessionStrategy.addPropertyValue("exceptionIfMaximumExceeded", exceptionIfMaximumExceeded); + concurrentSessionStrategy.addPropertyValue("exceptionIfMaximumExceeded", exceptionIfMaximumExceeded); } - } else if (sessionFixationProtectionRequired || StringUtils.hasText(invalidSessionUrl) - || StringUtils.hasText(sessionAuthStratRef)) { - sessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(SessionFixationProtectionStrategy.class); - } else { + delegateSessionStrategies.add(concurrentSessionStrategy.getBeanDefinition()); + } + boolean useChangeSessionId = OPT_CHANGE_SESSION_ID.equals(sessionFixationAttribute); + if (sessionFixationProtectionRequired || StringUtils.hasText(invalidSessionUrl)) { + if(useChangeSessionId) { + sessionFixationStrategy = BeanDefinitionBuilder.rootBeanDefinition(ChangeSessionIdAuthenticationStrategy.class); + } else { + sessionFixationStrategy = BeanDefinitionBuilder.rootBeanDefinition(SessionFixationProtectionStrategy.class); + } + delegateSessionStrategies.add(sessionFixationStrategy.getBeanDefinition()); + } + + if(StringUtils.hasText(sessionAuthStratRef)) { + delegateSessionStrategies.add(new RuntimeBeanReference(sessionAuthStratRef)); + } + + if(sessionControlEnabled) { + registerSessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(RegisterSessionAuthenticationStrategy.class); + registerSessionStrategy.addConstructorArgValue(sessionRegistryRef); + delegateSessionStrategies.add(registerSessionStrategy.getBeanDefinition()); + } + + if(delegateSessionStrategies.isEmpty()) { sfpf = null; return; } @@ -316,15 +349,21 @@ class HttpConfigurationBuilder { sessionMgmtFilter.addPropertyValue("authenticationFailureHandler", failureHandler); sessionMgmtFilter.addConstructorArgValue(contextRepoRef); - if (!StringUtils.hasText(sessionAuthStratRef)) { - BeanDefinition strategyBean = sessionStrategy.getBeanDefinition(); + if (!StringUtils.hasText(sessionAuthStratRef) && sessionFixationStrategy != null && !useChangeSessionId ) { if (sessionFixationProtectionRequired) { - sessionStrategy.addPropertyValue("migrateSessionAttributes", + sessionFixationStrategy.addPropertyValue("migrateSessionAttributes", Boolean.valueOf(sessionFixationAttribute.equals(OPT_SESSION_FIXATION_MIGRATE_SESSION))); } + } + + if(!delegateSessionStrategies.isEmpty()) { + BeanDefinitionBuilder sessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(CompositeSessionAuthenticationStrategy.class); + BeanDefinition strategyBean = sessionStrategy.getBeanDefinition(); + sessionStrategy.addConstructorArgValue(delegateSessionStrategies); sessionAuthStratRef = pc.getReaderContext().generateBeanName(strategyBean); pc.registerBeanComponent(new BeanComponentDefinition(strategyBean, sessionAuthStratRef)); + } if (StringUtils.hasText(invalidSessionUrl)) { diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy index e6e95a5af6..08234aa4fd 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy @@ -58,12 +58,13 @@ class NamespaceSessionManagementTests extends BaseSpringSpec { CustomSessionManagementConfig.SR = Mock(SessionRegistry) when: loadConfig(CustomSessionManagementConfig) + def concurrentStrategy = findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies[0] then: findFilter(SessionManagementFilter).invalidSessionStrategy.destinationUrl == "/invalid-session" findFilter(SessionManagementFilter).failureHandler.defaultFailureUrl == "/session-auth-error" - findFilter(SessionManagementFilter).sessionAuthenticationStrategy.maximumSessions == 1 - findFilter(SessionManagementFilter).sessionAuthenticationStrategy.exceptionIfMaximumExceeded - findFilter(SessionManagementFilter).sessionAuthenticationStrategy.sessionRegistry == CustomSessionManagementConfig.SR + concurrentStrategy.maximumSessions == 1 + concurrentStrategy.exceptionIfMaximumExceeded + concurrentStrategy.sessionRegistry == CustomSessionManagementConfig.SR findFilter(ConcurrentSessionFilter).expiredUrl == "/expired-session" } @@ -154,7 +155,8 @@ class NamespaceSessionManagementTests extends BaseSpringSpec { protected void configure(HttpSecurity http) throws Exception { http .sessionManagement() - .sessionAuthenticationStrategy(new SessionFixationProtectionStrategy(migrateSessionAttributes : false)) + .sessionFixation() + .newSession() } } } 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 b3dd35904a..9cb529ef5e 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 @@ -15,16 +15,24 @@ */ package org.springframework.security.config.annotation.web.configurers +import javax.servlet.http.HttpServletResponse + import org.springframework.context.annotation.Configuration +import org.springframework.mock.web.MockFilterChain +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse import org.springframework.security.config.annotation.AnyObjectPostProcessor import org.springframework.security.config.annotation.BaseSpringSpec -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -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.config.annotation.authentication.builders.AuthenticationManagerBuilder +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.web.access.ExceptionTranslationFilter -import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy +import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy +import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy +import org.springframework.security.web.context.NullSecurityContextRepository import org.springframework.security.web.context.SecurityContextPersistenceFilter import org.springframework.security.web.context.SecurityContextRepository import org.springframework.security.web.savedrequest.RequestCache @@ -110,6 +118,82 @@ class SessionManagementConfigurerTests extends BaseSpringSpec { } + def 'SEC-2137: disable session fixation and enable concurrency control'() { + setup: "context where session fixation is disabled and concurrency control is enabled" + loadConfig(DisableSessionFixationEnableConcurrencyControlConfig) + String originalSessionId = request.session.id + String credentials = "user:password" + request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) + when: "authenticate" + springSecurityFilterChain.doFilter(request, response, new MockFilterChain()) + then: "session invalidate is not called" + request.session.id == originalSessionId + } + + @EnableWebSecurity + @Configuration + static class DisableSessionFixationEnableConcurrencyControlConfig extends WebSecurityConfigurerAdapter { + @Override + public void configure(HttpSecurity http) { + http + .httpBasic() + .and() + .sessionManagement() + .sessionFixation().none() + .maximumSessions(1) + } + @Override + public void registerAuthentication(AuthenticationManagerBuilder auth) { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + } + + def 'session fixation and enable concurrency control'() { + setup: "context where session fixation is disabled and concurrency control is enabled" + loadConfig(ConcurrencyControlConfig) + when: "authenticate successfully" + request.servletPath = "/login" + request.method = "POST" + request.setParameter("username", "user"); + request.setParameter("password","password") + springSecurityFilterChain.doFilter(request, response, chain) + then: "authentication is sucessful" + response.status == HttpServletResponse.SC_MOVED_TEMPORARILY + response.redirectedUrl == "/" + when: "authenticate with the same user" + super.setup() + request.servletPath = "/login" + request.method = "POST" + request.setParameter("username", "user"); + request.setParameter("password","password") + springSecurityFilterChain.doFilter(request, response, chain) + then: + response.status == HttpServletResponse.SC_MOVED_TEMPORARILY + response.redirectedUrl == '/login?error' + } + + @EnableWebSecurity + @Configuration + static class ConcurrencyControlConfig extends WebSecurityConfigurerAdapter { + @Override + public void configure(HttpSecurity http) { + http + .formLogin() + .and() + .sessionManagement() + .maximumSessions(1) + .maxSessionsPreventsLogin(true) + } + @Override + public void registerAuthentication(AuthenticationManagerBuilder auth) { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + } + def "sessionManagement ObjectPostProcessor"() { setup: AnyObjectPostProcessor opp = Mock() @@ -122,9 +206,15 @@ class SessionManagementConfigurerTests extends BaseSpringSpec { .and() .build() - then: "SessionManagementFilter is registered with LifecycleManager" + then: "SessionManagementFilter is registered with ObjectPostProcessor" 1 * opp.postProcess(_ as SessionManagementFilter) >> {SessionManagementFilter o -> o} - and: "ConcurrentSessionFilter is registered with LifecycleManager" + and: "ConcurrentSessionFilter is registered with ObjectPostProcessor" 1 * opp.postProcess(_ as ConcurrentSessionFilter) >> {ConcurrentSessionFilter o -> o} + and: "ConcurrentSessionControlAuthenticationStrategy is registered with ObjectPostProcessor" + 1 * opp.postProcess(_ as ConcurrentSessionControlAuthenticationStrategy) >> {ConcurrentSessionControlAuthenticationStrategy o -> o} + and: "CompositeSessionAuthenticationStrategy is registered with ObjectPostProcessor" + 1 * opp.postProcess(_ as CompositeSessionAuthenticationStrategy) >> {CompositeSessionAuthenticationStrategy o -> o} + and: "RegisterSessionAuthenticationStrategy is registered with ObjectPostProcessor" + 1 * opp.postProcess(_ as RegisterSessionAuthenticationStrategy) >> {RegisterSessionAuthenticationStrategy o -> o} } } diff --git a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy index c4c27a2358..8731384229 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -15,28 +15,40 @@ */ package org.springframework.security.config.http +import static org.junit.Assert.assertSame +import static org.mockito.Mockito.* + +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import org.mockito.Mockito import org.springframework.mock.web.MockFilterChain import org.springframework.mock.web.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletResponse import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.AuthorityUtils import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.session.SessionRegistry import org.springframework.security.core.session.SessionRegistryImpl +import org.springframework.security.core.userdetails.User import org.springframework.security.util.FieldUtils +import org.springframework.security.web.FilterChainProxy import org.springframework.security.web.authentication.RememberMeServices import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler import org.springframework.security.web.authentication.logout.LogoutFilter import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter -import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy +import org.springframework.security.web.authentication.session.SessionAuthenticationException +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy import org.springframework.security.web.context.NullSecurityContextRepository import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper import org.springframework.security.web.context.SecurityContextPersistenceFilter import org.springframework.security.web.savedrequest.RequestCacheAwareFilter import org.springframework.security.web.session.ConcurrentSessionFilter import org.springframework.security.web.session.SessionManagementFilter -import static org.junit.Assert.assertSame /** * Tests session-related functionality for the <http> namespace element and <session-management> @@ -93,6 +105,46 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { filter.repo.allowSessionCreation } + def 'SEC-1208: Session is not created when rejecting user due to max sessions exceeded'() { + setup: + httpCreateSession('never') { + 'session-management'() { + 'concurrency-control'('max-sessions':1,'error-if-maximum-exceeded':'true') + } + } + createAppContext() + SessionRegistry registry = appContext.getBean(SessionRegistry) + registry.registerNewSession("1", new User("user","password",AuthorityUtils.createAuthorityList("ROLE_USER"))) + MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletResponse response = new MockHttpServletResponse() + String credentials = "user:password" + request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) + when: "exceed max authentication attempts" + appContext.getBean(FilterChainProxy).doFilter(request, response, new MockFilterChain()) + then: "no new session is created" + request.getSession(false) == null + response.status == HttpServletResponse.SC_UNAUTHORIZED + } + + def 'SEC-2137: disable session fixation and enable concurrency control'() { + setup: "context where session fixation is disabled and concurrency control is enabled" + httpAutoConfig { + 'session-management'('session-fixation-protection':'none') { + 'concurrency-control'('max-sessions':'1','error-if-maximum-exceeded':'true') + } + } + createAppContext() + MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletResponse response = new MockHttpServletResponse() + String originalSessionId = request.session.id + String credentials = "user:password" + request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) + when: "authenticate" + appContext.getBean(FilterChainProxy).doFilter(request, response, new MockFilterChain()) + then: "session invalidate is not called" + request.session.id == originalSessionId + } + def httpCreateSession(String create, Closure c) { xml.http(['auto-config': 'true', 'create-session': create], c) } @@ -219,15 +271,28 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { } def externalSessionStrategyIsSupported() { - when: - httpAutoConfig { - 'session-management'('session-authentication-strategy-ref':'ss') - } - bean('ss', SessionFixationProtectionStrategy.class.name) - createAppContext(); + setup: + httpAutoConfig { + 'session-management'('session-authentication-strategy-ref':'ss') + } + xml.'b:bean'(id: 'ss', 'class': Mockito.class.name, 'factory-method':'mock') { + 'b:constructor-arg'(value : SessionAuthenticationStrategy.class.name) + } + createAppContext() - then: - notThrown(Exception.class) + MockHttpServletRequest request = new MockHttpServletRequest(); + request.getSession(); + request.setRequestURI("/j_spring_security_check"); + request.setMethod("POST"); + request.setParameter("j_username", "user"); + request.setParameter("j_password", "password"); + + SessionAuthenticationStrategy sessionAuthStrategy = appContext.getBean('ss',SessionAuthenticationStrategy) + FilterChainProxy springSecurityFilterChain = appContext.getBean(FilterChainProxy) + when: + springSecurityFilterChain.doFilter(request,new MockHttpServletResponse(), new MockFilterChain()) + then: "CustomSessionAuthenticationStrategy has seen the request (although REQUEST is a wrapped request)" + verify(sessionAuthStrategy).onAuthentication(any(Authentication), any(HttpServletRequest), any(HttpServletResponse)) } def externalSessionRegistryBeanIsConfiguredCorrectly() { @@ -247,10 +312,8 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { Object sessionRegistry = appContext.getBean("sr"); Object sessionRegistryFromConcurrencyFilter = FieldUtils.getFieldValue( getFilter(ConcurrentSessionFilter.class), "sessionRegistry"); - Object sessionRegistryFromFormLoginFilter = FieldUtils.getFieldValue( - getFilter(UsernamePasswordAuthenticationFilter.class),"sessionStrategy.sessionRegistry"); - Object sessionRegistryFromMgmtFilter = FieldUtils.getFieldValue( - getFilter(SessionManagementFilter.class),"sessionAuthenticationStrategy.sessionRegistry"); + Object sessionRegistryFromFormLoginFilter = FieldUtils.getFieldValue(getFilter(UsernamePasswordAuthenticationFilter),"sessionStrategy").delegateStrategies[0].sessionRegistry + Object sessionRegistryFromMgmtFilter = FieldUtils.getFieldValue(getFilter(SessionManagementFilter),"sessionAuthenticationStrategy").delegateStrategies[0].sessionRegistry assertSame(sessionRegistry, sessionRegistryFromConcurrencyFilter); assertSame(sessionRegistry, sessionRegistryFromMgmtFilter); @@ -297,7 +360,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { createAppContext() expect: - !(getFilters("/someurl")[8] instanceof SessionManagementFilter) + !(getFilters("/someurl").find { it instanceof SessionManagementFilter}) } def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() { diff --git a/docs/manual/src/docbook/appendix-namespace.xml b/docs/manual/src/docbook/appendix-namespace.xml index 6804e0b43b..7984c60d5b 100644 --- a/docs/manual/src/docbook/appendix-namespace.xml +++ b/docs/manual/src/docbook/appendix-namespace.xml @@ -1208,7 +1208,7 @@ Adds support for concurrent session control, allowing limits to be placed on the number of active sessions a user can have. A ConcurrentSessionFilter will be created, and a - ConcurrentSessionControlStrategy will be used with the + ConcurrentSessionControlAuthenticationStrategy will be used with the SessionManagementFilter. If a form-login element has been declared, the strategy object will also be injected into the created authentication filter. An instance of @@ -1242,7 +1242,7 @@
<literal>max-sessions</literal> Maps to the maximumSessions property of - ConcurrentSessionControlStrategy. + ConcurrentSessionControlAuthenticationStrategy.
<literal>session-registry-alias</literal> diff --git a/docs/manual/src/docbook/session-mgmt.xml b/docs/manual/src/docbook/session-mgmt.xml index 785f955dbe..00b2412617 100644 --- a/docs/manual/src/docbook/session-mgmt.xml +++ b/docs/manual/src/docbook/session-mgmt.xml @@ -80,7 +80,7 @@ though. The implementation uses a specialized version of SessionAuthenticationStrategy, called - ConcurrentSessionControlStrategy. + ConcurrentSessionControlAuthenticationStrategy. Previously the concurrent authentication check was made by the ProviderManager, which could be injected with a ConcurrentSessionController. The latter would check if the user @@ -126,10 +126,21 @@ - - - + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + diff --git a/web/src/main/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategy.java b/web/src/main/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategy.java new file mode 100644 index 0000000000..21769f7195 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategy.java @@ -0,0 +1,91 @@ +/* +R * Copyright 2002-2013 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.web.authentication.session; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * A {@link SessionAuthenticationStrategy} that accepts multiple + * {@link SessionAuthenticationStrategy} implementations to delegate to. Each + * {@link SessionAuthenticationStrategy} is invoked in turn. The invocations are + * short circuited if any exception, (i.e. SessionAuthenticationException) is + * thrown. + * + *

+ * Typical usage would include having the following delegates (in this order) + *

+ * + *
    + *
  • {@link ConcurrentSessionControlAuthenticationStrategy} - verifies that a + * user is allowed to authenticate (i.e. they have not already logged into the + * application.
  • + *
  • {@link SessionFixationProtectionStrategy} - If session fixation is + * desired, {@link SessionFixationProtectionStrategy} should be after + * {@link ConcurrentSessionControlAuthenticationStrategy} to prevent unnecessary + * {@link HttpSession} creation if the + * {@link ConcurrentSessionControlAuthenticationStrategy} rejects + * authentication.
  • + *
  • {@link RegisterSessionAuthenticationStrategy} - It is important this is + * after {@link SessionFixationProtectionStrategy} so that the correct session + * is registered.
  • + *
+ * + * @author Rob Winch + * @since 3.2 + */ +public class CompositeSessionAuthenticationStrategy implements SessionAuthenticationStrategy { + private final Log logger = LogFactory.getLog(getClass()); + private final List delegateStrategies; + + public CompositeSessionAuthenticationStrategy(List delegateStrategies) { + Assert.notEmpty(delegateStrategies, "delegateStrategies cannot be null or empty"); + for(SessionAuthenticationStrategy strategy : delegateStrategies) { + if(strategy == null) { + throw new IllegalArgumentException("delegateStrategies cannot contain null entires. Got " + delegateStrategies); + } + } + this.delegateStrategies = new ArrayList(delegateStrategies); + } + + /* (non-Javadoc) + * @see org.springframework.security.web.authentication.session.SessionAuthenticationStrategy#onAuthentication(org.springframework.security.core.Authentication, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + public void onAuthentication(Authentication authentication, + HttpServletRequest request, HttpServletResponse response) + throws SessionAuthenticationException { + for(SessionAuthenticationStrategy delegate : delegateStrategies) { + if(logger.isDebugEnabled()) { + logger.debug("Delegating to " + delegate); + } + delegate.onAuthentication(authentication, request, response); + } + } + + @Override + public String toString() { + return getClass().getName() + " [delegateStrategies = " + delegateStrategies + "]"; + } +} \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java b/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java new file mode 100644 index 0000000000..6314d69f09 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java @@ -0,0 +1,183 @@ +package org.springframework.security.web.authentication.session; + +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.SpringSecurityMessageSource; +import org.springframework.security.core.session.SessionInformation; +import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.SessionManagementFilter; +import org.springframework.util.Assert; + +/** + * Strategy which handles concurrent session-control. + * + *

+ * When invoked following an authentication, it will check whether the user in + * question should be allowed to proceed, by comparing the number of sessions + * they already have active with the configured maximumSessions value. + * The {@link SessionRegistry} is used as the source of data on authenticated + * users and session data. + *

+ *

+ * If a user has reached the maximum number of permitted sessions, the behaviour + * depends on the exceptionIfMaxExceeded property. The default + * behaviour is to expired the least recently used session, which will be + * invalidated by the {@link ConcurrentSessionFilter} if accessed again. If + * exceptionIfMaxExceeded is set to true, however, the user + * will be prevented from starting a new authenticated session. + *

+ *

+ * This strategy can be injected into both the {@link SessionManagementFilter} + * and instances of {@link AbstractAuthenticationProcessingFilter} (typically + * {@link UsernamePasswordAuthenticationFilter}), but is typically combined with + * {@link RegisterSessionAuthenticationStrategy} using + * {@link CompositeSessionAuthenticationStrategy}. + *

+ * + * @see CompositeSessionAuthenticationStrategy + * + * @author Luke Taylor + * @author Rob Winch + * @since 3.2 + */ +public class ConcurrentSessionControlAuthenticationStrategy implements MessageSourceAware, SessionAuthenticationStrategy { + protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); + private final SessionRegistry sessionRegistry; + private boolean exceptionIfMaximumExceeded = false; + private int maximumSessions = 1; + + /** + * @param sessionRegistry the session registry which should be updated when the authenticated session is changed. + */ + public ConcurrentSessionControlAuthenticationStrategy(SessionRegistry sessionRegistry) { + Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null"); + this.sessionRegistry = sessionRegistry; + } + + /** + * In addition to the steps from the superclass, the sessionRegistry will be updated with the new session information. + */ + public void onAuthentication(Authentication authentication, HttpServletRequest request, + HttpServletResponse response) { + + final List sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false); + + int sessionCount = sessions.size(); + int allowedSessions = getMaximumSessionsForThisUser(authentication); + + if (sessionCount < allowedSessions) { + // They haven't got too many login sessions running at present + return; + } + + if (allowedSessions == -1) { + // We permit unlimited logins + return; + } + + if (sessionCount == allowedSessions) { + HttpSession session = request.getSession(false); + + if (session != null) { + // Only permit it though if this request is associated with one of the already registered sessions + for (SessionInformation si : sessions) { + if (si.getSessionId().equals(session.getId())) { + return; + } + } + } + // If the session is null, a new one will be created by the parent class, exceeding the allowed number + } + + allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry); + } + + /** + * Method intended for use by subclasses to override the maximum number of sessions that are permitted for + * a particular authentication. The default implementation simply returns the maximumSessions value + * for the bean. + * + * @param authentication to determine the maximum sessions for + * + * @return either -1 meaning unlimited, or a positive integer to limit (never zero) + */ + protected int getMaximumSessionsForThisUser(Authentication authentication) { + return maximumSessions; + } + + /** + * Allows subclasses to customise behaviour when too many sessions are detected. + * + * @param sessions either null or all unexpired sessions associated with the principal + * @param allowableSessions the number of concurrent sessions the user is allowed to have + * @param registry an instance of the SessionRegistry for subclass use + * + */ + protected void allowableSessionsExceeded(List sessions, int allowableSessions, + SessionRegistry registry) throws SessionAuthenticationException { + if (exceptionIfMaximumExceeded || (sessions == null)) { + throw new SessionAuthenticationException(messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", + new Object[] {Integer.valueOf(allowableSessions)}, + "Maximum sessions of {0} for this principal exceeded")); + } + + // Determine least recently used session, and mark it for invalidation + SessionInformation leastRecentlyUsed = null; + + for (SessionInformation session : sessions) { + if ((leastRecentlyUsed == null) + || session.getLastRequest().before(leastRecentlyUsed.getLastRequest())) { + leastRecentlyUsed = session; + } + } + + leastRecentlyUsed.expireNow(); + } + + /** + * Sets the exceptionIfMaximumExceeded property, which determines + * whether the user should be prevented from opening more sessions than + * allowed. If set to true, a + * SessionAuthenticationException will be raised which means the + * user authenticating will be prevented from authenticating. if set to + * false, the user that has already authenticated will be forcibly + * logged out. + * + * @param exceptionIfMaximumExceeded + * defaults to false. + */ + public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) { + this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded; + } + + /** + * Sets the maxSessions property. The default value is 1. Use -1 for unlimited sessions. + * + * @param maximumSessions the maximimum number of permitted sessions a user can have open simultaneously. + */ + public void setMaximumSessions(int maximumSessions) { + Assert.isTrue(maximumSessions != 0, + "MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum"); + this.maximumSessions = maximumSessions; + } + + /** + * Sets the {@link MessageSource} used for reporting errors back to the user + * when the user has exceeded the maximum number of authentications. + */ + public void setMessageSource(MessageSource messageSource) { + Assert.notNull(messageSource, "messageSource cannot be null"); + this.messages = new MessageSourceAccessor(messageSource); + } +} diff --git a/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlStrategy.java b/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlStrategy.java index 2c14211f52..b1a6399386 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlStrategy.java +++ b/web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlStrategy.java @@ -37,7 +37,9 @@ import org.springframework.util.Assert; * * @author Luke Taylor * @since 3.0 + * @deprecated Use {@link ConcurrentSessionControlAuthenticationStrategy} instead */ +@Deprecated public class ConcurrentSessionControlStrategy extends SessionFixationProtectionStrategy implements MessageSourceAware { protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); diff --git a/web/src/main/java/org/springframework/security/web/authentication/session/RegisterSessionAuthenticationStrategy.java b/web/src/main/java/org/springframework/security/web/authentication/session/RegisterSessionAuthenticationStrategy.java new file mode 100644 index 0000000000..74fecf9262 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/session/RegisterSessionAuthenticationStrategy.java @@ -0,0 +1,51 @@ +package org.springframework.security.web.authentication.session; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.web.session.HttpSessionEventPublisher; +import org.springframework.util.Assert; + +/** + * Strategy used to register a user with the {@link SessionRegistry} after + * successful {@link Authentication}. + * + *

+ * {@link RegisterSessionAuthenticationStrategy} is typically used in + * combination with {@link CompositeSessionAuthenticationStrategy} and + * {@link ConcurrentSessionControlAuthenticationStrategy}, but can be used on + * its own if tracking of sessions is desired but no need to control + * concurrency.

+ * NOTE: When using a {@link SessionRegistry} it is important that all sessions + * (including timed out sessions) are removed. This is typically done by adding + * {@link HttpSessionEventPublisher}.

+ * + * @see CompositeSessionAuthenticationStrategy + * + * @author Luke Taylor + * @author Rob Winch + * @since 3.2 + */ +public class RegisterSessionAuthenticationStrategy implements SessionAuthenticationStrategy { + private final SessionRegistry sessionRegistry; + + /** + * @param sessionRegistry the session registry which should be updated when the authenticated session is changed. + */ + public RegisterSessionAuthenticationStrategy(SessionRegistry sessionRegistry) { + Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null"); + this.sessionRegistry = sessionRegistry; + } + + /** + * In addition to the steps from the superclass, the sessionRegistry will be updated with the new session information. + */ + public void onAuthentication(Authentication authentication, HttpServletRequest request, + HttpServletResponse response) { + sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal()); + } +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategyTests.java b/web/src/test/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategyTests.java new file mode 100644 index 0000000000..5ab9c06a64 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategyTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2013 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.web.authentication.session; + +import static junit.framework.Assert.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.Collections; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.security.core.Authentication; + +/** + * @author Rob Winch + * + */ +@RunWith(MockitoJUnitRunner.class) +public class CompositeSessionAuthenticationStrategyTests { + @Mock + private SessionAuthenticationStrategy strategy1; + @Mock + private SessionAuthenticationStrategy strategy2; + @Mock + private Authentication authentication; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + + + @Test(expected = IllegalArgumentException.class) + public void constructorNullDelegates() { + new CompositeSessionAuthenticationStrategy(null); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorEmptyDelegates() { + new CompositeSessionAuthenticationStrategy(Collections.emptyList()); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorDelegatesContainNull() { + new CompositeSessionAuthenticationStrategy(Collections.singletonList(null)); + } + + @Test + public void delegatesToAll() { + CompositeSessionAuthenticationStrategy strategy = new CompositeSessionAuthenticationStrategy(Arrays.asList(strategy1,strategy2)); + strategy.onAuthentication(authentication, request, response); + + verify(strategy1).onAuthentication(authentication, request, response); + verify(strategy2).onAuthentication(authentication, request, response); + } + + + @Test + public void delegateShortCircuits() { + doThrow(new SessionAuthenticationException("oops")).when(strategy1).onAuthentication(authentication, request, response); + + CompositeSessionAuthenticationStrategy strategy = new CompositeSessionAuthenticationStrategy(Arrays.asList(strategy1,strategy2)); + + try { + strategy.onAuthentication(authentication, request, response); + fail("Expected Exception"); + } catch (SessionAuthenticationException success) {} + + verify(strategy1).onAuthentication(authentication, request, response); + verify(strategy2,times(0)).onAuthentication(authentication, request, response); + } +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java b/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java new file mode 100644 index 0000000000..76f5c4fec4 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2013 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.web.authentication.session; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; + +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.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.session.SessionInformation; +import org.springframework.security.core.session.SessionRegistry; + +/** + * + * @author Rob Winch + * + */ +@RunWith(MockitoJUnitRunner.class) +public class ConcurrentSessionControlAuthenticationStrategyTests { + @Mock + private SessionRegistry sessionRegistry; + + private Authentication authentication; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private SessionInformation sessionInformation; + + private ConcurrentSessionControlAuthenticationStrategy strategy; + + @Before + public void setup() throws Exception { + authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + sessionInformation = new SessionInformation(authentication.getPrincipal(), "unique", new Date(1374766134216L)); + + strategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullRegistry() { + new ConcurrentSessionControlAuthenticationStrategy(null); + } + + @Test + public void noRegisteredSession() { + when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Collections.emptyList()); + strategy.setMaximumSessions(1); + strategy.setExceptionIfMaximumExceeded(true); + + strategy.onAuthentication(authentication, request, response); + + // no exception + } + + @Test + public void maxSessionsSameSessionId() { + MockHttpSession session = new MockHttpSession(new MockServletContext(), sessionInformation.getSessionId()); + request.setSession(session); + when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Collections.singletonList(sessionInformation)); + strategy.setMaximumSessions(1); + strategy.setExceptionIfMaximumExceeded(true); + + strategy.onAuthentication(authentication, request, response); + + // no exception + } + + @Test(expected = SessionAuthenticationException.class) + public void maxSessionsWithException() { + when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Collections.singletonList(sessionInformation)); + strategy.setMaximumSessions(1); + strategy.setExceptionIfMaximumExceeded(true); + + strategy.onAuthentication(authentication, request, response); + } + + @Test + public void maxSessionsExpireExistingUser() { + when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Collections.singletonList(sessionInformation)); + strategy.setMaximumSessions(1); + + strategy.onAuthentication(authentication, request, response); + + assertThat(sessionInformation.isExpired()).isTrue(); + } + + @Test + public void maxSessionsExpireLeastRecentExistingUser() { + SessionInformation moreRecentSessionInfo = new SessionInformation(authentication.getPrincipal(), "unique", new Date(1374766999999L)); + when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Arrays.asList(moreRecentSessionInfo,sessionInformation)); + strategy.setMaximumSessions(2); + + strategy.onAuthentication(authentication, request, response); + + assertThat(sessionInformation.isExpired()).isTrue(); + } + + @Test(expected = IllegalArgumentException.class) + public void setMessageSourceNull() { + strategy.setMessageSource(null); + } +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/session/RegisterSessionAuthenticationStrategyTests.java b/web/src/test/java/org/springframework/security/web/authentication/session/RegisterSessionAuthenticationStrategyTests.java new file mode 100644 index 0000000000..75b2002720 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/session/RegisterSessionAuthenticationStrategyTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2013 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.web.authentication.session; + +import static org.mockito.Mockito.verify; + +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.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.session.SessionRegistry; + +/** + * @author Rob Winch + * + */ +@RunWith(MockitoJUnitRunner.class) +public class RegisterSessionAuthenticationStrategyTests { + + @Mock + private SessionRegistry registry; + + private RegisterSessionAuthenticationStrategy authenticationStrategy; + + private Authentication authentication; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + @Before + public void setup() { + authenticationStrategy = new RegisterSessionAuthenticationStrategy(registry); + authentication = new TestingAuthenticationToken("user", "password","ROLE_USER"); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullRegistry() { + new RegisterSessionAuthenticationStrategy(null); + } + + @Test + public void onAuthenticationRegistersSession() { + authenticationStrategy.onAuthentication(authentication, request, response); + + verify(registry).registerNewSession(request.getSession().getId(), authentication.getPrincipal()); + } + +}