1
0
mirror of synced 2026-08-04 17:27:13 +00:00

SEC-1229: Redesign Concurrent Session Control implementation. Added ConcurrentSessionControlAuthenticatedSessionStrategy

This commit is contained in:
Luke Taylor
2009-08-27 10:43:01 +00:00
parent ab0d66071a
commit 471206a29d
13 changed files with 377 additions and 198 deletions
@@ -202,6 +202,7 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
// return immediately as subclass has indicated that it hasn't completed authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (AuthenticationException failed) {
// Authentication failed
@@ -291,8 +292,6 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
SecurityContextHolder.getContext().setAuthentication(authResult);
sessionStrategy.onAuthenticationSuccess(authResult, request, response);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
@@ -394,9 +393,9 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
}
/**
* The session handling strategy which will be invoked when an authentication request is
* successfully processed. Used, for example, to handle changing of the session identifier to prevent session
* fixation attacks.
* The session handling strategy which will be invoked immediately after an authentication request is
* successfully processed by the <tt>AuthenticationManager</tt>. Used, for example, to handle changing of the
* session identifier to prevent session fixation attacks.
*
* @param sessionStrategy the implementation to use. If not set a null implementation is
* used.
@@ -29,6 +29,8 @@ import org.springframework.security.authentication.concurrent.SessionInformation
import org.springframework.security.authentication.concurrent.SessionRegistry;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.util.UrlUtils;
@@ -59,6 +61,7 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
private SessionRegistry sessionRegistry;
private String expiredUrl;
private LogoutHandler[] handlers = new LogoutHandler[] {new SecurityContextLogoutHandler()};
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
//~ Methods ========================================================================================================
@@ -87,8 +90,7 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
String targetUrl = determineExpiredUrl(request, info);
if (targetUrl != null) {
targetUrl = request.getContextPath() + targetUrl;
response.sendRedirect(response.encodeRedirectURL(targetUrl));
redirectStrategy.sendRedirect(request, response, targetUrl);
} else {
response.getWriter().print("This session has been expired (possibly due to multiple concurrent " +
"logins being attempted as the same user).");
@@ -130,4 +132,8 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
Assert.notNull(handlers);
this.handlers = handlers;
}
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
}
@@ -4,22 +4,26 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
/**
* Allows pluggable support for Http session-related behaviour when an authentication occurs.
* <p>
* Typical use would be to make sure a session exists or to change the session Id to guard against session-fixation
* Typical use would be to make sure a session exists or to change the session Id to guard against session-fixation
* attacks.
*
*
* @author Luke Taylor
* @version $Id$
* @since
*/
public interface AuthenticatedSessionStrategy {
/**
* Performs Http session-related functionality when a new authentication occurs.
*
* @throws AuthenticationException if it is decided that the authentication is not allowed for the session.
*/
void onAuthenticationSuccess(Authentication authentication, HttpServletRequest request, HttpServletResponse response);
void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException;
}
@@ -0,0 +1,159 @@
package org.springframework.security.web.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.authentication.concurrent.ConcurrentLoginException;
import org.springframework.security.authentication.concurrent.SessionInformation;
import org.springframework.security.authentication.concurrent.SessionRegistry;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert;
/**
*
* @author Luke Taylor
* @version $Id$
* @since 3.0
*/
public class ConcurrentSessionControlAuthenticatedSessionStrategy extends DefaultAuthenticatedSessionStrategy
implements MessageSourceAware {
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 ConcurrentSessionControlAuthenticatedSessionStrategy(SessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");
super.setAlwaysCreateSession(true);
this.sessionRegistry = sessionRegistry;
}
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
checkAuthenticationAllowed(authentication, request);
// Allow the parent to create a new session if necessary
super.onAuthentication(authentication, request, response);
sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}
private void checkAuthenticationAllowed(Authentication authentication, HttpServletRequest request)
throws AuthenticationException {
final List<SessionInformation> sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
int sessionCount = sessions == null ? 0 : 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 sessionId the session ID of the present request
* @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
*
* @throws ConcurrentLoginException if the
*/
protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
SessionRegistry registry) {
if (exceptionIfMaximumExceeded || (sessions == null)) {
throw new ConcurrentLoginException(messages.getMessage("ConcurrentSessionControllerImpl.exceededAllowed",
new Object[] {new Integer(allowableSessions)},
"Maximum sessions of {0} for this principal exceeded"));
}
// Determine least recently used session, and mark it for invalidation
SessionInformation leastRecentlyUsed = null;
for (int i = 0; i < sessions.size(); i++) {
if ((leastRecentlyUsed == null)
|| sessions.get(i).getLastRequest().before(leastRecentlyUsed.getLastRequest())) {
leastRecentlyUsed = sessions.get(i);
}
}
leastRecentlyUsed.expireNow();
}
@Override
protected void onSessionChange(String originalSessionId, HttpSession newSession, Authentication auth) {
// Update the session registry
sessionRegistry.removeSessionInformation(originalSessionId);
sessionRegistry.registerNewSession(newSession.getId(), auth.getPrincipal());
}
public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) {
this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded;
}
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;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
@Override
public final void setAlwaysCreateSession(boolean alwaysCreateSession) {
if (!alwaysCreateSession) {
throw new IllegalArgumentException("Cannot set alwaysCreateSession to false when concurrent session " +
"control is required");
}
}
}
@@ -12,7 +12,6 @@ import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.concurrent.SessionRegistry;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.savedrequest.SavedRequest;
@@ -33,11 +32,9 @@ import org.springframework.security.web.savedrequest.SavedRequest;
* @version $Id$
* @since 3.0
*/
public class DefaultAuthenticatedSessionStrategy implements AuthenticatedSessionStrategy{
public class DefaultAuthenticatedSessionStrategy implements AuthenticatedSessionStrategy {
protected final Log logger = LogFactory.getLog(this.getClass());
private SessionRegistry sessionRegistry;
/**
* Indicates that the session attributes of an existing session
* should be migrated to the new session. Defaults to <code>true</code>.
@@ -65,52 +62,59 @@ public class DefaultAuthenticatedSessionStrategy implements AuthenticatedSession
* If there is no session, no action is taken unless the <tt>alwaysCreateSession</tt> property is set, in which
* case a session will be created if one doesn't already exist.
*/
public void onAuthenticationSuccess(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
if (request.getSession(false) == null) {
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
boolean hadSessionAlready = request.getSession(false) != null;
if (!hadSessionAlready && !alwaysCreateSession) {
// Session fixation isn't a problem if there's no session
if (alwaysCreateSession) {
request.getSession();
}
return;
}
// Create new session
// Create new session if necessary
HttpSession session = request.getSession();
String originalSessionId = session.getId();
if (hadSessionAlready) {
// We need to migrate to a new session
String originalSessionId = session.getId();
if (logger.isDebugEnabled()) {
logger.debug("Invalidating session with Id '" + originalSessionId +"' " + (migrateSessionAttributes ?
"and" : "without") + " migrating attributes.");
}
if (logger.isDebugEnabled()) {
logger.debug("Invalidating session with Id '" + originalSessionId +"' " + (migrateSessionAttributes ?
"and" : "without") + " migrating attributes.");
}
HashMap<String, Object> attributesToMigrate = createMigratedAttributeMap(session);
HashMap<String, Object> attributesToMigrate = createMigratedAttributeMap(session);
session.invalidate();
session = request.getSession(true); // we now have a new session
session.invalidate();
session = request.getSession(true); // we now have a new session
if (logger.isDebugEnabled()) {
logger.debug("Started new session: " + session.getId());
}
if (logger.isDebugEnabled()) {
logger.debug("Started new session: " + session.getId());
}
if (originalSessionId.equals(session.getId())) {
logger.warn("Your servlet container did not change the session ID when a new session was created. You will" +
" not be adequately protected against session-fixation attacks");
}
if (originalSessionId.equals(session.getId())) {
logger.warn("Your servlet container did not change the session ID when a new session was created. You will" +
" not be adequately protected against session-fixation attacks");
}
// Copy attributes to new session
if (attributesToMigrate != null) {
for (Map.Entry<String, Object> entry : attributesToMigrate.entrySet()) {
session.setAttribute(entry.getKey(), entry.getValue());
// Copy attributes to new session
if (attributesToMigrate != null) {
for (Map.Entry<String, Object> entry : attributesToMigrate.entrySet()) {
session.setAttribute(entry.getKey(), entry.getValue());
}
}
}
}
// Update the session registry
if (sessionRegistry != null) {
sessionRegistry.removeSessionInformation(originalSessionId);
sessionRegistry.registerNewSession(session.getId(), authentication.getPrincipal());
}
/**
* Called when the session has been changed and the old attributes have been migrated to the new session.
* Only called if a session existed to start with. Allows subclasses to plug in additional behaviour.
*
* @param originalSessionId the original session identifier
* @param newSession the newly created session
* @param auth the token for the newly authenticated principal
*/
protected void onSessionChange(String originalSessionId, HttpSession newSession, Authentication auth) {
}
@SuppressWarnings("unchecked")
@@ -146,16 +150,6 @@ public class DefaultAuthenticatedSessionStrategy implements AuthenticatedSession
this.migrateSessionAttributes = migrateSessionAttributes;
}
/**
* Sets the session registry which should be updated when the authenticated session is changed.
* This must be set if you are using concurrent session control.
*
* @param sessionRegistry
*/
public void setSessionRegistry(SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
}
public void setRetainedAttributes(List<String> retainedAttributes) {
this.retainedAttributes = retainedAttributes;
}
@@ -13,7 +13,7 @@ import org.springframework.security.core.Authentication;
*/
public final class NullAuthenticatedSessionStrategy implements AuthenticatedSessionStrategy {
public void onAuthenticationSuccess(Authentication authentication, HttpServletRequest request,
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
}
}
@@ -13,6 +13,8 @@ import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
@@ -32,17 +34,15 @@ import org.springframework.web.filter.GenericFilterBean;
public class SessionManagementFilter extends GenericFilterBean {
//~ Static fields/initializers =====================================================================================
static final String FILTER_APPLIED = "__spring_security_session_fixation_filter_applied";
static final String FILTER_APPLIED = "__spring_security_session_mgmt_filter_applied";
//~ Instance fields ================================================================================================
private final SecurityContextRepository securityContextRepository;
private AuthenticatedSessionStrategy sessionStrategy = new DefaultAuthenticatedSessionStrategy();
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
private String invalidSessionUrl;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public SessionManagementFilter(SecurityContextRepository securityContextRepository) {
this.securityContextRepository = securityContextRepository;
@@ -65,12 +65,12 @@ public class SessionManagementFilter extends GenericFilterBean {
if (authentication != null && !authenticationTrustResolver.isAnonymous(authentication)) {
// The user has been authenticated during the current request, so call the session strategy
sessionStrategy.onAuthenticationSuccess(authentication, request, response);
sessionStrategy.onAuthentication(authentication, request, response);
} else {
// No security context or authentication present. Check for a session timeout
if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
if (invalidSessionUrl != null) {
response.sendRedirect(invalidSessionUrl);
redirectStrategy.sendRedirect(request, response, invalidSessionUrl);
}
}
}
@@ -99,4 +99,8 @@ public class SessionManagementFilter extends GenericFilterBean {
public void setInvalidSessionUrl(String invalidSessionUrl) {
this.invalidSessionUrl = invalidSessionUrl;
}
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
}
@@ -25,22 +25,22 @@ public class DefaultAuthenticatedSessionStrategyTests {
DefaultAuthenticatedSessionStrategy strategy = new DefaultAuthenticatedSessionStrategy();
HttpServletRequest request = new MockHttpServletRequest();
strategy.onAuthenticationSuccess(mock(Authentication.class), request, new MockHttpServletResponse());
strategy.onAuthentication(mock(Authentication.class), request, new MockHttpServletResponse());
assertNull(request.getSession(false));
}
@Test
public void newSessionIsCreatedIfSessionAlreadyExists() throws Exception {
DefaultAuthenticatedSessionStrategy strategy = new DefaultAuthenticatedSessionStrategy();
strategy.setSessionRegistry(mock(SessionRegistry.class));
HttpServletRequest request = new MockHttpServletRequest();
String sessionId = request.getSession().getId();
strategy.onAuthenticationSuccess(mock(Authentication.class), request, new MockHttpServletResponse());
assertFalse(sessionId.equals(request.getSession().getId()));
}
// @Test
// public void newSessionIsCreatedIfSessionAlreadyExists() throws Exception {
// DefaultAuthenticatedSessionStrategy strategy = new DefaultAuthenticatedSessionStrategy();
// strategy.setSessionRegistry(mock(SessionRegistry.class));
// HttpServletRequest request = new MockHttpServletRequest();
// String sessionId = request.getSession().getId();
//
// strategy.onAuthentication(mock(Authentication.class), request, new MockHttpServletResponse());
//
// assertFalse(sessionId.equals(request.getSession().getId()));
// }
// See SEC-1077
@Test
@@ -52,7 +52,7 @@ public class DefaultAuthenticatedSessionStrategyTests {
session.setAttribute("blah", "blah");
session.setAttribute(SavedRequest.SPRING_SECURITY_SAVED_REQUEST_KEY, "SavedRequest");
strategy.onAuthenticationSuccess(mock(Authentication.class), request, new MockHttpServletResponse());
strategy.onAuthentication(mock(Authentication.class), request, new MockHttpServletResponse());
assertNull(request.getSession().getAttribute("blah"));
assertNotNull(request.getSession().getAttribute(SavedRequest.SPRING_SECURITY_SAVED_REQUEST_KEY));
@@ -62,7 +62,9 @@ public class DefaultAuthenticatedSessionStrategyTests {
public void sessionIsCreatedIfAlwaysCreateTrue() throws Exception {
DefaultAuthenticatedSessionStrategy strategy = new DefaultAuthenticatedSessionStrategy();
strategy.setAlwaysCreateSession(true);
HttpServletRequest request = new MockHttpServletRequest();
strategy.onAuthentication(mock(Authentication.class), request, new MockHttpServletResponse());
assertNotNull(request.getSession(false));
}
}
@@ -82,7 +82,7 @@ public class SessionManagementFilterTests {
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain());
verify(strategy).onAuthenticationSuccess(any(Authentication.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
verify(strategy).onAuthentication(any(Authentication.class), any(HttpServletRequest.class), any(HttpServletResponse.class));
// Check that it is only applied once to the request
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain());
verifyNoMoreInteractions(strategy);