Browse Source

SEC-2137: Allow disabling session fixation and enable concurrency control

Rob Winch 12 years ago
parent
commit
13da42ca1b
15 changed files with 970 additions and 65 deletions
  1. 80 9
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java
  2. 56 17
      config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java
  3. 6 4
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy
  4. 98 8
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.groovy
  5. 79 16
      config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy
  6. 2 2
      docs/manual/src/docbook/appendix-namespace.xml
  7. 16 5
      docs/manual/src/docbook/session-mgmt.xml
  8. 15 4
      itest/web/src/main/webapp/WEB-INF/http-security-custom-concurrency.xml
  9. 91 0
      web/src/main/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategy.java
  10. 183 0
      web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java
  11. 2 0
      web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlStrategy.java
  12. 51 0
      web/src/main/java/org/springframework/security/web/authentication/session/RegisterSessionAuthenticationStrategy.java
  13. 92 0
      web/src/test/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategyTests.java
  14. 132 0
      web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java
  15. 67 0
      web/src/test/java/org/springframework/security/web/authentication/session/RegisterSessionAuthenticationStrategyTests.java

+ 80 - 9
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;
 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.HttpServletResponse;
+import javax.servlet.http.HttpSession;
 
 
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 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.SessionRegistry;
 import org.springframework.security.core.session.SessionRegistryImpl;
 import org.springframework.security.core.session.SessionRegistryImpl;
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
 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.SessionAuthenticationStrategy;
 import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
 import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
@@ -70,7 +77,8 @@ import org.springframework.util.Assert;
  * @see ConcurrentSessionFilter
  * @see ConcurrentSessionFilter
  */
  */
 public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<H> {
 public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<H> {
-    private SessionAuthenticationStrategy sessionAuthenticationStrategy = new SessionFixationProtectionStrategy();
+    private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = new SessionFixationProtectionStrategy();
+    private SessionAuthenticationStrategy sessionAuthenticationStrategy;
     private SessionRegistry sessionRegistry = new SessionRegistryImpl();
     private SessionRegistry sessionRegistry = new SessionRegistryImpl();
     private Integer maximumSessions;
     private Integer maximumSessions;
     private String expiredUrl;
     private String expiredUrl;
@@ -149,17 +157,25 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
     /**
     /**
      * Allows explicitly specifying the {@link SessionAuthenticationStrategy}.
      * Allows explicitly specifying the {@link SessionAuthenticationStrategy}.
      * The default is to use {@link SessionFixationProtectionStrategy}. If
      * 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
      * @param sessionAuthenticationStrategy
-     * @return the {@link SessionManagementConfigurer} for further customizations
+     * @return the {@link SessionManagementConfigurer} for further
+     *         customizations
      */
      */
     public SessionManagementConfigurer<H> sessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
     public SessionManagementConfigurer<H> sessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
-        this.sessionAuthenticationStrategy = sessionAuthenticationStrategy;
+        this.sessionFixationAuthenticationStrategy = sessionAuthenticationStrategy;
         return this;
         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.
      * 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
      * @param maximumSessions the maximum number of sessions for a user
@@ -167,10 +183,57 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
      */
      */
     public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
     public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
         this.maximumSessions = maximumSessions;
         this.maximumSessions = maximumSessions;
-        this.sessionAuthenticationStrategy = null;
         return new ConcurrencyControlConfigurer();
         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<H> 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<H> 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<H> none() {
+            SessionManagementConfigurer.this.sessionFixationAuthenticationStrategy = new NullAuthenticatedSessionStrategy();
+            return SessionManagementConfigurer.this;
+        }
+    }
+
     /**
     /**
      * Allows configuring controlling of multiple sessions.
      * Allows configuring controlling of multiple sessions.
      *
      *
@@ -314,10 +377,18 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
             return sessionAuthenticationStrategy;
             return sessionAuthenticationStrategy;
         }
         }
         if(isConcurrentSessionControlEnabled()) {
         if(isConcurrentSessionControlEnabled()) {
-            ConcurrentSessionControlStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlStrategy(sessionRegistry);
+            ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
             concurrentSessionControlStrategy.setMaximumSessions(maximumSessions);
             concurrentSessionControlStrategy.setMaximumSessions(maximumSessions);
             concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(maxSessionsPreventsLogin);
             concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(maxSessionsPreventsLogin);
-            sessionAuthenticationStrategy = concurrentSessionControlStrategy;
+            concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy);
+
+            RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(sessionRegistry);
+            registerSessionStrategy = postProcess(registerSessionStrategy);
+
+            List<SessionAuthenticationStrategy> delegateStrategies = Arrays.asList(concurrentSessionControlStrategy, sessionFixationAuthenticationStrategy, registerSessionStrategy);
+            sessionAuthenticationStrategy = postProcess(new CompositeSessionAuthenticationStrategy(delegateStrategies));
+        } else {
+            sessionAuthenticationStrategy = sessionFixationAuthenticationStrategy;
         }
         }
         return sessionAuthenticationStrategy;
         return sessionAuthenticationStrategy;
     }
     }

+ 56 - 17
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");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with 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.HttpSecurityBeanDefinitionParser.*;
 import static org.springframework.security.config.http.SecurityFilters.*;
 import static org.springframework.security.config.http.SecurityFilters.*;
 
 
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 
 
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
 
 
 import org.springframework.beans.BeanMetadataElement;
 import org.springframework.beans.BeanMetadataElement;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanReference;
 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.config.RuntimeBeanReference;
 import org.springframework.beans.factory.parsing.BeanComponentDefinition;
 import org.springframework.beans.factory.parsing.BeanComponentDefinition;
 import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
 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.DefaultFilterInvocationSecurityMetadataSource;
 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
 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.authentication.session.SessionFixationProtectionStrategy;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 import org.springframework.security.web.context.NullSecurityContextRepository;
 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.SessionManagementFilter;
 import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
 import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ClassUtils;
+import org.springframework.util.ReflectionUtils;
 import org.springframework.util.StringUtils;
 import org.springframework.util.StringUtils;
 import org.springframework.util.xml.DomUtils;
 import org.springframework.util.xml.DomUtils;
 import org.w3c.dom.Element;
 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 ATT_SESSION_FIXATION_PROTECTION = "session-fixation-protection";
     private static final String OPT_SESSION_FIXATION_NO_PROTECTION = "none";
     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_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_INVALID_SESSION_URL = "invalid-session-url";
     private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref";
     private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref";
@@ -247,6 +255,7 @@ class HttpConfigurationBuilder {
         String sessionAuthStratRef = null;
         String sessionAuthStratRef = null;
         String errorUrl = null;
         String errorUrl = null;
 
 
+        boolean sessionControlEnabled = false;
         if (sessionMgmtElt != null) {
         if (sessionMgmtElt != null) {
             if (sessionPolicy == SessionCreationPolicy.STATELESS) {
             if (sessionPolicy == SessionCreationPolicy.STATELESS) {
                 pc.getReaderContext().error(Elements.SESSION_MANAGEMENT + "  cannot be used" +
                 pc.getReaderContext().error(Elements.SESSION_MANAGEMENT + "  cannot be used" +
@@ -258,8 +267,9 @@ class HttpConfigurationBuilder {
             sessionAuthStratRef = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_STRATEGY_REF);
             sessionAuthStratRef = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_STRATEGY_REF);
             errorUrl = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_ERROR_URL);
             errorUrl = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_ERROR_URL);
             sessionCtrlElt = DomUtils.getChildElementByTagName(sessionMgmtElt, Elements.CONCURRENT_SESSIONS);
             sessionCtrlElt = DomUtils.getChildElementByTagName(sessionMgmtElt, Elements.CONCURRENT_SESSIONS);
+            sessionControlEnabled =  sessionCtrlElt != null;
 
 
-            if (sessionCtrlElt != null) {
+            if (sessionControlEnabled) {
                 if (StringUtils.hasText(sessionAuthStratRef)) {
                 if (StringUtils.hasText(sessionAuthStratRef)) {
                     pc.getReaderContext().error(ATT_SESSION_AUTH_STRATEGY_REF + " attribute cannot be used" +
                     pc.getReaderContext().error(ATT_SESSION_AUTH_STRATEGY_REF + " attribute cannot be used" +
                             " in combination with <" + Elements.CONCURRENT_SESSIONS + ">", pc.extractSource(sessionCtrlElt));
                             " in combination with <" + Elements.CONCURRENT_SESSIONS + ">", pc.extractSource(sessionCtrlElt));
@@ -269,7 +279,8 @@ class HttpConfigurationBuilder {
         }
         }
 
 
         if (!StringUtils.hasText(sessionFixationAttribute)) {
         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)) {
         } else if (StringUtils.hasText(sessionAuthStratRef)) {
             pc.getReaderContext().error(ATT_SESSION_FIXATION_PROTECTION + " attribute cannot be used" +
             pc.getReaderContext().error(ATT_SESSION_FIXATION_PROTECTION + " attribute cannot be used" +
                     " in combination with " + ATT_SESSION_AUTH_STRATEGY_REF, pc.extractSource(sessionMgmtElt));
                     " 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);
         boolean sessionFixationProtectionRequired = !sessionFixationAttribute.equals(OPT_SESSION_FIXATION_NO_PROTECTION);
 
 
-        BeanDefinitionBuilder sessionStrategy;
+        ManagedList<BeanMetadataElement> delegateSessionStrategies = new ManagedList<BeanMetadataElement>();
+        BeanDefinitionBuilder concurrentSessionStrategy;
+        BeanDefinitionBuilder sessionFixationStrategy = null;
+        BeanDefinitionBuilder registerSessionStrategy;
 
 
-        if (sessionCtrlElt != null) {
+        if (sessionControlEnabled) {
             assert sessionRegistryRef != null;
             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");
             String maxSessions = sessionCtrlElt.getAttribute("max-sessions");
 
 
             if (StringUtils.hasText(maxSessions)) {
             if (StringUtils.hasText(maxSessions)) {
-                sessionStrategy.addPropertyValue("maximumSessions", maxSessions);
+                concurrentSessionStrategy.addPropertyValue("maximumSessions", maxSessions);
             }
             }
 
 
             String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded");
             String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded");
 
 
             if (StringUtils.hasText(exceptionIfMaximumExceeded)) {
             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;
             sfpf = null;
             return;
             return;
         }
         }
@@ -316,15 +349,21 @@ class HttpConfigurationBuilder {
         sessionMgmtFilter.addPropertyValue("authenticationFailureHandler", failureHandler);
         sessionMgmtFilter.addPropertyValue("authenticationFailureHandler", failureHandler);
         sessionMgmtFilter.addConstructorArgValue(contextRepoRef);
         sessionMgmtFilter.addConstructorArgValue(contextRepoRef);
 
 
-        if (!StringUtils.hasText(sessionAuthStratRef)) {
-            BeanDefinition strategyBean = sessionStrategy.getBeanDefinition();
+        if (!StringUtils.hasText(sessionAuthStratRef) && sessionFixationStrategy != null && !useChangeSessionId ) {
 
 
             if (sessionFixationProtectionRequired) {
             if (sessionFixationProtectionRequired) {
-                sessionStrategy.addPropertyValue("migrateSessionAttributes",
+                sessionFixationStrategy.addPropertyValue("migrateSessionAttributes",
                         Boolean.valueOf(sessionFixationAttribute.equals(OPT_SESSION_FIXATION_MIGRATE_SESSION)));
                         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);
             sessionAuthStratRef = pc.getReaderContext().generateBeanName(strategyBean);
             pc.registerBeanComponent(new BeanComponentDefinition(strategyBean, sessionAuthStratRef));
             pc.registerBeanComponent(new BeanComponentDefinition(strategyBean, sessionAuthStratRef));
+
         }
         }
 
 
         if (StringUtils.hasText(invalidSessionUrl)) {
         if (StringUtils.hasText(invalidSessionUrl)) {

+ 6 - 4
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)
             CustomSessionManagementConfig.SR = Mock(SessionRegistry)
         when:
         when:
             loadConfig(CustomSessionManagementConfig)
             loadConfig(CustomSessionManagementConfig)
+            def concurrentStrategy = findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies[0]
         then:
         then:
             findFilter(SessionManagementFilter).invalidSessionStrategy.destinationUrl == "/invalid-session"
             findFilter(SessionManagementFilter).invalidSessionStrategy.destinationUrl == "/invalid-session"
             findFilter(SessionManagementFilter).failureHandler.defaultFailureUrl == "/session-auth-error"
             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"
             findFilter(ConcurrentSessionFilter).expiredUrl == "/expired-session"
     }
     }
 
 
@@ -154,7 +155,8 @@ class NamespaceSessionManagementTests extends BaseSpringSpec {
         protected void configure(HttpSecurity http) throws Exception {
         protected void configure(HttpSecurity http) throws Exception {
             http
             http
                 .sessionManagement()
                 .sessionManagement()
-                    .sessionAuthenticationStrategy(new SessionFixationProtectionStrategy(migrateSessionAttributes : false))
+                    .sessionFixation()
+                        .newSession()
         }
         }
     }
     }
 }
 }

+ 98 - 8
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
 package org.springframework.security.config.annotation.web.configurers
 
 
+import javax.servlet.http.HttpServletResponse
+
 import org.springframework.context.annotation.Configuration
 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.AnyObjectPostProcessor
 import org.springframework.security.config.annotation.BaseSpringSpec
 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.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.SecurityContextPersistenceFilter
 import org.springframework.security.web.context.SecurityContextRepository
 import org.springframework.security.web.context.SecurityContextRepository
 import org.springframework.security.web.savedrequest.RequestCache
 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"() {
     def "sessionManagement ObjectPostProcessor"() {
         setup:
         setup:
             AnyObjectPostProcessor opp = Mock()
             AnyObjectPostProcessor opp = Mock()
@@ -122,9 +206,15 @@ class SessionManagementConfigurerTests extends BaseSpringSpec {
                     .and()
                     .and()
                 .build()
                 .build()
 
 
-        then: "SessionManagementFilter is registered with LifecycleManager"
+        then: "SessionManagementFilter is registered with ObjectPostProcessor"
             1 * opp.postProcess(_ as SessionManagementFilter) >> {SessionManagementFilter o -> o}
             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}
             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}
     }
     }
 }
 }

+ 79 - 16
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");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -15,28 +15,40 @@
  */
  */
 package org.springframework.security.config.http
 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.MockFilterChain
 import org.springframework.mock.web.MockHttpServletRequest
 import org.springframework.mock.web.MockHttpServletRequest
 import org.springframework.mock.web.MockHttpServletResponse
 import org.springframework.mock.web.MockHttpServletResponse
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
 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.SecurityContext
 import org.springframework.security.core.context.SecurityContextHolder
 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.session.SessionRegistryImpl
+import org.springframework.security.core.userdetails.User
 import org.springframework.security.util.FieldUtils
 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.RememberMeServices
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
 import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler
 import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler
 import org.springframework.security.web.authentication.logout.LogoutFilter
 import org.springframework.security.web.authentication.logout.LogoutFilter
 import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler
 import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler
 import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter
 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.NullSecurityContextRepository
 import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper
 import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper
 import org.springframework.security.web.context.SecurityContextPersistenceFilter
 import org.springframework.security.web.context.SecurityContextPersistenceFilter
 import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
 import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
 import org.springframework.security.web.session.ConcurrentSessionFilter
 import org.springframework.security.web.session.ConcurrentSessionFilter
 import org.springframework.security.web.session.SessionManagementFilter
 import org.springframework.security.web.session.SessionManagementFilter
-import static org.junit.Assert.assertSame
 
 
 /**
 /**
  * Tests session-related functionality for the &lt;http&gt; namespace element and &lt;session-management&gt;
  * Tests session-related functionality for the &lt;http&gt; namespace element and &lt;session-management&gt;
@@ -93,6 +105,46 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
         filter.repo.allowSessionCreation
         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) {
     def httpCreateSession(String create, Closure c) {
         xml.http(['auto-config': 'true', 'create-session': create], c)
         xml.http(['auto-config': 'true', 'create-session': create], c)
     }
     }
@@ -219,15 +271,28 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
     }
     }
 
 
     def externalSessionStrategyIsSupported() {
     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() {
     def externalSessionRegistryBeanIsConfiguredCorrectly() {
@@ -247,10 +312,8 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
         Object sessionRegistry = appContext.getBean("sr");
         Object sessionRegistry = appContext.getBean("sr");
         Object sessionRegistryFromConcurrencyFilter = FieldUtils.getFieldValue(
         Object sessionRegistryFromConcurrencyFilter = FieldUtils.getFieldValue(
                 getFilter(ConcurrentSessionFilter.class), "sessionRegistry");
                 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, sessionRegistryFromConcurrencyFilter);
         assertSame(sessionRegistry, sessionRegistryFromMgmtFilter);
         assertSame(sessionRegistry, sessionRegistryFromMgmtFilter);
@@ -297,7 +360,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
         createAppContext()
         createAppContext()
 
 
         expect:
         expect:
-        !(getFilters("/someurl")[8] instanceof SessionManagementFilter)
+        !(getFilters("/someurl").find { it instanceof SessionManagementFilter})
     }
     }
 
 
     def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() {
     def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() {

+ 2 - 2
docs/manual/src/docbook/appendix-namespace.xml

@@ -1208,7 +1208,7 @@
             <para> Adds support for concurrent session control, allowing limits to be placed on the
             <para> Adds support for concurrent session control, allowing limits to be placed on the
                 number of active sessions a user can have. A
                 number of active sessions a user can have. A
                 <classname>ConcurrentSessionFilter</classname> will be created, and a
                 <classname>ConcurrentSessionFilter</classname> will be created, and a
-                <classname>ConcurrentSessionControlStrategy</classname> will be used with the
+                <classname>ConcurrentSessionControlAuthenticationStrategy</classname> will be used with the
                 <classname>SessionManagementFilter</classname>. If a <literal>form-login</literal>
                 <classname>SessionManagementFilter</classname>. If a <literal>form-login</literal>
                 element has been declared, the strategy object will also be injected into the
                 element has been declared, the strategy object will also be injected into the
                 created authentication filter. An instance of
                 created authentication filter. An instance of
@@ -1242,7 +1242,7 @@
                 <section xml:id="nsa-concurrency-control-max-sessions">
                 <section xml:id="nsa-concurrency-control-max-sessions">
                     <title><literal>max-sessions</literal></title>
                     <title><literal>max-sessions</literal></title>
                     <para>Maps to the <literal>maximumSessions</literal> property of
                     <para>Maps to the <literal>maximumSessions</literal> property of
-                        <classname>ConcurrentSessionControlStrategy</classname>.</para>
+                        <classname>ConcurrentSessionControlAuthenticationStrategy</classname>.</para>
                 </section>
                 </section>
                 <section xml:id="nsa-concurrency-control-session-registry-alias">
                 <section xml:id="nsa-concurrency-control-session-registry-alias">
                     <title><literal>session-registry-alias</literal></title>
                     <title><literal>session-registry-alias</literal></title>

+ 16 - 5
docs/manual/src/docbook/session-mgmt.xml

@@ -80,7 +80,7 @@
             though. </para>
             though. </para>
         <para>The implementation uses a specialized version of
         <para>The implementation uses a specialized version of
             <interfacename>SessionAuthenticationStrategy</interfacename>, called
             <interfacename>SessionAuthenticationStrategy</interfacename>, called
-            <classname>ConcurrentSessionControlStrategy</classname>. <note>
+            <classname>ConcurrentSessionControlAuthenticationStrategy</classname>. <note>
             <para>Previously the concurrent authentication check was made by the
             <para>Previously the concurrent authentication check was made by the
                 <classname>ProviderManager</classname>, which could be injected with a
                 <classname>ProviderManager</classname>, which could be injected with a
                 <literal>ConcurrentSessionController</literal>. The latter would check if the user
                 <literal>ConcurrentSessionController</literal>. The latter would check if the user
@@ -126,10 +126,21 @@
   <beans:property name="authenticationManager" ref="authenticationManager" />
   <beans:property name="authenticationManager" ref="authenticationManager" />
 </beans:bean>
 </beans:bean>
 
 
-<beans:bean id="sas" class=
- "org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
-  <beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
-  <beans:property name="maximumSessions" value="1" />
+<beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
+  <beans:constructor-arg>
+    <beans:list>
+      <beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
+        <beans:constructor-arg ref="sessionRegistry"/>
+        <beans:property name="maximumSessions" value="1" />
+        <beans:property name="exceptionIfMaximumExceeded" value="true" />
+      </beans:bean>
+      <beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
+      </beans:bean>
+      <beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
+        <beans:constructor-arg ref="sessionRegistry"/>
+      </beans:bean>
+    </beans:list>
+  </beans:constructor-arg>
 </beans:bean>
 </beans:bean>
 
 
 <beans:bean id="sessionRegistry"
 <beans:bean id="sessionRegistry"

+ 15 - 4
itest/web/src/main/webapp/WEB-INF/http-security-custom-concurrency.xml

@@ -32,10 +32,21 @@
         <beans:property name="expiredUrl" value="/session-expired.htm" />
         <beans:property name="expiredUrl" value="/session-expired.htm" />
     </beans:bean>
     </beans:bean>
 
 
-    <beans:bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
-        <beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
-        <beans:property name="maximumSessions" value="1" />
-        <beans:property name="exceptionIfMaximumExceeded" value="true" />
+    <beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
+        <beans:constructor-arg>
+            <beans:list>
+                <beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
+                    <beans:constructor-arg ref="sessionRegistry"/>
+                    <beans:property name="maximumSessions" value="1" />
+                    <beans:property name="exceptionIfMaximumExceeded" value="true" />
+                </beans:bean>
+                <beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
+                </beans:bean>
+                <beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
+                    <beans:constructor-arg ref="sessionRegistry"/>
+                </beans:bean>
+            </beans:list>
+        </beans:constructor-arg>
     </beans:bean>
     </beans:bean>
 
 
     <beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />
     <beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />

+ 91 - 0
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.
+ *
+ * <p>
+ * Typical usage would include having the following delegates (in this order)
+ * </p>
+ *
+ * <ul>
+ * <li> {@link ConcurrentSessionControlAuthenticationStrategy} - verifies that a
+ * user is allowed to authenticate (i.e. they have not already logged into the
+ * application.</li>
+ * <li> {@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.</li>
+ * <li> {@link RegisterSessionAuthenticationStrategy} - It is important this is
+ * after {@link SessionFixationProtectionStrategy} so that the correct session
+ * is registered.</li>
+ * </ul>
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public class CompositeSessionAuthenticationStrategy implements SessionAuthenticationStrategy {
+    private final Log logger = LogFactory.getLog(getClass());
+    private final List<SessionAuthenticationStrategy> delegateStrategies;
+
+    public CompositeSessionAuthenticationStrategy(List<SessionAuthenticationStrategy> 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<SessionAuthenticationStrategy>(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 + "]";
+    }
+}

+ 183 - 0
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.
+ *
+ * <p>
+ * 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 <tt>maximumSessions</tt> value.
+ * The {@link SessionRegistry} is used as the source of data on authenticated
+ * users and session data.
+ * </p>
+ * <p>
+ * If a user has reached the maximum number of permitted sessions, the behaviour
+ * depends on the <tt>exceptionIfMaxExceeded</tt> property. The default
+ * behaviour is to expired the least recently used session, which will be
+ * invalidated by the {@link ConcurrentSessionFilter} if accessed again. If
+ * <tt>exceptionIfMaxExceeded</tt> is set to <tt>true</tt>, however, the user
+ * will be prevented from starting a new authenticated session.
+ * </p>
+ * <p>
+ * 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}.
+ * </p>
+ *
+ * @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<SessionInformation> 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 <code>maximumSessions</code> 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 <code>null</code> 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 <code>SessionRegistry</code> for subclass use
+     *
+     */
+    protected void allowableSessionsExceeded(List<SessionInformation> 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 <tt>exceptionIfMaximumExceeded</tt> property, which determines
+     * whether the user should be prevented from opening more sessions than
+     * allowed. If set to <tt>true</tt>, a
+     * <tt>SessionAuthenticationException</tt> will be raised which means the
+     * user authenticating will be prevented from authenticating. if set to
+     * <tt>false</tt>, the user that has already authenticated will be forcibly
+     * logged out.
+     *
+     * @param exceptionIfMaximumExceeded
+     *            defaults to <tt>false</tt>.
+     */
+    public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) {
+        this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded;
+    }
+
+    /**
+     * Sets the <tt>maxSessions</tt> 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);
+    }
+}

+ 2 - 0
web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlStrategy.java

@@ -37,7 +37,9 @@ import org.springframework.util.Assert;
  *
  *
  * @author Luke Taylor
  * @author Luke Taylor
  * @since 3.0
  * @since 3.0
+ * @deprecated Use {@link ConcurrentSessionControlAuthenticationStrategy} instead
  */
  */
+@Deprecated
 public class ConcurrentSessionControlStrategy extends SessionFixationProtectionStrategy
 public class ConcurrentSessionControlStrategy extends SessionFixationProtectionStrategy
         implements MessageSourceAware {
         implements MessageSourceAware {
     protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
     protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

+ 51 - 0
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}.
+ *
+ * <p>
+ * {@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.</P
+ *
+ * <p>
+ * 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}.</p>
+ *
+ * @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());
+    }
+}

+ 92 - 0
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.<SessionAuthenticationStrategy>emptyList());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorDelegatesContainNull() {
+        new CompositeSessionAuthenticationStrategy(Collections.<SessionAuthenticationStrategy>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);
+    }
+}

+ 132 - 0
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.<SessionInformation>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.<SessionInformation>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.<SessionInformation>singletonList(sessionInformation));
+        strategy.setMaximumSessions(1);
+        strategy.setExceptionIfMaximumExceeded(true);
+
+        strategy.onAuthentication(authentication, request, response);
+    }
+
+    @Test
+    public void maxSessionsExpireExistingUser() {
+        when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Collections.<SessionInformation>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.<SessionInformation>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);
+    }
+}

+ 67 - 0
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());
+    }
+
+}