Przeglądaj źródła

SEC-710: Refactor concurrent session handling support.

Ben Alex 20 lat temu
rodzic
commit
1ae07779a2
20 zmienionych plików z 1284 dodań i 693 usunięć
  1. 6 7
      core/src/main/java/org/acegisecurity/concurrent/ConcurrentLoginException.java
  2. 65 0
      core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionController.java
  3. 155 0
      core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImpl.java
  4. 128 0
      core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionFilter.java
  5. 10 9
      core/src/main/java/org/acegisecurity/concurrent/NullConcurrentSessionController.java
  6. 32 0
      core/src/main/java/org/acegisecurity/concurrent/SessionAlreadyUsedException.java
  7. 94 0
      core/src/main/java/org/acegisecurity/concurrent/SessionInformation.java
  8. 83 0
      core/src/main/java/org/acegisecurity/concurrent/SessionRegistry.java
  9. 147 0
      core/src/main/java/org/acegisecurity/concurrent/SessionRegistryImpl.java
  10. 57 0
      core/src/main/java/org/acegisecurity/concurrent/SessionRegistryUtils.java
  11. 0 312
      core/src/main/java/org/acegisecurity/providers/ConcurrentSessionControllerImpl.java
  12. 0 42
      core/src/main/java/org/acegisecurity/providers/ConcurrentSessionViolationEvent.java
  13. 0 41
      core/src/main/java/org/acegisecurity/providers/NullConcurrentSessionController.java
  14. 4 2
      core/src/main/java/org/acegisecurity/providers/ProviderManager.java
  15. 124 0
      core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImplTests.java
  16. 173 0
      core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionFilterTests.java
  17. 49 0
      core/src/test/java/org/acegisecurity/concurrent/SessionInformationTests.java
  18. 155 0
      core/src/test/java/org/acegisecurity/concurrent/SessionRegistryImplTests.java
  19. 0 280
      core/src/test/java/org/acegisecurity/providers/ConcurrentSessionControllerImplTests.java
  20. 2 0
      core/src/test/java/org/acegisecurity/providers/ProviderManagerTests.java

+ 6 - 7
core/src/main/java/org/acegisecurity/providers/ConcurrentLoginException.java → core/src/main/java/org/acegisecurity/concurrent/ConcurrentLoginException.java

@@ -12,21 +12,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package net.sf.acegisecurity.providers;
+package net.sf.acegisecurity.concurrent;
 
 import net.sf.acegisecurity.AuthenticationException;
 
 
 /**
- * Thrown by the ConcurrentSessionController when the number of sessions
- * allowed is attempting to be exceeded.
+ * Thrown by <code>ConcurrentSessionControllerImpl</code> if
+ * an attempt is made to login and the user has already exceeded
+ * their maxmimum allowed sessions.
  *
- * @author Ray Krueger
+ * @author Ben Alex
+ * @version $Id$
  */
 public class ConcurrentLoginException extends AuthenticationException {
-    //~ Constructors ===========================================================
-
     public ConcurrentLoginException(String msg) {
         super(msg);
     }

+ 65 - 0
core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionController.java

@@ -0,0 +1,65 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+
+
+/**
+ * Provides two methods that can be called by an {@link
+ * net.sf.acegisecurity.AuthenticationManager} to integrate with the
+ * concurrent session handling infrastructure.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface ConcurrentSessionController {
+    //~ Methods ================================================================
+
+    /**
+     * Called by any class that wishes to know whether the current
+     * authentication request should be permitted. Generally callers will be
+     * <code>AuthenticationManager</code>s before they authenticate, but could
+     * equally include <code>Filter</code>s or other interceptors that wish to
+     * confirm the ongoing validity of a previously authenticated
+     * <code>Authentication</code>.
+     * 
+     * <p>
+     * The implementation should throw a suitable exception if the user has
+     * exceeded their maximum allowed concurrent sessions.
+     * </p>
+     *
+     * @param request the authentication request (never <code>null</code>)
+     *
+     * @throws AuthenticationException if the user has exceeded their maximum
+     *         allowed current sessions
+     */
+    public void checkAuthenticationAllowed(Authentication request)
+        throws AuthenticationException;
+
+    /**
+     * Called by an <code>AuthenticationManager</code> when the authentication
+     * was successful. An implementation is expected to register the
+     * authenticated user in some sort of registry, for future concurrent
+     * tracking via the {@link #checkConcurrentAuthentication(Authentication)}
+     * method.
+     *
+     * @param authentication the successfully authenticated user (never
+     *        <code>null</code>)
+     */
+    public void registerSuccessfulAuthentication(Authentication authentication);
+}

+ 155 - 0
core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImpl.java

@@ -0,0 +1,155 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import java.util.Date;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+
+
+/**
+ * Base implementation of {@link ConcurrentSessionControllerImpl} which
+ * prohibits simultaneous logins.
+ * 
+ * <p>
+ * By default uses {@link net.sf.acegisecurity.concurrent.SessionRegistryImpl},
+ * although any <code>SessionRegistry</code> may be used.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class ConcurrentSessionControllerImpl
+    implements ConcurrentSessionController, InitializingBean {
+    //~ Instance fields ========================================================
+
+    private SessionRegistry sessionRegistry = new SessionRegistryImpl();
+    private int maximumSessions = 1;
+    private boolean exceptionIfMaximumExceeded = false;
+
+    //~ Methods ================================================================
+
+    public void setMaximumSessions(int maximumSessions) {
+        this.maximumSessions = maximumSessions;
+    }
+
+    public void setSessionRegistry(SessionRegistry sessionRegistry) {
+        this.sessionRegistry = sessionRegistry;
+    }
+
+    public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) {
+		this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded;
+	}
+
+	public void afterPropertiesSet() throws Exception {
+        Assert.notNull(sessionRegistry, "SessionRegistry required");
+        Assert.isTrue(maximumSessions != 0,
+            "MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
+    }
+
+    public void checkAuthenticationAllowed(Authentication request)
+        throws AuthenticationException {
+        Assert.notNull(request,
+            "Authentication request cannot be null (violation of interface contract)");
+
+        Object principal = SessionRegistryUtils
+            .obtainPrincipalFromAuthentication(request);
+        String sessionId = SessionRegistryUtils
+            .obtainSessionIdFromAuthentication(request);
+
+        SessionInformation[] sessions = sessionRegistry.getAllSessions(principal);
+
+        int sessionCount = 0;
+
+        if (sessions != null) {
+            sessionCount = sessions.length;
+        }
+
+        int allowableSessions = getMaximumSessionsForThisUser(request);
+        Assert.isTrue(allowableSessions != 0,
+            "getMaximumSessionsForThisUser() must return either -1 to allow unlimited logins, or a positive integer to specify a maximum");
+
+        if (sessionCount < allowableSessions) {
+            return;
+        } else if (sessionCount == allowableSessions) {
+            // Only permit it though if this request is associated with one of the sessions
+            for (int i = 0; i < sessionCount; i++) {
+                if (sessions[i].getSessionId().equals(sessionId)) {
+                    return;
+                }
+            }
+        }
+
+        allowableSessionsExceeded(sessionId, sessions, allowableSessions, sessionRegistry);
+    }
+    
+    /**
+     * Allows subclasses to customise behaviour when too many sessions are
+     * detected.
+     * 
+     * @param sessionId the session ID of the present request
+     * @param sessions either <code>null</code> or all unexpired sessions associated with the principal
+     * @param registry an instance of the <code>SessionRegistry</code> for subclass use
+     */
+    protected void allowableSessionsExceeded(String sessionId, SessionInformation[] sessions, int allowableSessions, SessionRegistry registry) {
+        if (exceptionIfMaximumExceeded || sessions == null) {
+        	throw new ConcurrentLoginException("Maximum sessions of "
+                    + allowableSessions + " for this principal exceeded");
+        }
+        
+        // Determine least recently used session, and mark it for invalidation
+        SessionInformation leastRecentlyUsed = null;
+        for (int i = 0; i < sessions.length; i++) {
+        	if (leastRecentlyUsed == null || sessions[i].getLastRequest().before(leastRecentlyUsed.getLastRequest())) {
+        		leastRecentlyUsed = sessions[i];
+        	}
+        }
+        
+        leastRecentlyUsed.expireNow();
+    }
+
+    public void registerSuccessfulAuthentication(Authentication authentication) {
+        Assert.notNull(authentication,
+            "Authentication cannot be null (violation of interface contract)");
+
+        Object principal = SessionRegistryUtils
+            .obtainPrincipalFromAuthentication(authentication);
+        String sessionId = SessionRegistryUtils
+            .obtainSessionIdFromAuthentication(authentication);
+
+        sessionRegistry.removeSessionInformation(sessionId);
+        sessionRegistry.registerNewSession(sessionId, principal);
+    }
+
+    /**
+     * 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;
+    }
+}

+ 128 - 0
core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionFilter.java

@@ -0,0 +1,128 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import org.springframework.util.Assert;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+
+/**
+ * Filter required by concurrent session handling package.
+ * 
+ * <p>
+ * This filter performs two functions. First, it calls {@link
+ * net.sf.acegisecurity.concurrent.SessionRegistry#refreshLastRequest(String)}
+ * for each request. That way, registered sessions always have a correct "last
+ * update" date/time. Second, it retrieves {@link
+ * net.sf.acegisecurity.concurrent.SessionInformation} from the
+ * <code>SessionRegistry</code> for each request and checks if the session has
+ * been marked as expired. If it has been marked as expired, the session is
+ * invalidated. The invalidation of the session will also cause the request to
+ * redirect to the URL specified, and a {@link
+ * net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent} to be published
+ * via the {@link net.sf.acegisecurity.ui.session.HttpSessionEventPublisher}
+ * registered in <code>web.xml</code>.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class ConcurrentSessionFilter implements Filter,
+    InitializingBean {
+    //~ Instance fields ========================================================
+
+    private SessionRegistry sessionRegistry;
+    private String expiredUrl;
+
+    //~ Methods ================================================================
+
+    public void setExpiredUrl(String expiredUrl) {
+        this.expiredUrl = expiredUrl;
+    }
+
+    public void setSessionRegistry(SessionRegistry sessionRegistry) {
+        this.sessionRegistry = sessionRegistry;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.notNull(sessionRegistry, "SessionRegistry required");
+        Assert.hasText(expiredUrl, "ExpiredUrl required");
+    }
+
+    /**
+     * Does nothing. We use IoC container lifecycle services instead.
+     */
+    public void destroy() {}
+
+    public void doFilter(ServletRequest request, ServletResponse response,
+        FilterChain chain) throws IOException, ServletException {
+        Assert.isInstanceOf(HttpServletRequest.class, request,
+            "Can only process HttpServletRequest");
+        Assert.isInstanceOf(HttpServletResponse.class, response,
+            "Can only process HttpServletResponse");
+
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        HttpSession session = httpRequest.getSession(false);
+
+        if (session != null) {
+            SessionInformation info = sessionRegistry.getSessionInformation(session
+                    .getId());
+
+            if (info != null) {
+                if (info.isExpired()) {
+                    // Expired - abort processing
+                    session.invalidate();
+
+                    String targetUrl = httpRequest.getContextPath()
+                        + expiredUrl;
+                    httpResponse.sendRedirect(httpResponse.encodeRedirectURL(
+                            targetUrl));
+
+                    return;
+                } else {
+                    // Non-expired - update last request date/time
+                    info.refreshLastRequest();
+                }
+            }
+        }
+
+        chain.doFilter(request, response);
+    }
+
+    /**
+     * Does nothing. We use IoC container lifecycle services instead.
+     *
+     * @param arg0 ignored
+     *
+     * @throws ServletException ignored
+     */
+    public void init(FilterConfig arg0) throws ServletException {}
+}

+ 10 - 9
core/src/main/java/org/acegisecurity/providers/ConcurrentSessionController.java → core/src/main/java/org/acegisecurity/concurrent/NullConcurrentSessionController.java

@@ -13,24 +13,25 @@
  * limitations under the License.
  */
 
-package net.sf.acegisecurity.providers;
+package net.sf.acegisecurity.concurrent;
 
 import net.sf.acegisecurity.Authentication;
 import net.sf.acegisecurity.AuthenticationException;
 
 
 /**
- * See: {@link ConcurrentSessionControllerImpl}
+ * No-op implementation of {@link
+ * net.sf.acegisecurity.concurrent.ConcurrentSessionController}.
  *
- * @author Ray Krueger
- * @see ConcurrentSessionControllerImpl
+ * @author Ben Alex
+ * @version $Id$
  */
-public interface ConcurrentSessionController {
+public class NullConcurrentSessionController
+    implements ConcurrentSessionController {
     //~ Methods ================================================================
 
-    void afterAuthentication(Authentication initialAuth, Authentication result)
-            throws AuthenticationException;
+    public void checkAuthenticationAllowed(Authentication request)
+        throws AuthenticationException {}
 
-    void beforeAuthentication(Authentication initialAuth)
-            throws AuthenticationException;
+    public void registerSuccessfulAuthentication(Authentication authentication) {}
 }

+ 32 - 0
core/src/main/java/org/acegisecurity/concurrent/SessionAlreadyUsedException.java

@@ -0,0 +1,32 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import net.sf.acegisecurity.AuthenticationException;
+
+
+/**
+ * Thrown by a <code>SessionRegistry</code> implementation if
+ * an attempt is made to create new session information for an existing
+ * sessionId. The user should firstly clear the existing session from the
+ * <code>ConcurrentSessionRegistry</code>.
+ *
+ * @author Ben Alex
+ */
+public class SessionAlreadyUsedException extends AuthenticationException {
+    public SessionAlreadyUsedException(String msg) {
+        super(msg);
+    }
+}

+ 94 - 0
core/src/main/java/org/acegisecurity/concurrent/SessionInformation.java

@@ -0,0 +1,94 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import org.springframework.util.Assert;
+
+import java.util.Date;
+
+
+/**
+ * Represents a record of a session within the Acegi Security framework.
+ * 
+ * <p>
+ * This is primarily used for concurrent session support.
+ * </p>
+ * 
+ * <p>
+ * Sessions have three states: active, expired, and destroyed. A session can
+ * that is invalidated by <code>session.invalidate()</code> or via Servlet
+ * Container management is considered "destroyed". An "expired" session, on
+ * the other hand, is a session that Acegi Security wants to end because it
+ * was selected for removal for some reason (generally as it was the least
+ * recently used session and the maximum sessions for the user were reached).
+ * An "expired" session is removed as soon as possible by a
+ * <code>Filter</code>.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class SessionInformation {
+    //~ Instance fields ========================================================
+
+    private Date lastRequest;
+    private Object principal;
+    private String sessionId;
+    private boolean expired = false;
+
+    //~ Constructors ===========================================================
+
+    public SessionInformation(Object principal, String sessionId,
+        Date lastRequest) {
+        Assert.notNull(principal, "Principal required");
+        Assert.hasText(sessionId, "SessionId required");
+        Assert.notNull(lastRequest, "LastRequest required");
+        this.principal = principal;
+        this.sessionId = sessionId;
+        this.lastRequest = lastRequest;
+    }
+
+    private SessionInformation() {}
+
+    //~ Methods ================================================================
+
+    public boolean isExpired() {
+        return expired;
+    }
+
+    public Date getLastRequest() {
+        return lastRequest;
+    }
+
+    public Object getPrincipal() {
+        return principal;
+    }
+
+    public String getSessionId() {
+        return sessionId;
+    }
+
+    public void expireNow() {
+        this.expired = true;
+    }
+
+    /**
+     * Refreshes the internal lastRequest to the current date and time.
+     */
+    public void refreshLastRequest() {
+        this.lastRequest = new Date();
+    }
+}

+ 83 - 0
core/src/main/java/org/acegisecurity/concurrent/SessionRegistry.java

@@ -0,0 +1,83 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+/**
+ * Maintains a registry of <code>SessionInformation</code> instances.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface SessionRegistry {
+    //~ Methods ================================================================
+
+    /**
+     * Obtains all the known sessions for the specified principal. Sessions
+     * that have expired or destroyed are not returned.
+     *
+     * @param principal to locate sessions for (should never be
+     *        <code>null</code>)
+     *
+     * @return the unexpired and undestroyed sessions for this principal, or
+     *         <code>null</code> if none were found
+     */
+    public SessionInformation[] getAllSessions(Object principal);
+
+    /**
+     * Obtains the session information for the specified
+     * <code>sessionId</code>. Even expired sessions are returned (although
+     * destroyed sessions are never returned).
+     *
+     * @param sessionId to lookup (should never be <code>null</code>)
+     *
+     * @return the session information, or <code>null</code> if not found
+     */
+    public SessionInformation getSessionInformation(String sessionId);
+
+    /**
+     * Updates the given <code>sessionId</code> so its last request time is
+     * equal to the present date and time. Silently returns if the given
+     * <code>sessionId</code> cannot be found or the session is marked to expire.
+     *
+     * @param sessionId for which to update the date and time of the last
+     *        request (should never be <code>null</code>)
+     */
+    public void refreshLastRequest(String sessionId);
+
+    /**
+     * Registers a new session for the specified principal. The newly
+     * registered session will not be marked for expiration.
+     *
+     * @param sessionId to associate with the principal (should never be
+     *        <code>null</code>)
+     * @param principal to associate with the session (should never be
+     *        <code>null</code>)
+     *
+     * @throws SessionAlreadyUsedException DOCUMENT ME!
+     */
+    public void registerNewSession(String sessionId, Object principal)
+        throws SessionAlreadyUsedException;
+
+    /**
+     * Deletes all the session information being maintained for the specified
+     * <code>sessionId</code>. If the <code>sessionId</code> is not found, the
+     * method gracefully returns.
+     *
+     * @param sessionId to delete information for (should never be
+     *        <code>null</code>)
+     */
+    public void removeSessionInformation(String sessionId);
+}

+ 147 - 0
core/src/main/java/org/acegisecurity/concurrent/SessionRegistryImpl.java

@@ -0,0 +1,147 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpSession;
+
+import net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent;
+
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.util.Assert;
+
+
+/**
+ * Base implementation of {@link
+ * net.sf.acegisecurity.concurrent.SessionRegistry} which also listens for
+ * {@link net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent}s
+ * published in the Spring application context.
+ * 
+ * <p>
+ * NB: It is important that you register the {@link
+ * net.sf.acegisecurity.ui.session.HttpSessionEventPublisher} in
+ * <code>web.xml</code> so that this class is notified of sessions that
+ * expire.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id${date}
+ */
+public class SessionRegistryImpl implements SessionRegistry,
+    ApplicationListener {
+    //~ Instance fields ========================================================
+
+    private Map principals = Collections.synchronizedMap(new HashMap()); // <principal:Object,SessionIdSet>
+    private Map sessionIds = Collections.synchronizedMap(new HashMap()); // <sessionId:Object,SessionInformation>
+
+    //~ Methods ================================================================
+
+    public SessionInformation[] getAllSessions(Object principal) {
+        Set sessionsUsedByPrincipal = (Set) principals.get(principal);
+
+        if (sessionsUsedByPrincipal == null) {
+            return null;
+        }
+        
+        List list = new ArrayList();
+        Iterator iter = sessionsUsedByPrincipal.iterator();
+        while (iter.hasNext()) {
+        	String sessionId = (String) iter.next();
+        	list.add(getSessionInformation(sessionId));
+        }
+
+        return (SessionInformation[]) list.toArray(new SessionInformation[] {});
+    }
+
+    public SessionInformation getSessionInformation(String sessionId) {
+        Assert.hasText(sessionId, "SessionId required as per inerface contract");
+
+        return (SessionInformation) sessionIds.get(sessionId);
+    }
+
+    public void onApplicationEvent(ApplicationEvent event) {
+        if (event instanceof HttpSessionDestroyedEvent) {
+            String sessionId = ((HttpSession) event.getSource()).getId();
+            removeSessionInformation(sessionId);
+        }
+    }
+
+    public void refreshLastRequest(String sessionId) {
+        Assert.hasText(sessionId, "SessionId required as per inerface contract");
+
+        SessionInformation info = getSessionInformation(sessionId);
+
+        if (info != null) {
+            info.refreshLastRequest();
+        }
+    }
+
+    public void registerNewSession(String sessionId, Object principal)
+        throws SessionAlreadyUsedException {
+        Assert.hasText(sessionId, "SessionId required as per inerface contract");
+        Assert.notNull(principal, "Principal required as per inerface contract");
+
+        if (getSessionInformation(sessionId) != null) {
+            throw new SessionAlreadyUsedException("Session " + sessionId
+                + " is already is use");
+        }
+
+        sessionIds.put(sessionId,
+            new SessionInformation(principal, sessionId, new Date()));
+
+        Set sessionsUsedByPrincipal = (Set) principals.get(principal);
+
+        if (sessionsUsedByPrincipal == null) {
+            sessionsUsedByPrincipal = Collections.synchronizedSet(new HashSet());
+        }
+
+        sessionsUsedByPrincipal.add(sessionId);
+
+        principals.put(principal, sessionsUsedByPrincipal);
+    }
+
+    public void removeSessionInformation(String sessionId) {
+        Assert.hasText(sessionId, "SessionId required as per inerface contract");
+
+        SessionInformation info = getSessionInformation(sessionId);
+
+        if (info != null) {
+            sessionIds.remove(sessionId);
+
+            Set sessionsUsedByPrincipal = (Set) principals.get(info
+                    .getPrincipal());
+
+            if (sessionsUsedByPrincipal != null) {
+                sessionsUsedByPrincipal.remove(sessionId);
+                
+                if (sessionsUsedByPrincipal.size() == 0) {
+                	// No need to keep pbject in principals Map anymore 
+                	principals.remove(info.getPrincipal());
+                }
+            }
+        }
+    }
+}

+ 57 - 0
core/src/main/java/org/acegisecurity/concurrent/SessionRegistryUtils.java

@@ -0,0 +1,57 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.UserDetails;
+import net.sf.acegisecurity.ui.WebAuthenticationDetails;
+
+import org.springframework.util.Assert;
+
+
+/**
+ * Utility methods to assist with concurrent session management.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class SessionRegistryUtils {
+    //~ Methods ================================================================
+
+    public static Object obtainPrincipalFromAuthentication(Authentication auth) {
+        Assert.notNull(auth, "Authentication required");
+        Assert.notNull(auth.getPrincipal(),
+            "Authentication.getPrincipal() required");
+
+        if (auth.getPrincipal() instanceof UserDetails) {
+            return ((UserDetails) auth.getPrincipal()).getUsername();
+        } else {
+            return auth.getPrincipal();
+        }
+    }
+
+    public static String obtainSessionIdFromAuthentication(Authentication auth) {
+        Assert.notNull(auth, "Authentication required");
+        Assert.notNull(auth.getDetails(), "Authentication.getDetails() required");
+        Assert.isInstanceOf(WebAuthenticationDetails.class, auth.getDetails());
+
+        String sessionId = ((WebAuthenticationDetails) auth.getDetails())
+            .getSessionId();
+        Assert.hasText(sessionId, "WebAuthenticationDetails missing SessionId");
+
+        return sessionId;
+    }
+}

+ 0 - 312
core/src/main/java/org/acegisecurity/providers/ConcurrentSessionControllerImpl.java

@@ -1,312 +0,0 @@
-/* Copyright 2004, 2005 Acegi Technology Pty Limited
- *
- * 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 net.sf.acegisecurity.providers;
-
-import net.sf.acegisecurity.Authentication;
-import net.sf.acegisecurity.AuthenticationTrustResolver;
-import net.sf.acegisecurity.AuthenticationTrustResolverImpl;
-import net.sf.acegisecurity.UserDetails;
-import net.sf.acegisecurity.ui.WebAuthenticationDetails;
-import net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent;
-import org.springframework.beans.BeansException;
-import org.springframework.context.ApplicationContext;
-import org.springframework.context.ApplicationContextAware;
-import org.springframework.context.ApplicationEvent;
-import org.springframework.context.ApplicationListener;
-
-import javax.servlet.http.HttpSession;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-
-/**
- * Used by the {@link ProviderManager} to track Authentications and their
- * respective sessions. A given user is allowed {@link #setMaxSessions(int)}
- * sessions. If they attempt to exceed that ammount a {@link
- * ConcurrentLoginException} will be thrown. The
- * ConcurrentSessionControllerImpl class will listen for {@link
- * HttpSessionDestroyedEvent}s in the ApplicationContext to remove a session
- * from the internal tracking. <b>This class will not function properly
- * without a {@link net.sf.acegisecurity.ui.session.HttpSessionEventPublisher}
- * configured in web.xml.</b>
- *
- * @author Ray Krueger
- * @author Ben Alex
- */
-public class ConcurrentSessionControllerImpl
-        implements ConcurrentSessionController, ApplicationListener,
-        ApplicationContextAware {
-    //~ Instance fields ========================================================
-
-    protected Map principalsToSessions = new HashMap();
-    protected Map sessionsToPrincipals = new HashMap();
-    protected Set sessionSet = new HashSet();
-    private ApplicationContext applicationContext;
-    private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
-    private int maxSessions = 1;
-
-    //~ Methods ================================================================
-
-    public void setApplicationContext(ApplicationContext applicationContext)
-            throws BeansException {
-        this.applicationContext = applicationContext;
-    }
-
-    public ApplicationContext getApplicationContext() {
-        return applicationContext;
-    }
-
-    /**
-     * Set the maximum number of sessions a user is allowed to have, defaults
-     * to 1. Setting this to anything less than 1 will allow unlimited
-     * sessions
-     *
-     * @param maxSessions
-     */
-    public void setMaxSessions(int maxSessions) {
-        this.maxSessions = maxSessions;
-    }
-
-    /**
-     * The maximum sessions per user.
-     *
-     * @return int
-     */
-    public int getMaxSessions() {
-        return maxSessions;
-    }
-
-    /**
-     * The trustResolver to use for determining Anonymous users and ignoring
-     * them. Defaults to {@link AuthenticationTrustResolverImpl}
-     *
-     * @param trustResolver
-     */
-    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
-        this.trustResolver = trustResolver;
-    }
-
-    /**
-     * Get the configured AuthenticationTrustResolver
-     *
-     * @return The configured AuthenticationTrustResolver or {@link
-     *         AuthenticationTrustResolverImpl} by default.
-     */
-    public AuthenticationTrustResolver getTrustResolver() {
-        return trustResolver;
-    }
-
-    /**
-     * Called by the {@link ProviderManager} after receiving a response from a
-     * configured AuthenticationProvider.
-     *
-     * @param request  Used to retieve the {@link WebAuthenticationDetails}
-     * @param response Used to store the sessionId for the current Principal
-     * @throws ConcurrentLoginException If the user is already logged in the
-     *                                  maximum number of times
-     * @see #determineSessionPrincipal(net.sf.acegisecurity.Authentication)
-     */
-    public void afterAuthentication(Authentication request,
-                                    Authentication response) throws ConcurrentLoginException {
-        enforceConcurrentLogins(response);
-
-        if (request.getDetails() instanceof WebAuthenticationDetails) {
-            String sessionId = ((WebAuthenticationDetails) request.getDetails())
-                    .getSessionId();
-            addSession(determineSessionPrincipal(response), sessionId);
-        }
-    }
-
-    /**
-     * Called by the {@link ProviderManager} before iterating the configured
-     * {@link AuthenticationProvider}s
-     *
-     * @param request The Authentication in question
-     * @throws ConcurrentLoginException If the user is already logged in the
-     *                                  maximum number of times #setMaxSessions(int)}
-     */
-    public void beforeAuthentication(Authentication request)
-            throws ConcurrentLoginException {
-        enforceConcurrentLogins(request);
-    }
-
-    /**
-     * Checks for {@link HttpSessionDestroyedEvent}s and calls {@link
-     * #removeSession(String)} for the destoyed HttpSessions id.
-     *
-     * @param event
-     */
-    public void onApplicationEvent(ApplicationEvent event) {
-        if (event instanceof HttpSessionDestroyedEvent) {
-            String sessionId = ((HttpSession) event.getSource()).getId();
-            removeSession(sessionId);
-        }
-    }
-
-    /**
-     * Compares the sessionIds stored for the given principal to determine if
-     * the given sessionId is new or existing.
-     *
-     * @param principal The principal in question
-     * @param sessionId The new or existing sessionId
-     * @return true if it's the same as a session already in use, false if it
-     *         is a new session
-     */
-    protected boolean isActiveSession(Object principal, String sessionId) {
-        Set sessions = (Set) principalsToSessions.get(principal);
-
-        if (sessions == null) {
-            return false;
-        }
-
-        return sessions.contains(sessionId);
-    }
-
-    /**
-     * Updates internal maps with the sessionId for the given principal. Can be
-     * overridden by subclasses to provide a specialized means of principal
-     * -&gt; session tracking.
-     *
-     * @param principal
-     * @param sessionId
-     */
-    protected void addSession(Object principal, String sessionId) {
-        Set sessions = (Set) principalsToSessions.get(principal);
-
-        if (sessions == null) {
-            sessions = new HashSet();
-            principalsToSessions.put(principal, sessions);
-        }
-
-        sessions.add(sessionId);
-        sessionsToPrincipals.put(sessionId, principal);
-    }
-
-    /**
-     * Counts the number of sessions in use by the given principal
-     *
-     * @param principal The principal object
-     * @return 0 if there are no sessions, &gt; if there are any
-     */
-    protected int countSessions(Object principal) {
-        Set set = (Set) principalsToSessions.get(principal);
-
-        if (set == null) {
-            return 0;
-        }
-
-        return set.size();
-    }
-
-    /**
-     * Checks to see if the Authentication principal is of type UserDetails. If
-     * it is then the {@link net.sf.acegisecurity.UserDetails#getUsername()}
-     * is returned. Otherwise Authentication.getPrincipal().toString() is
-     * returned. Subclasses can override this method to provide a more
-     * specific implementation.
-     *
-     * @param auth The Authentication in question
-     * @return The principal to be used as the key against sessions
-     */
-    protected Object determineSessionPrincipal(Authentication auth) {
-        if (auth.getPrincipal() instanceof UserDetails) {
-            return ((UserDetails) auth.getPrincipal()).getUsername();
-        } else {
-            return auth.getPrincipal().toString();
-        }
-    }
-
-    /**
-     * Called by both the beforeAuthentication and afterAuthentication methods.
-     * Anonymous requests as determined by the configured {@link
-     * AuthenticationTrustResolver} are ignored. If the details are
-     * WebAuthenticationDetails, get the sessionId and and the principal off
-     * of the authentication using the {@link
-     * #determineSessionPrincipal(net.sf.acegisecurity.Authentication)}
-     * method.  Uses the sessionId and principal to determine if the session
-     * is new, and if the user is already at the maxSessions value. Subclasses
-     * may override for more specific functionality
-     *
-     * @param request Authentication being evaluated
-     * @throws ConcurrentLoginException If the session is new, and the user is
-     *                                  already at maxSessions
-     */
-    protected void enforceConcurrentLogins(Authentication request)
-            throws ConcurrentLoginException {
-        //If the max is less than 1, sessions are unlimited
-        if (maxSessions < 1) {
-            return;
-        }
-
-        //If it is an anonymous user, ignore them
-        if (trustResolver.isAnonymous(request)) {
-            return;
-        }
-
-        if (request.getDetails() instanceof WebAuthenticationDetails) {
-            String sessionId = ((WebAuthenticationDetails) request.getDetails())
-                    .getSessionId();
-
-            Object principal = determineSessionPrincipal(request);
-
-            if (!isActiveSession(principal, sessionId)) {
-                if (maxSessions == countSessions(principal)) {
-                    //Publish the event                    
-                    publishViolationEvent(request);
-
-                    //The user is AT their max, toss them out
-                    throw new ConcurrentLoginException(principal
-                            + " has reached the maximum concurrent logins");
-                }
-            }
-        }
-    }
-
-    /**
-     * Publish the even to the application context.
-     * The default action is to publish a new {@link ConcurrentSessionViolationEvent}
-     *
-     * @param auth The authentication object that caused the violation
-     */
-    protected void publishViolationEvent(Authentication auth) {
-        getApplicationContext().publishEvent(new ConcurrentSessionViolationEvent(auth));
-    }
-
-    /**
-     * Remove the given sessionId from storage. Used by {@link
-     * #onApplicationEvent(org.springframework.context.ApplicationEvent)} for
-     * HttpSessionDestroyedEvent
-     *
-     * @param sessionId
-     */
-    protected void removeSession(String sessionId) {
-        // find out which principal is associated with this sessionId
-        Object associatedPrincipal = sessionsToPrincipals.get(sessionId);
-
-        if (associatedPrincipal != null) {
-            Set sessions = (Set) principalsToSessions.get(associatedPrincipal);
-            sessions.remove(sessionId);
-
-            if (sessions.isEmpty()) {
-                principalsToSessions.remove(associatedPrincipal);
-            }
-
-            sessionsToPrincipals.remove(sessionId);
-        }
-    }
-}

+ 0 - 42
core/src/main/java/org/acegisecurity/providers/ConcurrentSessionViolationEvent.java

@@ -1,42 +0,0 @@
-/* Copyright 2004, 2005 Acegi Technology Pty Limited
- *
- * 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 net.sf.acegisecurity.providers;
-
-import net.sf.acegisecurity.Authentication;
-
-import org.springframework.context.ApplicationEvent;
-
-
-/**
- * Published by the ConcurrentSessionControllerImpl to notify the application
- * context that a user has attempted to login more than the maximum times
- * allowed by the {@link ConcurrentSessionControllerImpl#setMaxSessions(int)}
- *
- * @author Ray Krueger
- */
-public class ConcurrentSessionViolationEvent extends ApplicationEvent {
-    //~ Constructors ===========================================================
-
-    public ConcurrentSessionViolationEvent(Authentication auth) {
-        super(auth);
-    }
-
-    //~ Methods ================================================================
-
-    public Authentication getAuthentication() {
-        return (Authentication) getSource();
-    }
-}

+ 0 - 41
core/src/main/java/org/acegisecurity/providers/NullConcurrentSessionController.java

@@ -1,41 +0,0 @@
-/* Copyright 2004, 2005 Acegi Technology Pty Limited
- *
- * 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 net.sf.acegisecurity.providers;
-
-import net.sf.acegisecurity.Authentication;
-import net.sf.acegisecurity.AuthenticationException;
-
-
-/**
- * Do nothing implementation of ConcurrentSessionController. This is the {@link ProviderManager} default
- *
- * @author Ray Krueger
- * @see ConcurrentSessionControllerImpl
- */
-public class NullConcurrentSessionController
-        implements ConcurrentSessionController {
-    //~ Methods ================================================================
-
-    public void afterAuthentication(Authentication initialAuth,
-                                    Authentication result) throws AuthenticationException {
-        //Do nothing
-    }
-
-    public void beforeAuthentication(Authentication initialAuth)
-            throws AuthenticationException {
-        //Do nothing
-    }
-}

+ 4 - 2
core/src/main/java/org/acegisecurity/providers/ProviderManager.java

@@ -18,6 +18,8 @@ package net.sf.acegisecurity.providers;
 import net.sf.acegisecurity.AbstractAuthenticationManager;
 import net.sf.acegisecurity.Authentication;
 import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.concurrent.ConcurrentSessionController;
+import net.sf.acegisecurity.concurrent.NullConcurrentSessionController;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
@@ -146,7 +148,7 @@ public class ProviderManager extends AbstractAuthenticationManager
 
         Class toTest = authentication.getClass();
 
-        sessionController.beforeAuthentication(authentication);
+        sessionController.checkAuthenticationAllowed(authentication);
 
         while (iter.hasNext()) {
             AuthenticationProvider provider = (AuthenticationProvider) iter
@@ -159,7 +161,7 @@ public class ProviderManager extends AbstractAuthenticationManager
                 Authentication result = provider.authenticate(authentication);
 
                 if (result != null) {
-                    sessionController.afterAuthentication(authentication, result);
+                    sessionController.registerSuccessfulAuthentication(result);
 
                     return result;
                 }

+ 124 - 0
core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImplTests.java

@@ -0,0 +1,124 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.ui.WebAuthenticationDetails;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpSession;
+
+
+/**
+ * Tests {@link ConcurrentSessionControllerImpl}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class ConcurrentSessionControllerImplTests extends TestCase {
+    //~ Methods ================================================================
+
+    public void testLifecycle() throws Exception {
+        // Build a test fixture
+        ConcurrentSessionControllerImpl sc = new ConcurrentSessionControllerImpl();
+        SessionRegistry registry = new SessionRegistryImpl();
+        sc.setSessionRegistry(registry);
+
+        // Attempt to authenticate - it should be successful
+        Authentication auth = createAuthentication("bob", "1212");
+        sc.checkAuthenticationAllowed(auth);
+        sc.registerSuccessfulAuthentication(auth);
+
+        String sessionId1 = ((WebAuthenticationDetails) auth.getDetails())
+            .getSessionId();
+        assertFalse(registry.getSessionInformation(sessionId1).isExpired());
+
+        // Attempt to authenticate again - it should still be successful
+        sc.checkAuthenticationAllowed(auth);
+        sc.registerSuccessfulAuthentication(auth);
+
+        // Attempt to authenticate with a different session for same principal - should fail
+        sc.setExceptionIfMaximumExceeded(true);
+
+        Authentication auth2 = createAuthentication("bob", "1212");
+        assertFalse(registry.getSessionInformation(sessionId1).isExpired());
+
+        try {
+            sc.checkAuthenticationAllowed(auth2);
+            fail("Should have thrown ConcurrentLoginException");
+        } catch (ConcurrentLoginException expected) {
+            assertTrue(true);
+        }
+
+        // Attempt to authenticate with a different session for same principal - should expire first session
+        sc.setExceptionIfMaximumExceeded(false);
+
+        Authentication auth3 = createAuthentication("bob", "1212");
+        sc.checkAuthenticationAllowed(auth3);
+        sc.registerSuccessfulAuthentication(auth3);
+
+        String sessionId3 = ((WebAuthenticationDetails) auth3.getDetails())
+            .getSessionId();
+        assertTrue(registry.getSessionInformation(sessionId1).isExpired());
+        assertFalse(registry.getSessionInformation(sessionId3).isExpired());
+    }
+
+    public void testStartupDetectsInvalidMaximumSessions()
+        throws Exception {
+        ConcurrentSessionControllerImpl sc = new ConcurrentSessionControllerImpl();
+        sc.setMaximumSessions(0);
+
+        try {
+            sc.afterPropertiesSet();
+            fail("Should have thrown IAE");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testStartupDetectsInvalidSessionRegistry()
+        throws Exception {
+        ConcurrentSessionControllerImpl sc = new ConcurrentSessionControllerImpl();
+        sc.setSessionRegistry(null);
+
+        try {
+            sc.afterPropertiesSet();
+            fail("Should have thrown IAE");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    private Authentication createAuthentication(String user, String password) {
+        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user,
+                password);
+        auth.setDetails(createWebDetails(auth));
+
+        return auth;
+    }
+
+    private WebAuthenticationDetails createWebDetails(Authentication auth) {
+        MockHttpSession session = new MockHttpSession();
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setSession(session);
+        request.setUserPrincipal(auth);
+
+        return new WebAuthenticationDetails(request);
+    }
+}

+ 173 - 0
core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionFilterTests.java

@@ -0,0 +1,173 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import junit.framework.TestCase;
+
+import org.springframework.mock.web.MockFilterConfig;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockHttpSession;
+
+import java.io.IOException;
+
+import java.util.Date;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+
+/**
+ * Tests {@link ConcurrentSessionFilter}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class ConcurrentSessionFilterTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public ConcurrentSessionFilterTests() {
+        super();
+    }
+
+    public ConcurrentSessionFilterTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(ConcurrentSessionFilterTests.class);
+    }
+
+    public void testDetectsExpiredSessions() throws Exception {
+        // Setup our HTTP request
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockHttpSession session = new MockHttpSession();
+        request.setSession(session);
+
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        MockFilterConfig config = new MockFilterConfig(null);
+
+        // Setup our expectation that the filter chain will not be invoked, as we redirect to expiredUrl
+        MockFilterChain chain = new MockFilterChain(false);
+
+        // Setup our test fixture and registry to want this session to be expired
+        ConcurrentSessionFilter filter = new ConcurrentSessionFilter();
+        SessionRegistry registry = new SessionRegistryImpl();
+        registry.registerNewSession(session.getId(), "principal");
+        registry.getSessionInformation(session.getId()).expireNow();
+        filter.setSessionRegistry(registry);
+        filter.setExpiredUrl("/expired.jsp");
+
+        // Test
+        executeFilterInContainerSimulator(config, filter, request, response,
+            chain);
+
+        assertEquals("/expired.jsp", response.getRedirectedUrl());
+    }
+
+    public void testDetectsMissingExpiredUrl() throws Exception {
+        ConcurrentSessionFilter filter = new ConcurrentSessionFilter();
+        filter.setSessionRegistry(new SessionRegistryImpl());
+
+        try {
+            filter.afterPropertiesSet();
+            fail("Should have thrown IAE");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testDetectsMissingSessionRegistry() throws Exception {
+        ConcurrentSessionFilter filter = new ConcurrentSessionFilter();
+        filter.setExpiredUrl("xcx");
+
+        try {
+            filter.afterPropertiesSet();
+            fail("Should have thrown IAE");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testUpdatesLastRequestTime() throws Exception {
+        // Setup our HTTP request
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockHttpSession session = new MockHttpSession();
+        request.setSession(session);
+
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        MockFilterConfig config = new MockFilterConfig(null);
+
+        // Setup our expectation that the filter chain will be invoked, as our session hasn't expired
+        MockFilterChain chain = new MockFilterChain(true);
+
+        // Setup our test fixture
+        ConcurrentSessionFilter filter = new ConcurrentSessionFilter();
+        SessionRegistry registry = new SessionRegistryImpl();
+        registry.registerNewSession(session.getId(), "principal");
+
+        Date lastRequest = registry.getSessionInformation(session.getId())
+                                   .getLastRequest();
+        filter.setSessionRegistry(registry);
+        filter.setExpiredUrl("/expired.jsp");
+
+        Thread.sleep(1000);
+
+        // Test
+        executeFilterInContainerSimulator(config, filter, request, response,
+            chain);
+
+        assertTrue(registry.getSessionInformation(session.getId())
+                           .getLastRequest().after(lastRequest));
+    }
+
+    private void executeFilterInContainerSimulator(FilterConfig filterConfig,
+        Filter filter, ServletRequest request, ServletResponse response,
+        FilterChain filterChain) throws ServletException, IOException {
+        filter.init(filterConfig);
+        filter.doFilter(request, response, filterChain);
+        filter.destroy();
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockFilterChain implements FilterChain {
+        private boolean expectToProceed;
+
+        public MockFilterChain(boolean expectToProceed) {
+            this.expectToProceed = expectToProceed;
+        }
+
+        private MockFilterChain() {
+            super();
+        }
+
+        public void doFilter(ServletRequest request, ServletResponse response)
+            throws IOException, ServletException {
+            if (expectToProceed) {
+                assertTrue(true);
+            } else {
+                fail("Did not expect filter chain to proceed");
+            }
+        }
+    }
+}

+ 49 - 0
core/src/test/java/org/acegisecurity/concurrent/SessionInformationTests.java

@@ -0,0 +1,49 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import junit.framework.TestCase;
+
+import java.util.Date;
+
+
+/**
+ * Tests {@link SessionInformation}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class SessionInformationTests extends TestCase {
+    //~ Methods ================================================================
+
+    public void testObject() throws Exception {
+        Object principal = "Some principal object";
+        String sessionId = "1234567890";
+        Date currentDate = new Date();
+
+        SessionInformation info = new SessionInformation(principal, sessionId,
+                currentDate);
+        assertEquals(principal, info.getPrincipal());
+        assertEquals(sessionId, info.getSessionId());
+        assertEquals(currentDate, info.getLastRequest());
+
+        Thread.sleep(1000);
+
+        info.refreshLastRequest();
+
+        assertTrue(info.getLastRequest().after(currentDate));
+    }
+}

+ 155 - 0
core/src/test/java/org/acegisecurity/concurrent/SessionRegistryImplTests.java

@@ -0,0 +1,155 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.concurrent;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent;
+
+import org.springframework.mock.web.MockHttpSession;
+
+import java.util.Date;
+
+
+/**
+ * Tests {@link SessionRegistryImpl}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class SessionRegistryImplTests extends TestCase {
+    //~ Methods ================================================================
+
+    public void testEventPublishing() {
+        MockHttpSession httpSession = new MockHttpSession();
+        Object principal = "Some principal object";
+        String sessionId = httpSession.getId();
+        assertNotNull(sessionId);
+
+        SessionRegistryImpl sessionRegistry = new SessionRegistryImpl();
+
+        // Register new Session
+        sessionRegistry.registerNewSession(sessionId, principal);
+
+        // Deregister session via an ApplicationEvent
+        sessionRegistry.onApplicationEvent(new HttpSessionDestroyedEvent(
+                httpSession));
+
+        // Check attempts to retrieve cleared session return null
+        assertNull(sessionRegistry.getSessionInformation(sessionId));
+    }
+
+    public void testSessionInformationLifecycle() throws Exception {
+        Object principal = "Some principal object";
+        String sessionId = "1234567890";
+        SessionRegistryImpl sessionRegistry = new SessionRegistryImpl();
+
+        // Register new Session
+        sessionRegistry.registerNewSession(sessionId, principal);
+
+        // Retrieve existing session by session ID
+        Date currentDateTime = sessionRegistry.getSessionInformation(sessionId)
+                                              .getLastRequest();
+        assertEquals(principal,
+            sessionRegistry.getSessionInformation(sessionId).getPrincipal());
+        assertEquals(sessionId,
+            sessionRegistry.getSessionInformation(sessionId).getSessionId());
+        assertNotNull(sessionRegistry.getSessionInformation(sessionId)
+                                     .getLastRequest());
+
+        // Retrieve existing session by principal
+        assertEquals(1, sessionRegistry.getAllSessions(principal).length);
+
+        // Sleep to ensure SessionRegistryImpl will update time
+        Thread.sleep(1000);
+
+        // Update request date/time
+        sessionRegistry.refreshLastRequest(sessionId);
+
+        Date retrieved = sessionRegistry.getSessionInformation(sessionId)
+                                        .getLastRequest();
+        assertTrue(retrieved.after(currentDateTime));
+
+        // Check it retrieves correctly when looked up via principal
+        assertEquals(retrieved,
+            sessionRegistry.getAllSessions(principal)[0].getLastRequest());
+
+        // Clear session information
+        sessionRegistry.removeSessionInformation(sessionId);
+
+        // Check attempts to retrieve cleared session return null
+        assertNull(sessionRegistry.getSessionInformation(sessionId));
+        assertNull(sessionRegistry.getAllSessions(principal));
+    }
+
+    public void testTwoSessionsOnePrincipalHandling() throws Exception {
+        Object principal = "Some principal object";
+        String sessionId1 = "1234567890";
+        String sessionId2 = "9876543210";
+        SessionRegistryImpl sessionRegistry = new SessionRegistryImpl();
+
+        // Register new Session
+        sessionRegistry.registerNewSession(sessionId1, principal);
+        assertEquals(1, sessionRegistry.getAllSessions(principal).length);
+        assertEquals(sessionId1,
+            sessionRegistry.getAllSessions(principal)[0].getSessionId());
+
+        // Register new Session
+        sessionRegistry.registerNewSession(sessionId2, principal);
+        assertEquals(2, sessionRegistry.getAllSessions(principal).length);
+        assertEquals(sessionId2,
+            sessionRegistry.getAllSessions(principal)[1].getSessionId());
+
+        // Clear session information
+        sessionRegistry.removeSessionInformation(sessionId1);
+        assertEquals(1, sessionRegistry.getAllSessions(principal).length);
+        assertEquals(sessionId2,
+            sessionRegistry.getAllSessions(principal)[0].getSessionId());
+
+        // Clear final session
+        sessionRegistry.removeSessionInformation(sessionId2);
+        assertNull(sessionRegistry.getSessionInformation(sessionId2));
+        assertNull(sessionRegistry.getAllSessions(principal));
+    }
+
+    public void testTwoSessionsOnePrincipalExpiring() throws Exception {
+        Object principal = "Some principal object";
+        String sessionId1 = "1234567890";
+        String sessionId2 = "9876543210";
+        SessionRegistryImpl sessionRegistry = new SessionRegistryImpl();
+
+        // Register new Session
+        sessionRegistry.registerNewSession(sessionId1, principal);
+        assertEquals(1, sessionRegistry.getAllSessions(principal).length);
+        assertEquals(sessionId1,
+            sessionRegistry.getAllSessions(principal)[0].getSessionId());
+
+        // Register new Session
+        sessionRegistry.registerNewSession(sessionId2, principal);
+        assertEquals(2, sessionRegistry.getAllSessions(principal).length);
+        assertEquals(sessionId2,
+            sessionRegistry.getAllSessions(principal)[1].getSessionId());
+
+        // Expire one session
+        SessionInformation session = sessionRegistry.getSessionInformation(sessionId2);
+        session.expireNow();
+        
+        // Check retrieval still correct
+        assertTrue(sessionRegistry.getSessionInformation(sessionId2).isExpired());
+        assertFalse(sessionRegistry.getSessionInformation(sessionId1).isExpired());
+    }
+
+}

+ 0 - 280
core/src/test/java/org/acegisecurity/providers/ConcurrentSessionControllerImplTests.java

@@ -1,280 +0,0 @@
-/* Copyright 2004, 2005 Acegi Technology Pty Limited
- *
- * 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 net.sf.acegisecurity.providers;
-
-import junit.framework.TestCase;
-
-
-import net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
-import net.sf.acegisecurity.providers.dao.User;
-import net.sf.acegisecurity.ui.WebAuthenticationDetails;
-import net.sf.acegisecurity.ui.session.HttpSessionCreatedEvent;
-import net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent;
-import net.sf.acegisecurity.GrantedAuthority;
-import net.sf.acegisecurity.GrantedAuthorityImpl;
-import net.sf.acegisecurity.Authentication;
-import net.sf.acegisecurity.UserDetails;
-import net.sf.acegisecurity.AuthenticationTrustResolverImpl;
-import net.sf.acegisecurity.MockApplicationContext;
-
-import org.springframework.context.ApplicationListener;
-import org.springframework.mock.web.MockHttpSession;
-import org.springframework.mock.web.MockHttpServletRequest;
-
-import java.security.Principal;
-
-
-/**
- * Tests for {@link ConcurrentSessionControllerImpl}
- *
- * @author Ray Krueger
- * @author Luke Taylor
- * @version $Id$
- */
-public class ConcurrentSessionControllerImplTests extends TestCase {
-    //~ Instance fields ========================================================
-
-    ConcurrentSessionControllerImpl target;
-
-    //~ Methods ================================================================
-
-    public void testAnonymous() throws Exception {
-        AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken("blah",
-                "anon",
-                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ANON")});
-        target.beforeAuthentication(auth);
-        target.afterAuthentication(auth, auth);
-    }
-
-    public void testBumpCoverage() throws Exception {
-        target.onApplicationEvent(new HttpSessionCreatedEvent(
-                new MockHttpSession()));
-    }
-
-    public void testEnforcementKnownGood() throws Exception {
-        Authentication auth = createAuthentication("user", "password");
-        target.beforeAuthentication(auth);
-        target.afterAuthentication(auth, auth);
-    }
-
-    public void testEnforcementMultipleSessions() throws Exception {
-        target.setMaxSessions(5);
-
-        Authentication auth = null;
-
-        for (int i = 0; i < 5; i++) {  // creates 5 sessions
-            auth = createAuthentication("user", "password");
-            target.beforeAuthentication(auth);
-            target.afterAuthentication(auth, auth);
-        }
-
-        try {
-            auth = createAuthentication("user", "password");
-            target.beforeAuthentication(auth);
-            fail(
-                "Only allowed 5 sessions, this should have thrown a ConcurrentLoginException");
-        } catch (ConcurrentLoginException e) {
-            assertTrue(e.getMessage().startsWith(auth.getPrincipal().toString()));
-        }
-    }
-
-    public void testEnforcementSingleSession() throws Exception {
-        target.setMaxSessions(1);
-
-        Authentication auth = createAuthentication("user", "password");
-
-        target.beforeAuthentication(auth);
-        target.afterAuthentication(auth, auth);
-
-        try {
-            target.beforeAuthentication(createAuthentication("user", "password"));
-            fail(
-                "Only allowed 1 session, this should have thrown a ConcurrentLoginException");
-        } catch (ConcurrentLoginException e) {}
-    }
-
-    public void testEnforcementUnlimitedSameSession() throws Exception {
-        target.setMaxSessions(1);
-        MockHttpSession session = new MockHttpSession(); // all requests are within this session
-
-        for (int i = 0; i < 100; i++) {
-            UsernamePasswordAuthenticationToken auth =  new UsernamePasswordAuthenticationToken("user",
-                    "password");
-            MockHttpServletRequest request = new MockHttpServletRequest();
-            request.setSession(session);
-            request.setUserPrincipal(auth);
-            auth.setDetails(new WebAuthenticationDetails(request));
-            target.beforeAuthentication(auth);
-            target.afterAuthentication(auth, auth);
-        }
-    }
-
-    public void testEnforcementUnlimitedSessions() throws Exception {
-        target.setMaxSessions(0);
-
-        for (int i = 0; i < 100; i++) {
-            Authentication auth = createAuthentication("user", "password");
-            target.beforeAuthentication(auth);
-            target.afterAuthentication(auth, auth);
-        }
-    }
-
-    public void testEventHandler() throws Exception {
-        target.setMaxSessions(1);
-
-        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("user",
-                "password");
-        MockHttpSession session = new MockHttpSession();
-        MockHttpServletRequest request = new MockHttpServletRequest();
-        request.setSession(session);
-        request.setUserPrincipal(auth);
-        auth.setDetails(new WebAuthenticationDetails(request));
-
-        target.beforeAuthentication(auth);
-        target.afterAuthentication(auth, auth);
-
-        target.onApplicationEvent(new HttpSessionDestroyedEvent(session));
-
-        Authentication different = createAuthentication("user", "password");
-        target.beforeAuthentication(different);
-        target.afterAuthentication(different, different);
-    }
-
-    public void testEventObject() {
-        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user",
-                "password");
-        ConcurrentSessionViolationEvent ev = new ConcurrentSessionViolationEvent(token);
-        assertEquals("The token that went in should be the token that comes out",
-            token, ev.getAuthentication());
-    }
-
-    public void testImplementsApplicationListener() throws Exception {
-        assertTrue("This class must implement ApplicationListener, and at one point it didn't.",
-            target instanceof ApplicationListener);
-    }
-
-    public void testNonWebDetails() throws Exception {
-        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("asdf",
-                "asdf");
-        auth.setDetails("Hi there");
-        target.beforeAuthentication(auth);
-        target.afterAuthentication(auth, auth);
-    }
-
-    public void testPrincipals() throws Exception {
-        target.setMaxSessions(1);
-
-        final UserDetails user = new User("user", "password", true, true, true,
-                true, new GrantedAuthority[0]);
-        final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user,
-                "password", user.getAuthorities());
-        auth.setDetails(createWebDetails(auth));
-
-        target.beforeAuthentication(auth);
-        target.afterAuthentication(auth, auth);
-
-        try {
-            UsernamePasswordAuthenticationToken otherAuth = new UsernamePasswordAuthenticationToken(new Principal() {
-                        public String getName() {
-                            return "user";
-                        }
-
-                        public String toString() {
-                            return getName();
-                        }
-                    }, "password");
-
-            otherAuth.setDetails(createWebDetails(otherAuth));
-            target.beforeAuthentication(otherAuth);
-            fail(
-                "Same principal, different principal type, different session should have thrown ConcurrentLoginException");
-        } catch (ConcurrentLoginException e) {}
-    }
-
-    public void testSetMax() throws Exception {
-        target.setMaxSessions(1);
-        assertEquals(1, target.getMaxSessions());
-
-        target.setMaxSessions(2);
-        assertEquals(2, target.getMaxSessions());
-    }
-
-    public void testSetTrustManager() throws Exception {
-        assertNotNull("There is supposed to be a default AuthenticationTrustResolver",
-            target.getTrustResolver());
-
-        AuthenticationTrustResolverImpl impl = new AuthenticationTrustResolverImpl();
-        target.setTrustResolver(impl);
-        assertEquals(impl, target.getTrustResolver());
-    }
-
-    public void testUtilityMethods() throws Exception {
-        Object key = new Object();
-
-        target.addSession(key, "1");
-        target.addSession(key, "2");
-        target.addSession(key, "3");
-
-        target.removeSession("2");
-
-        assertFalse(target.isActiveSession(key, "2"));
-        assertTrue(target.isActiveSession(key, "1"));
-        assertTrue(target.isActiveSession(key, "3"));
-
-        assertNull(target.sessionsToPrincipals.get("2"));
-
-        assertEquals(2, target.countSessions(key));
-        target.addSession(key, "2");
-        assertEquals(3, target.countSessions(key));
-
-        target.addSession(key, "2");
-        target.addSession(key, "2");
-        assertEquals(3, target.countSessions(key));
-
-        assertTrue(target.isActiveSession(key, "1"));
-        assertTrue(target.isActiveSession(key, "2"));
-        assertTrue(target.isActiveSession(key, "3"));
-
-        assertFalse(target.isActiveSession(key, "nope"));
-
-        assertFalse(target.isActiveSession(new Object(), "1"));
-        assertFalse(target.isActiveSession(new Object(), "1"));
-
-        target.removeSession("nothing to see here");
-    }
-
-    protected void setUp() throws Exception {
-        target = new ConcurrentSessionControllerImpl();
-        target.setApplicationContext(MockApplicationContext.getContext());
-    }
-
-    private Authentication createAuthentication(String user, String password) {
-        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user,
-                password);
-        auth.setDetails(createWebDetails(auth));
-
-        return auth;
-    }
-
-    private WebAuthenticationDetails createWebDetails(Authentication auth) {
-        MockHttpSession session = new MockHttpSession();
-        MockHttpServletRequest request = new MockHttpServletRequest();
-        request.setSession(session);
-        request.setUserPrincipal(auth);
-
-        return new WebAuthenticationDetails(request);
-    }
-}

+ 2 - 0
core/src/test/java/org/acegisecurity/providers/ProviderManagerTests.java

@@ -18,6 +18,8 @@ package net.sf.acegisecurity.providers;
 import junit.framework.TestCase;
 
 import net.sf.acegisecurity.*;
+import net.sf.acegisecurity.concurrent.ConcurrentSessionControllerImpl;
+import net.sf.acegisecurity.concurrent.NullConcurrentSessionController;
 
 import java.util.List;
 import java.util.Vector;