diff --git a/config/src/main/java/org/springframework/security/config/http/FormLoginBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/FormLoginBeanDefinitionParser.java index 5dc8dbd74d..d01faac026 100644 --- a/config/src/main/java/org/springframework/security/config/http/FormLoginBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/FormLoginBeanDefinitionParser.java @@ -40,18 +40,21 @@ public class FormLoginBeanDefinitionParser { private final String defaultLoginProcessingUrl; private final String filterClassName; private final BeanReference requestCache; + private final BeanReference sessionStrategy; private RootBeanDefinition filterBean; private RootBeanDefinition entryPointBean; private String loginPage; - FormLoginBeanDefinitionParser(String defaultLoginProcessingUrl, String filterClassName, BeanReference requestCache) { + FormLoginBeanDefinitionParser(String defaultLoginProcessingUrl, String filterClassName, + BeanReference requestCache, BeanReference sessionStrategy) { this.defaultLoginProcessingUrl = defaultLoginProcessingUrl; this.filterClassName = filterClassName; this.requestCache = requestCache; + this.sessionStrategy = sessionStrategy; } - public BeanDefinition parse(Element elt, ParserContext pc, RootBeanDefinition sfpf) { + public BeanDefinition parse(Element elt, ParserContext pc) { String loginUrl = null; String defaultTargetUrl = null; String authenticationFailureUrl = null; @@ -84,12 +87,6 @@ public class FormLoginBeanDefinitionParser { successHandlerRef, failureHandlerRef); filterBean.setSource(source); - // Copy session migration values from the session fixation protection filter - if (sfpf != null) { - filterBean.getPropertyValues().addPropertyValue("migrateInvalidatedSessionAttributes", sfpf.getPropertyValues().getPropertyValue("migrateSessionAttributes").getValue()); - filterBean.getPropertyValues().addPropertyValue("invalidateSessionOnSuccessfulAuthentication", Boolean.TRUE); - } - BeanDefinitionBuilder entryPointBuilder = BeanDefinitionBuilder.rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class); entryPointBuilder.getRawBeanDefinition().setSource(source); @@ -122,6 +119,10 @@ public class FormLoginBeanDefinitionParser { filterBuilder.addPropertyValue("authenticationSuccessHandler", successHandler.getBeanDefinition()); } + if (sessionStrategy != null) { + filterBuilder.addPropertyValue("authenticatedSessionStrategy", sessionStrategy); + } + if (StringUtils.hasText(failureHandlerRef)) { filterBuilder.addPropertyReference("authenticationFailureHandler", failureHandlerRef); } else { diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java index 2b186f8b6a..d0b4667fbd 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java @@ -66,6 +66,7 @@ import org.springframework.security.web.context.HttpSessionSecurityContextReposi import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; +import org.springframework.security.web.session.DefaultAuthenticatedSessionStrategy; import org.springframework.security.web.session.SessionFixationProtectionFilter; import org.springframework.security.web.util.AntUrlPathMatcher; import org.springframework.security.web.util.RegexUrlPathMatcher; @@ -189,6 +190,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { BeanDefinition concurrentSessionFilter = createConcurrentSessionFilterAndRelatedBeansIfRequired(element, pc); BeanDefinition scpf = createSecurityContextPersistenceFilter(element, pc); + BeanReference contextRepoRef = (BeanReference) scpf.getPropertyValues().getPropertyValue("securityContextRepository").getValue(); if (concurrentSessionFilter != null) { sessionRegistryRef = (BeanReference) @@ -214,7 +216,12 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { BeanDefinition etf = createExceptionTranslationFilter(element, pc, requestCache); RootBeanDefinition sfpf = createSessionFixationProtectionFilter(pc, element.getAttribute(ATT_SESSION_FIXATION_PROTECTION), - sessionRegistryRef); + sessionRegistryRef, contextRepoRef); + BeanReference sessionStrategyRef = null; + + if (sfpf != null) { + sessionStrategyRef = (BeanReference) sfpf.getPropertyValues().getPropertyValue("authenticatedSessionStrategy").getValue(); + } BeanDefinition fsi = createFilterSecurityInterceptor(element, pc, matcher, convertPathsToLowerCase, authenticationManager); if (channelRequestMap.size() > 0) { @@ -222,16 +229,11 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { cpf = createChannelProcessingFilter(pc, matcher, channelRequestMap, portMapperName); } -// if (sfpf != null) { -// // Used by SessionRegistryinjectionPP -// pc.getRegistry().registerBeanDefinition(BeanIds.SESSION_FIXATION_PROTECTION_FILTER, sfpf); -// } - final FilterAndEntryPoint basic = createBasicFilter(element, pc, autoConfig, authenticationManager); final FilterAndEntryPoint form = createFormLoginFilter(element, pc, autoConfig, allowSessionCreation, - sfpf, authenticationManager, requestCache); + sessionStrategyRef, authenticationManager, requestCache); final FilterAndEntryPoint openID = createOpenIDLoginFilter(element, pc, autoConfig, allowSessionCreation, - sfpf, authenticationManager, requestCache); + sessionStrategyRef, authenticationManager, requestCache); String rememberMeServicesId = null; if (rememberMeFilter != null) { @@ -247,7 +249,6 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { pc.getRegistry().registerBeanDefinition(BeanIds.FORM_LOGIN_FILTER, form.filter); pc.registerBeanComponent(new BeanComponentDefinition(form.filter, BeanIds.FORM_LOGIN_FILTER)); injectRememberMeServicesRef(form.filter, rememberMeServicesId); - injectSessionRegistryRef(form.filter, sessionRegistryRef); } if (openID.filter != null) { @@ -255,7 +256,6 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { pc.getRegistry().registerBeanDefinition(BeanIds.OPEN_ID_FILTER, openID.filter); pc.registerBeanComponent(new BeanComponentDefinition(openID.filter, BeanIds.OPEN_ID_FILTER)); injectRememberMeServicesRef(openID.filter, rememberMeServicesId); - injectSessionRegistryRef(openID.filter, sessionRegistryRef); } String x509ProviderId = null; @@ -385,12 +385,6 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { } } - private void injectSessionRegistryRef(RootBeanDefinition bean, BeanReference sessionRegistryRef){ - if (sessionRegistryRef != null) { - bean.getPropertyValues().addPropertyValue("sessionRegistry", sessionRegistryRef); - } - } - private void checkFilterChainOrder(List filters, ParserContext pc, Object source) { logger.info("Checking sorted filter chain: " + filters); @@ -691,7 +685,9 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { contextRepo.addPropertyValue("disableUrlRewriting", Boolean.TRUE); } - scpf.addPropertyValue("securityContextRepository", contextRepo.getBeanDefinition()); + String id = pc.getReaderContext().registerWithGeneratedName(contextRepo.getBeanDefinition()); + + scpf.addPropertyReference("securityContextRepository", id); } return scpf.getBeanDefinition(); @@ -914,7 +910,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { } private RootBeanDefinition createSessionFixationProtectionFilter(ParserContext pc, String sessionFixationAttribute, - BeanReference sessionRegistryRef) { + BeanReference sessionRegistryRef, BeanReference contextRepoRef) { if(!StringUtils.hasText(sessionFixationAttribute)) { sessionFixationAttribute = OPT_SESSION_FIXATION_MIGRATE_SESSION; } @@ -922,18 +918,25 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { if (!sessionFixationAttribute.equals(OPT_SESSION_FIXATION_NO_PROTECTION)) { BeanDefinitionBuilder sessionFixationFilter = BeanDefinitionBuilder.rootBeanDefinition(SessionFixationProtectionFilter.class); - sessionFixationFilter.addPropertyValue("migrateSessionAttributes", + sessionFixationFilter.addConstructorArgValue(contextRepoRef); + + BeanDefinitionBuilder sessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(DefaultAuthenticatedSessionStrategy.class); + + sessionStrategy.addPropertyValue("migrateSessionAttributes", Boolean.valueOf(sessionFixationAttribute.equals(OPT_SESSION_FIXATION_MIGRATE_SESSION))); if (sessionRegistryRef != null) { - sessionFixationFilter.addPropertyValue("sessionRegistry", sessionRegistryRef); + sessionStrategy.addPropertyValue("sessionRegistry", sessionRegistryRef); } + + String id = pc.getReaderContext().registerWithGeneratedName(sessionStrategy.getBeanDefinition()); + sessionFixationFilter.addPropertyReference("authenticatedSessionStrategy", id); return (RootBeanDefinition) sessionFixationFilter.getBeanDefinition(); } return null; } private FilterAndEntryPoint createFormLoginFilter(Element element, ParserContext pc, boolean autoConfig, - boolean allowSessionCreation, RootBeanDefinition sfpf, BeanReference authManager, BeanReference requestCache) { + boolean allowSessionCreation, BeanReference sessionStrategy, BeanReference authManager, BeanReference requestCache) { RootBeanDefinition formLoginFilter = null; RootBeanDefinition formLoginEntryPoint = null; @@ -941,9 +944,9 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { if (formLoginElt != null || autoConfig) { FormLoginBeanDefinitionParser parser = new FormLoginBeanDefinitionParser("/j_spring_security_check", - AUTHENTICATION_PROCESSING_FILTER_CLASS, requestCache); + AUTHENTICATION_PROCESSING_FILTER_CLASS, requestCache, sessionStrategy); - parser.parse(formLoginElt, pc, sfpf); + parser.parse(formLoginElt, pc); formLoginFilter = parser.getFilterBean(); formLoginEntryPoint = parser.getEntryPointBean(); } @@ -957,16 +960,16 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { } private FilterAndEntryPoint createOpenIDLoginFilter(Element element, ParserContext pc, boolean autoConfig, - boolean allowSessionCreation, RootBeanDefinition sfpf, BeanReference authManager, BeanReference requestCache) { + boolean allowSessionCreation, BeanReference sessionStrategy, BeanReference authManager, BeanReference requestCache) { Element openIDLoginElt = DomUtils.getChildElementByTagName(element, Elements.OPENID_LOGIN); RootBeanDefinition openIDFilter = null; RootBeanDefinition openIDEntryPoint = null; if (openIDLoginElt != null) { FormLoginBeanDefinitionParser parser = new FormLoginBeanDefinitionParser("/j_spring_openid_security_check", - OPEN_ID_AUTHENTICATION_PROCESSING_FILTER_CLASS, requestCache); + OPEN_ID_AUTHENTICATION_PROCESSING_FILTER_CLASS, requestCache, sessionStrategy); - parser.parse(openIDLoginElt, pc, sfpf); + parser.parse(openIDLoginElt, pc); openIDFilter = parser.getFilterBean(); openIDEntryPoint = parser.getEntryPointBean(); } diff --git a/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java index d08dbf9cb4..47c6540e63 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParserTests.java @@ -647,10 +647,10 @@ public class HttpSecurityBeanDefinitionParserTests { Object sessionRegistryFromConcurrencyFilter = FieldUtils.getFieldValue( getFilter(ConcurrentSessionFilter.class),"sessionRegistry"); Object sessionRegistryFromFormLoginFilter = FieldUtils.getFieldValue( - getFilter(UsernamePasswordAuthenticationProcessingFilter.class),"sessionRegistry"); + getFilter(UsernamePasswordAuthenticationProcessingFilter.class),"sessionStrategy.sessionRegistry"); Object sessionRegistryFromController = FieldUtils.getFieldValue(getConcurrentSessionController(),"sessionRegistry"); Object sessionRegistryFromFixationFilter = FieldUtils.getFieldValue( - getFilter(SessionFixationProtectionFilter.class),"sessionRegistry"); + getFilter(SessionFixationProtectionFilter.class),"sessionStrategy.sessionRegistry"); assertSame(sessionRegistry, sessionRegistryFromConcurrencyFilter); assertSame(sessionRegistry, sessionRegistryFromController); diff --git a/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java index 02dd4f3df3..6cc22c6297 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java @@ -31,14 +31,14 @@ import org.springframework.context.MessageSourceAware; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.concurrent.SessionRegistry; import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.SpringSecurityFilter; -import org.springframework.security.web.session.SessionUtils; +import org.springframework.security.web.session.AuthenticatedSessionStrategy; +import org.springframework.security.web.session.DefaultAuthenticatedSessionStrategy; import org.springframework.security.web.util.UrlUtils; import org.springframework.util.Assert; @@ -61,7 +61,9 @@ import org.springframework.util.Assert; * * If authentication is successful, the resulting {@link Authentication} object will be placed into the * SecurityContext for the current thread, which is guaranteed to have already been created by an earlier - * filter. The configured {@link #setAuthenticationSuccessHandler(AuthenticationSuccessHandler) AuthenticationSuccessHandler} will + * filter. + *

+ * The configured {@link #setAuthenticationSuccessHandler(AuthenticationSuccessHandler) AuthenticationSuccessHandler} will * then be called to take the redirect to the appropriate destination after a successful login. The default behaviour * is implemented in a {@link SavedRequestAwareAuthenticationSuccessHandler} which will make use of any * SavedRequest set by the ExceptionTranslationFilter and redirect the user to the URL contained @@ -127,26 +129,10 @@ public abstract class AbstractAuthenticationProcessingFilter extends SpringSecur private boolean continueChainBeforeSuccessfulAuthentication = false; - /** - * Tells if we on successful authentication should invalidate the - * current session. This is a common guard against session fixation attacks. - * Defaults to false. - */ - private boolean invalidateSessionOnSuccessfulAuthentication = false; - - /** - * If {@link #invalidateSessionOnSuccessfulAuthentication} is true, this - * flag indicates that the session attributes of the session to be invalidated - * are to be migrated to the new session. Defaults to true since - * nothing will happen unless {@link #invalidateSessionOnSuccessfulAuthentication} - * is true. - */ - private boolean migrateInvalidatedSessionAttributes = true; + private AuthenticatedSessionStrategy sessionStrategy = new DefaultAuthenticatedSessionStrategy(); private boolean allowSessionCreation = true; - private SessionRegistry sessionRegistry; - private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); @@ -281,7 +267,8 @@ public abstract class AbstractAuthenticationProcessingFilter extends SpringSecur * Default behaviour for successful authentication. *

    *
  1. Sets the successful Authentication object on the {@link SecurityContextHolder}
  2. - *
  3. Performs any configured session migration behaviour
  4. + *
  5. Invokes the configured {@link AuthenticatedSessionStrategy} to handle any session-related behaviour + * (such as creating a new session to protect against session-fixation attacks).
  6. *
  7. Informs the configured RememberMeServices of the successful login
  8. *
  9. Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured * ApplicationEventPublisher
  10. @@ -299,9 +286,7 @@ public abstract class AbstractAuthenticationProcessingFilter extends SpringSecur SecurityContextHolder.getContext().setAuthentication(authResult); - if (invalidateSessionOnSuccessfulAuthentication) { - SessionUtils.startNewSessionIfRequired(request, migrateInvalidatedSessionAttributes, sessionRegistry); - } + sessionStrategy.onAuthenticationSuccess(authResult, request, response); rememberMeServices.loginSuccess(request, response, authResult); @@ -332,14 +317,10 @@ public abstract class AbstractAuthenticationProcessingFilter extends SpringSecur logger.debug("Delegating to authentication failure handler" + failureHandler); } - try { - HttpSession session = request.getSession(false); + HttpSession session = request.getSession(false); - if (session != null || allowSessionCreation) { - request.getSession().setAttribute(SPRING_SECURITY_LAST_EXCEPTION_KEY, failed); - } - } - catch (Exception ignored) { + if (session != null || allowSessionCreation) { + request.getSession().setAttribute(SPRING_SECURITY_LAST_EXCEPTION_KEY, failed); } rememberMeServices.loginFail(request, response); @@ -394,14 +375,6 @@ public abstract class AbstractAuthenticationProcessingFilter extends SpringSecur this.messages = new MessageSourceAccessor(messageSource); } - public void setInvalidateSessionOnSuccessfulAuthentication(boolean invalidateSessionOnSuccessfulAuthentication) { - this.invalidateSessionOnSuccessfulAuthentication = invalidateSessionOnSuccessfulAuthentication; - } - - public void setMigrateInvalidatedSessionAttributes(boolean migrateInvalidatedSessionAttributes) { - this.migrateInvalidatedSessionAttributes = migrateInvalidatedSessionAttributes; - } - public AuthenticationDetailsSource getAuthenticationDetailsSource() { // Required due to SEC-310 return authenticationDetailsSource; @@ -416,11 +389,15 @@ public abstract class AbstractAuthenticationProcessingFilter extends SpringSecur } /** - * The session registry needs to be set if session fixation attack protection is in use (and concurrent - * session control is enabled). + * 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. + * + * @param sessionStrategy the implementation to use. If not set a {@link DefaultAuthenticatedSessionStrategy} is + * used. */ - public void setSessionRegistry(SessionRegistry sessionRegistry) { - this.sessionRegistry = sessionRegistry; + public void setAuthenticatedSessionStrategy(AuthenticatedSessionStrategy sessionStrategy) { + this.sessionStrategy = sessionStrategy; } /** diff --git a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java index df5745418a..0077278315 100644 --- a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java @@ -111,6 +111,16 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo } } + public boolean containsContext(HttpServletRequest request) { + HttpSession session = request.getSession(false); + + if (session == null) { + return false; + } + + return session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null; + } + /** * * @param httpSession the session obtained from the request. diff --git a/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java index b90d4f438a..8952c44686 100644 --- a/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/SecurityContextRepository.java @@ -50,4 +50,13 @@ public interface SecurityContextRepository { * @param response */ void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response); + + /** + * Allows the repository to be queried as to whether it contains a security context for the + * current request. + * + * @param request the current request + * @return true if a context is found for the request, false otherwise + */ + boolean containsContext(HttpServletRequest request); } diff --git a/web/src/main/java/org/springframework/security/web/session/AuthenticatedSessionStrategy.java b/web/src/main/java/org/springframework/security/web/session/AuthenticatedSessionStrategy.java new file mode 100644 index 0000000000..1ddf1e8b7b --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/session/AuthenticatedSessionStrategy.java @@ -0,0 +1,25 @@ +package org.springframework.security.web.session; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; + +/** + * Allows pluggable support for Http session-related behaviour when an authentication occurs. + *

    + * 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. + */ + void onAuthenticationSuccess(Authentication authentication, HttpServletRequest request, HttpServletResponse response); + +} diff --git a/web/src/main/java/org/springframework/security/web/session/DefaultAuthenticatedSessionStrategy.java b/web/src/main/java/org/springframework/security/web/session/DefaultAuthenticatedSessionStrategy.java new file mode 100644 index 0000000000..ec33205b03 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/session/DefaultAuthenticatedSessionStrategy.java @@ -0,0 +1,155 @@ +package org.springframework.security.web.session; + +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.authentication.concurrent.SessionRegistry; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.savedrequest.SavedRequest; + +/** + * The default implementation of {@link AuthenticatedSessionStrategy}. + *

    + * Creates a new session for the newly authenticated user if they already have a session, and copies their + * session attributes across to the new session (can be disabled by setting migrateSessionAttributes to + * false). + *

    + * If concurrent session control is in use, then a SessionRegistry must be injected. + * + * @author Luke Taylor + * @version $Id$ + * @since 3.0 + */ +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 true. + */ + private boolean migrateSessionAttributes = true; + + /** + * In the case where the attributes will not be migrated, this field allows a list of named attributes + * which should not be discarded. + */ + private List retainedAttributes = Arrays.asList(SavedRequest.SPRING_SECURITY_SAVED_REQUEST_KEY); + + /** + * If set to true, a session will always be created, even if one didn't exist at the start of the request. + * Defaults to false. + */ + private boolean alwaysCreateSession; + + /** + * Called when a user is newly authenticated. + *

    + * If a session already exists, a new session will be created, the session attributes copied to it (if + * migrateSessionAttributes is set) and the sessionRegistry updated with the new session information. + *

    + * If there is no session, no action is taken unless the alwaysCreateSession 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) { + // Session fixation isn't a problem if there's no session + if (alwaysCreateSession) { + request.getSession(); + } + + return; + } + + // Create new session + HttpSession session = request.getSession(); + + String originalSessionId = session.getId(); + + if (logger.isDebugEnabled()) { + logger.debug("Invalidating session with Id '" + originalSessionId +"' " + (migrateSessionAttributes ? + "and" : "without") + " migrating attributes."); + } + + HashMap attributesToMigrate = createMigratedAttributeMap(session); + + session.invalidate(); + session = request.getSession(true); // we now have a new session + + if (logger.isDebugEnabled()) { + logger.debug("Started new session: " + session.getId()); + } + + // Copy attributes to new session + if (attributesToMigrate != null) { + for (Map.Entry entry : attributesToMigrate.entrySet()) { + session.setAttribute(entry.getKey(), entry.getValue()); + } + } + + // Update the session registry + if (sessionRegistry != null) { + sessionRegistry.removeSessionInformation(originalSessionId); + sessionRegistry.registerNewSession(session.getId(), + SecurityContextHolder.getContext().getAuthentication().getPrincipal()); + } + } + + @SuppressWarnings("unchecked") + private HashMap createMigratedAttributeMap(HttpSession session) { + HashMap attributesToMigrate = null; + + if (migrateSessionAttributes) { + attributesToMigrate = new HashMap(); + + Enumeration enumer = session.getAttributeNames(); + + while (enumer.hasMoreElements()) { + String key = (String) enumer.nextElement(); + attributesToMigrate.put(key, session.getAttribute(key)); + } + } else { + // Only retain the attributes which have been specified in the retainAttributes list + if (!retainedAttributes.isEmpty()) { + attributesToMigrate = new HashMap(); + for (String name : retainedAttributes) { + Object value = session.getAttribute(name); + + if (value != null) { + attributesToMigrate.put(name, value); + } + } + } + } + return attributesToMigrate; + } + + public void setMigrateSessionAttributes(boolean migrateSessionAttributes) { + 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 retainedAttributes) { + this.retainedAttributes = retainedAttributes; + } +} diff --git a/web/src/main/java/org/springframework/security/web/session/SessionFixationProtectionFilter.java b/web/src/main/java/org/springframework/security/web/session/SessionFixationProtectionFilter.java index b6d2690cb7..f35b5badc3 100644 --- a/web/src/main/java/org/springframework/security/web/session/SessionFixationProtectionFilter.java +++ b/web/src/main/java/org/springframework/security/web/session/SessionFixationProtectionFilter.java @@ -6,26 +6,25 @@ import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; -import org.springframework.security.authentication.concurrent.SessionRegistry; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.SpringSecurityFilter; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.util.Assert; /** - * Detects that a user has been authenticated since the start of the request and starts a new session. + * Detects that a user has been authenticated since the start of the request and, if they have, calls the + * configured {@link AuthenticatedSessionStrategy} to perform any session-related activity (such as + * activating session-fixation protection mechanisms). *

    * This is essentially a generalization of the functionality that was implemented for SEC-399. - * Additionally, it will update the configured SessionRegistry if one is in use, thus preventing problems when used - * with Spring Security's concurrent session control. * * @author Martin Algesten * @author Luke Taylor + * @version $Id$ * @since 2.0 */ public class SessionFixationProtectionFilter extends SpringSecurityFilter { @@ -35,59 +34,46 @@ public class SessionFixationProtectionFilter extends SpringSecurityFilter { //~ Instance fields ================================================================================================ - private SessionRegistry sessionRegistry; + private final SecurityContextRepository securityContextRepository; - /** - * Indicates that the session attributes of the session to be invalidated - * should be migrated to the new session. Defaults to true. - */ - private boolean migrateSessionAttributes = true; + private AuthenticatedSessionStrategy sessionStrategy = new DefaultAuthenticatedSessionStrategy(); private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); + public SessionFixationProtectionFilter(SecurityContextRepository securityContextRepository) { + this.securityContextRepository = securityContextRepository; + } + protected void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - // Session fixation isn't a problem if there's no session - if(request.getSession(false) == null || request.getAttribute(FILTER_APPLIED) != null) { + + if (request.getAttribute(FILTER_APPLIED) != null) { chain.doFilter(request, response); return; } request.setAttribute(FILTER_APPLIED, Boolean.TRUE); - HttpSession session = request.getSession(); - SecurityContext sessionSecurityContext = - (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + if (!securityContextRepository.containsContext(request)) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (sessionSecurityContext == null && isAuthenticated()) { - // The user has been authenticated during the current request, so do the session migration - startNewSessionIfRequired(request, response); + 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); + } } chain.doFilter(request, response); } - private boolean isAuthenticated() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - return authentication != null && !authenticationTrustResolver.isAnonymous(authentication); - } - - public void setMigrateSessionAttributes(boolean migrateSessionAttributes) { - this.migrateSessionAttributes = migrateSessionAttributes; - } - - public void setSessionRegistry(SessionRegistry sessionRegistry) { - this.sessionRegistry = sessionRegistry; - } - /** - * Called when the a user wasn't authenticated at the start of the request but has been during it - *

    - * A new session will be created, the session attributes copied to it (if - * migrateSessionAttributes is set) and the sessionRegistry updated with the new session information. + * Sets the strategy object which handles the session management behaviour when a + * user has been authenticated during the current request. + * + * @param sessionStrategy the strategy object. If not set, a {@link DefaultAuthenticatedSessionStrategy} is used. */ - protected void startNewSessionIfRequired(HttpServletRequest request, HttpServletResponse response) { - SessionUtils.startNewSessionIfRequired(request, migrateSessionAttributes, sessionRegistry); + public void setAuthenticatedSessionStrategy(AuthenticatedSessionStrategy sessionStrategy) { + Assert.notNull(sessionStrategy, "authenticatedSessionStratedy must not be null"); + this.sessionStrategy = sessionStrategy; } } diff --git a/web/src/test/java/org/springframework/security/web/authentication/AbstractProcessingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/AbstractProcessingFilterTests.java index 7f117bd591..5db5708cf4 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/AbstractProcessingFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/AbstractProcessingFilterTests.java @@ -50,6 +50,7 @@ import org.springframework.security.web.authentication.SavedRequestAwareAuthenti import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; import org.springframework.security.web.savedrequest.SavedRequest; +import org.springframework.security.web.session.AuthenticatedSessionStrategy; /** @@ -239,6 +240,7 @@ public class AbstractProcessingFilterTests extends TestCase { MockAbstractProcessingFilter filter = new MockAbstractProcessingFilter(true); filter.setFilterProcessesUrl("/j_mock_post"); + filter.setAuthenticatedSessionStrategy(mock(AuthenticatedSessionStrategy.class)); filter.setAuthenticationSuccessHandler(successHandler); filter.setAuthenticationFailureHandler(failureHandler); filter.setAuthenticationManager(mock(AuthenticationManager.class)); @@ -390,48 +392,6 @@ public class AbstractProcessingFilterTests extends TestCase { assertNotNull(SecurityContextHolder.getContext().getAuthentication()); } - public void testNewSessionIsCreatedIfInvalidateSessionOnSuccessfulAuthenticationIsSet() throws Exception { - MockHttpServletRequest request = createMockRequest(); - HttpSession oldSession = request.getSession(); - oldSession.setAttribute("test","test"); - MockFilterConfig config = new MockFilterConfig(null, null); - - MockFilterChain chain = new MockFilterChain(true); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // Setup our test object, to grant access - MockAbstractProcessingFilter filter = new MockAbstractProcessingFilter(true); - filter.setInvalidateSessionOnSuccessfulAuthentication(true); - successHandler.setDefaultTargetUrl("http://monkeymachine.co.uk/"); - filter.setAuthenticationSuccessHandler(successHandler); - - executeFilterInContainerSimulator(config, filter, request, response, chain); - - HttpSession newSession = request.getSession(); - assertFalse(newSession.getId().equals(oldSession.getId())); - assertEquals("test", newSession.getAttribute("test")); - } - - public void testAttributesAreNotMigratedToNewlyCreatedSessionIfMigrateAttributesIsFalse() throws Exception { - MockHttpServletRequest request = createMockRequest(); - HttpSession oldSession = request.getSession(); - MockFilterConfig config = new MockFilterConfig(null, null); - MockFilterChain chain = new MockFilterChain(true); - MockHttpServletResponse response = new MockHttpServletResponse(); - - MockAbstractProcessingFilter filter = new MockAbstractProcessingFilter(true); - filter.setInvalidateSessionOnSuccessfulAuthentication(true); - filter.setMigrateInvalidatedSessionAttributes(false); - successHandler.setDefaultTargetUrl("http://monkeymachine.co.uk/"); - filter.setAuthenticationSuccessHandler(successHandler); - - executeFilterInContainerSimulator(config, filter, request, response, chain); - - HttpSession newSession = request.getSession(); - assertFalse(newSession.getId().equals(oldSession.getId())); - assertNull(newSession.getAttribute("test")); - } - /** * SEC-571 */ diff --git a/web/src/test/java/org/springframework/security/web/session/DefaultAuthenticatedSessionStrategyTests.java b/web/src/test/java/org/springframework/security/web/session/DefaultAuthenticatedSessionStrategyTests.java new file mode 100644 index 0000000000..912e4862a5 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/session/DefaultAuthenticatedSessionStrategyTests.java @@ -0,0 +1,41 @@ +package org.springframework.security.web.session; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.Authentication; + +/** + * + * @author Luke Taylor + * @version $Id$ + */ +public class DefaultAuthenticatedSessionStrategyTests { + + @Test + public void newSessionShouldNotBeCreatedIfNoSessionExistsAndAlwaysCreateIsFalse() throws Exception { + DefaultAuthenticatedSessionStrategy strategy = new DefaultAuthenticatedSessionStrategy(); + HttpServletRequest request = new MockHttpServletRequest(); + + strategy.onAuthenticationSuccess(mock(Authentication.class), request, new MockHttpServletResponse()); + + assertNull(request.getSession(false)); + } + + @Test + public void newSessionIsCreatedIfSessionAlreadyExists() throws Exception { + DefaultAuthenticatedSessionStrategy strategy = new DefaultAuthenticatedSessionStrategy(); + HttpServletRequest request = new MockHttpServletRequest(); + String sessionId = request.getSession().getId(); + + strategy.onAuthenticationSuccess(mock(Authentication.class), request, new MockHttpServletResponse()); + + assertFalse(sessionId.equals(request.getSession().getId())); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/session/SessionFixationProtectionFilterTests.java b/web/src/test/java/org/springframework/security/web/session/SessionFixationProtectionFilterTests.java index 3f37f039b3..6402043e49 100644 --- a/web/src/test/java/org/springframework/security/web/session/SessionFixationProtectionFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/session/SessionFixationProtectionFilterTests.java @@ -1,8 +1,11 @@ package org.springframework.security.web.session; import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.junit.After; import org.junit.Test; @@ -10,9 +13,9 @@ import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; -import org.springframework.security.web.session.SessionFixationProtectionFilter; +import org.springframework.security.web.context.SecurityContextRepository; /** * @@ -26,32 +29,10 @@ public class SessionFixationProtectionFilterTests { SecurityContextHolder.clearContext(); } - @Test - public void newSessionShouldNotBeCreatedIfNoSessionExists() throws Exception { - SessionFixationProtectionFilter filter = new SessionFixationProtectionFilter(); - HttpServletRequest request = new MockHttpServletRequest(); - authenticateUser(); - - filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()); - - assertNull(request.getSession(false)); - } - - @Test - public void newSessionBeCreatedIfAuthenticatedOccurredDuringRequest() throws Exception { - SessionFixationProtectionFilter filter = new SessionFixationProtectionFilter(); - HttpServletRequest request = new MockHttpServletRequest(); - String sessionId = request.getSession().getId(); - authenticateUser(); - - filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()); - - assertFalse(sessionId.equals(request.getSession().getId())); - } - @Test public void newSessionShouldNotBeCreatedIfSessionExistsAndUserIsNotAuthenticated() throws Exception { - SessionFixationProtectionFilter filter = new SessionFixationProtectionFilter(); + SecurityContextRepository repo = mock(SecurityContextRepository.class); + SessionFixationProtectionFilter filter = new SessionFixationProtectionFilter(repo); HttpServletRequest request = new MockHttpServletRequest(); String sessionId = request.getSession().getId(); @@ -61,17 +42,47 @@ public class SessionFixationProtectionFilterTests { } @Test - public void newSessionShouldNotBeCreatedIfUserIsAlreadyAuthenticated() throws Exception { - SessionFixationProtectionFilter filter = new SessionFixationProtectionFilter(); + public void strategyIsNotInvokedIfSecurityContextAlreadyExistsForRequest() throws Exception { + SecurityContextRepository repo = mock(SecurityContextRepository.class); + AuthenticatedSessionStrategy strategy = mock(AuthenticatedSessionStrategy.class); + // mock that repo contains a security context + when(repo.containsContext(any(HttpServletRequest.class))).thenReturn(true); + SessionFixationProtectionFilter filter = new SessionFixationProtectionFilter(repo); + filter.setAuthenticatedSessionStrategy(strategy); HttpServletRequest request = new MockHttpServletRequest(); - String sessionId = request.getSession().getId(); authenticateUser(); - request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - SecurityContextHolder.getContext()); filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()); - assertEquals(sessionId, request.getSession().getId()); + verifyZeroInteractions(strategy); + } + + @Test + public void strategyIsNotInvokedIfAuthenticationIsNull() throws Exception { + SecurityContextRepository repo = mock(SecurityContextRepository.class); + AuthenticatedSessionStrategy strategy = mock(AuthenticatedSessionStrategy.class); + SessionFixationProtectionFilter filter = new SessionFixationProtectionFilter(repo); + filter.setAuthenticatedSessionStrategy(strategy); + HttpServletRequest request = new MockHttpServletRequest(); + + filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()); + + verifyZeroInteractions(strategy); + } + + @Test + public void strategyIsInvokedIfUserIsNewlyAuthenticated() throws Exception { + SecurityContextRepository repo = mock(SecurityContextRepository.class); + // repo will return false to containsContext() + AuthenticatedSessionStrategy strategy = mock(AuthenticatedSessionStrategy.class); + SessionFixationProtectionFilter filter = new SessionFixationProtectionFilter(repo); + filter.setAuthenticatedSessionStrategy(strategy); + HttpServletRequest request = new MockHttpServletRequest(); + authenticateUser(); + + filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()); + + verify(strategy).onAuthenticationSuccess(any(Authentication.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); } private void authenticateUser() {