diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java index 01a63a20b9..ad963a5030 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java @@ -17,9 +17,7 @@ package org.springframework.security.authentication.jaas; import java.io.IOException; import java.security.Principal; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; +import java.util.*; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -54,7 +52,7 @@ import org.springframework.util.ObjectUtils; *

This implementation is backed by a JAAS configuration that is provided by * a subclass's implementation of {@link #createLoginContext(CallbackHandler)}. - * + * *

When using JAAS login modules as the authentication source, sometimes the * LoginContext will * require CallbackHandlers. The AbstractJaasAuthenticationProvider uses an internal @@ -91,7 +89,7 @@ import org.springframework.util.ObjectUtils; * </list> * </property> * - * + * * @author Ray Krueger * @author Rob Winch */ @@ -190,7 +188,7 @@ ApplicationEventPublisherAware, InitializingBean, ApplicationListenernull). * @return the LoginContext to use for authentication. * @throws LoginException @@ -198,39 +196,42 @@ ApplicationEventPublisherAware, InitializingBean, ApplicationListenerMUST NOT use - * SecurityContextHolder as we are logging out a session that is not related to the current user. + * Handles the logout by getting the security contexts for the destroyed session and invoking + * {@code LoginContext.logout()} for any which contain a {@code JaasAuthenticationToken}. * - * @param event + * + * @param event the session event which contains the current session */ protected void handleLogout(SessionDestroyedEvent event) { - SecurityContext context = event.getSecurityContext(); + List contexts = event.getSecurityContexts(); - if (context == null) { - log.debug("The destroyed session has no SecurityContext"); + if (contexts.isEmpty()) { + log.debug("The destroyed session has no SecurityContexts"); return; } - Authentication auth = context.getAuthentication(); + for(SecurityContext context : contexts) { + Authentication auth = context.getAuthentication(); - if ((auth != null) && (auth instanceof JaasAuthenticationToken)) { - JaasAuthenticationToken token = (JaasAuthenticationToken) auth; + if ((auth != null) && (auth instanceof JaasAuthenticationToken)) { + JaasAuthenticationToken token = (JaasAuthenticationToken) auth; - try { - LoginContext loginContext = token.getLoginContext(); - boolean debug = log.isDebugEnabled(); - if (loginContext != null) { - if (debug) { - log.debug("Logging principal: [" + token.getPrincipal() + "] out of LoginContext"); + try { + LoginContext loginContext = token.getLoginContext(); + boolean debug = log.isDebugEnabled(); + if (loginContext != null) { + if (debug) { + log.debug("Logging principal: [" + token.getPrincipal() + "] out of LoginContext"); + } + loginContext.logout(); + } else if (debug) { + log.debug("Cannot logout principal: [" + token.getPrincipal() + "] from LoginContext. " + + "The LoginContext is unavailable"); } - loginContext.logout(); - } else if (debug) { - log.debug("Cannot logout principal: [" + token.getPrincipal() + "] from LoginContext. " - + "The LoginContext is unavailable"); + } catch (LoginException e) { + log.warn("Error error logging out of LoginContext", e); } - } catch (LoginException e) { - log.warn("Error error logging out of LoginContext", e); } } } diff --git a/core/src/main/java/org/springframework/security/core/session/SessionDestroyedEvent.java b/core/src/main/java/org/springframework/security/core/session/SessionDestroyedEvent.java index 6b34903f56..c288caeb3a 100644 --- a/core/src/main/java/org/springframework/security/core/session/SessionDestroyedEvent.java +++ b/core/src/main/java/org/springframework/security/core/session/SessionDestroyedEvent.java @@ -3,6 +3,8 @@ package org.springframework.security.core.session; import org.springframework.context.ApplicationEvent; import org.springframework.security.core.context.SecurityContext; +import java.util.*; + /** * Generic "session termination" event which indicates that a session (potentially * represented by a security context) has ended. @@ -17,11 +19,13 @@ public abstract class SessionDestroyedEvent extends ApplicationEvent { } /** - * Provides the SecurityContext under which the session was running. + * Provides the {@code SecurityContext} instances which were associated with the destroyed session. Usually there + * will be only one security context per session. * - * @return the SecurityContext associated with the session, or null if there is no context. + * @return the {@code SecurityContext} instances which were stored in the current session (an empty list if there + * are none). */ - public abstract SecurityContext getSecurityContext(); + public abstract List getSecurityContexts(); /** * @return the identifier associated with the destroyed session. diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java index 3effeb7621..4568517ada 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java @@ -27,7 +27,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import java.util.Collections; +import java.util.*; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; @@ -136,13 +136,13 @@ public class DefaultJaasAuthenticationProviderTests { JaasAuthenticationToken token = mock(JaasAuthenticationToken.class); LoginContext context = mock(LoginContext.class); - when(event.getSecurityContext()).thenReturn(securityContext); + when(event.getSecurityContexts()).thenReturn(Arrays.asList(securityContext)); when(securityContext.getAuthentication()).thenReturn(token); when(token.getLoginContext()).thenReturn(context); provider.onApplicationEvent(event); - verify(event).getSecurityContext(); + verify(event).getSecurityContexts(); verify(securityContext).getAuthentication(); verify(token).getLoginContext(); verify(context).logout(); @@ -155,7 +155,7 @@ public class DefaultJaasAuthenticationProviderTests { provider.handleLogout(event); - verify(event).getSecurityContext(); + verify(event).getSecurityContexts(); verify(log).debug(anyString()); verifyNoMoreInteractions(event); } @@ -165,12 +165,12 @@ public class DefaultJaasAuthenticationProviderTests { SessionDestroyedEvent event = mock(SessionDestroyedEvent.class); SecurityContext securityContext = mock(SecurityContext.class); - when(event.getSecurityContext()).thenReturn(securityContext); + when(event.getSecurityContexts()).thenReturn(Arrays.asList(securityContext)); provider.handleLogout(event); - verify(event).getSecurityContext(); - verify(event).getSecurityContext(); + verify(event).getSecurityContexts(); + verify(event).getSecurityContexts(); verify(securityContext).getAuthentication(); verifyNoMoreInteractions(event, securityContext); } @@ -180,13 +180,13 @@ public class DefaultJaasAuthenticationProviderTests { SessionDestroyedEvent event = mock(SessionDestroyedEvent.class); SecurityContext securityContext = mock(SecurityContext.class); - when(event.getSecurityContext()).thenReturn(securityContext); + when(event.getSecurityContexts()).thenReturn(Arrays.asList(securityContext)); when(securityContext.getAuthentication()).thenReturn(token); provider.handleLogout(event); - verify(event).getSecurityContext(); - verify(event).getSecurityContext(); + verify(event).getSecurityContexts(); + verify(event).getSecurityContexts(); verify(securityContext).getAuthentication(); verifyNoMoreInteractions(event, securityContext); } @@ -197,11 +197,11 @@ public class DefaultJaasAuthenticationProviderTests { SecurityContext securityContext = mock(SecurityContext.class); JaasAuthenticationToken token = mock(JaasAuthenticationToken.class); - when(event.getSecurityContext()).thenReturn(securityContext); + when(event.getSecurityContexts()).thenReturn(Arrays.asList(securityContext)); when(securityContext.getAuthentication()).thenReturn(token); provider.onApplicationEvent(event); - verify(event).getSecurityContext(); + verify(event).getSecurityContexts(); verify(securityContext).getAuthentication(); verify(token).getLoginContext(); @@ -216,14 +216,14 @@ public class DefaultJaasAuthenticationProviderTests { LoginContext context = mock(LoginContext.class); LoginException loginException = new LoginException("Failed Login"); - when(event.getSecurityContext()).thenReturn(securityContext); + when(event.getSecurityContexts()).thenReturn(Arrays.asList(securityContext)); when(securityContext.getAuthentication()).thenReturn(token); when(token.getLoginContext()).thenReturn(context); doThrow(loginException).when(context).logout(); provider.onApplicationEvent(event); - verify(event).getSecurityContext(); + verify(event).getSecurityContexts(); verify(securityContext).getAuthentication(); verify(token).getLoginContext(); verify(context).logout(); diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java index 3f832b6785..83bdd6e759 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java @@ -41,6 +41,8 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.core.session.SessionDestroyedEvent; @@ -244,11 +246,11 @@ public class JaasAuthenticationProviderTests { JaasAuthenticationToken token = new JaasAuthenticationToken(null, null, loginContext); - SecurityContextImpl context = new SecurityContextImpl(); + SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(token); SessionDestroyedEvent event = mock(SessionDestroyedEvent.class); - when(event.getSecurityContext()).thenReturn(context); + when(event.getSecurityContexts()).thenReturn(Arrays.asList(context)); jaasProvider.handleLogout(event); diff --git a/core/src/test/java/org/springframework/security/core/session/SessionRegistryImplTests.java b/core/src/test/java/org/springframework/security/core/session/SessionRegistryImplTests.java index 7949b31968..76ea72ea17 100644 --- a/core/src/test/java/org/springframework/security/core/session/SessionRegistryImplTests.java +++ b/core/src/test/java/org/springframework/security/core/session/SessionRegistryImplTests.java @@ -58,7 +58,7 @@ public class SessionRegistryImplTests { } @Override - public SecurityContext getSecurityContext() { + public List getSecurityContexts() { return null; } }); 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 79b28c5606..bd4c856ea7 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 @@ -8,6 +8,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import javax.servlet.http.HttpServletRequest; @@ -19,7 +20,7 @@ import javax.servlet.http.HttpSession; * between requests. *

* The {@code HttpSession} will be queried to retrieve the {@code SecurityContext} in the loadContext - * method (using the key {@link #SPRING_SECURITY_CONTEXT_KEY}). If a valid {@code SecurityContext} cannot be + * method (using the key {@link #SPRING_SECURITY_CONTEXT_KEY} by default). If a valid {@code SecurityContext} cannot be * obtained from the {@code HttpSession} for whatever reason, a fresh {@code SecurityContext} will be created * by calling by {@link SecurityContextHolder#createEmptyContext()} and this instance will be returned instead. *

@@ -50,6 +51,9 @@ import javax.servlet.http.HttpSession; * @since 3.0 */ public class HttpSessionSecurityContextRepository implements SecurityContextRepository { + /** + * The default key under which the security context will be stored in the session. + */ public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; protected final Log logger = LogFactory.getLog(this.getClass()); @@ -59,6 +63,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo private final Object contextObject = SecurityContextHolder.createEmptyContext(); private boolean allowSessionCreation = true; private boolean disableUrlRewriting = false; + private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY; private final AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); @@ -108,7 +113,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo return false; } - return session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null; + return session.getAttribute(springSecurityContextKey) != null; } /** @@ -128,7 +133,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo // Session exists, so try to obtain a context from it. - Object contextFromSession = httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + Object contextFromSession = httpSession.getAttribute(springSecurityContextKey); if (contextFromSession == null) { if (debug) { @@ -141,7 +146,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo // We now have the security context object from the session. if (!(contextFromSession instanceof SecurityContext)) { if (logger.isWarnEnabled()) { - logger.warn("SPRING_SECURITY_CONTEXT did not contain a SecurityContext but contained: '" + logger.warn(springSecurityContextKey + " did not contain a SecurityContext but contained: '" + contextFromSession + "'; are you improperly modifying the HttpSession directly " + "(you should always use SecurityContextHolder) or using the HttpSession attribute " + "reserved for this class?"); @@ -151,7 +156,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo } if (debug) { - logger.debug("Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: '" + contextFromSession + "'"); + logger.debug("Obtained a valid SecurityContext from " + springSecurityContextKey + ": '" + contextFromSession + "'"); } // Everything OK. The only non-null return from this method. @@ -212,6 +217,17 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo this.disableUrlRewriting = disableUrlRewriting; } + /** + * Allows the session attribute name to be customized for this repository instance. + * + * @param springSecurityContextKey the key under which the security context will be stored. Defaults to + * {@link #SPRING_SECURITY_CONTEXT_KEY}. + */ + public void setSpringSecurityContextKey(String springSecurityContextKey) { + Assert.hasText(springSecurityContextKey, "springSecurityContextKey cannot be empty"); + this.springSecurityContextKey = springSecurityContextKey; + } + //~ Inner Classes ================================================================================================== /** @@ -273,7 +289,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo if (httpSession != null) { // SEC-1587 A non-anonymous context may still be in the session - httpSession.removeAttribute(SPRING_SECURITY_CONTEXT_KEY); + httpSession.removeAttribute(springSecurityContextKey); } return; } @@ -286,8 +302,8 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo // actually changed in this thread (see SEC-37, SEC-1307, SEC-1528) if (httpSession != null) { // We may have a new session, so check also whether the context attribute is set SEC-1561 - if (contextChanged(context) || httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY) == null) { - httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); + if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) { + httpSession.setAttribute(springSecurityContextKey, context); if (logger.isDebugEnabled()) { logger.debug("SecurityContext stored to HttpSession: '" + context + "'"); diff --git a/web/src/main/java/org/springframework/security/web/session/HttpSessionDestroyedEvent.java b/web/src/main/java/org/springframework/security/web/session/HttpSessionDestroyedEvent.java index cb7f219e51..cee1b0ce7b 100644 --- a/web/src/main/java/org/springframework/security/web/session/HttpSessionDestroyedEvent.java +++ b/web/src/main/java/org/springframework/security/web/session/HttpSessionDestroyedEvent.java @@ -17,10 +17,12 @@ package org.springframework.security.web.session; import javax.servlet.http.HttpSession; +import com.sun.xml.internal.ws.encoding.ContentType; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.session.SessionDestroyedEvent; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import java.util.*; /** * Published by the {@link HttpSessionEventPublisher} when a HttpSession is created in the container @@ -39,9 +41,23 @@ public class HttpSessionDestroyedEvent extends SessionDestroyedEvent { return (HttpSession) getSource(); } + @SuppressWarnings("unchecked") @Override - public SecurityContext getSecurityContext() { - return (SecurityContext) ((HttpSession)getSource()).getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + public List getSecurityContexts() { + HttpSession session = (HttpSession)getSource(); + + Enumeration attributes = session.getAttributeNames(); + + ArrayList contexts = new ArrayList(); + + while(attributes.hasMoreElements()) { + Object attribute = attributes.nextElement(); + if (attribute instanceof SecurityContext) { + contexts.add((SecurityContext) attribute); + } + } + + return contexts; } @Override diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index 70dfb92787..f06e7bba25 100644 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java @@ -47,9 +47,10 @@ public class HttpSessionSecurityContextRepositoryTests { @Test public void existingContextIsSuccessFullyLoadedFromSessionAndSavedBack() throws Exception { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + repo.setSpringSecurityContextKey("imTheContext"); MockHttpServletRequest request = new MockHttpServletRequest(); SecurityContextHolder.getContext().setAuthentication(testToken); - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); + request.getSession().setAttribute("imTheContext", SecurityContextHolder.getContext()); MockHttpServletResponse response = new MockHttpServletResponse(); HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContext context = repo.loadContext(holder); @@ -57,7 +58,7 @@ public class HttpSessionSecurityContextRepositoryTests { assertEquals(testToken, context.getAuthentication()); // Won't actually be saved as it hasn't changed, but go through the use case anyway repo.saveContext(context, holder.getRequest(), holder.getResponse()); - assertEquals(context, request.getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY)); + assertEquals(context, request.getSession().getAttribute("imTheContext")); } // SEC-1528 @@ -113,33 +114,35 @@ public class HttpSessionSecurityContextRepositoryTests { @Test public void redirectCausesEarlySaveOfContext() throws Exception { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + repo.setSpringSecurityContextKey("imTheContext"); MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContextHolder.setContext(repo.loadContext(holder)); SecurityContextHolder.getContext().setAuthentication(testToken); holder.getResponse().sendRedirect("/doesntmatter"); - assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY)); + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext")); assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved()); repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse()); // Check it's still the same - assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY)); + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext")); } @Test public void sendErrorCausesEarlySaveOfContext() throws Exception { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + repo.setSpringSecurityContextKey("imTheContext"); MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContextHolder.setContext(repo.loadContext(holder)); SecurityContextHolder.getContext().setAuthentication(testToken); holder.getResponse().sendError(404); - assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY)); + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext")); assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved()); repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse()); // Check it's still the same - assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY)); + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext")); } @Test @@ -188,15 +191,16 @@ public class HttpSessionSecurityContextRepositoryTests { @Test public void contextIsRemovedFromSessionIfCurrentContextIsEmpty() throws Exception { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + repo.setSpringSecurityContextKey("imTheContext"); MockHttpServletRequest request = new MockHttpServletRequest(); SecurityContext ctxInSession = SecurityContextHolder.createEmptyContext(); ctxInSession.setAuthentication(testToken); - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, ctxInSession); + request.getSession().setAttribute("imTheContext", ctxInSession); HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, new MockHttpServletResponse()); repo.loadContext(holder); // Save an empty context repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse()); - assertNull(request.getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY)); + assertNull(request.getSession().getAttribute("imTheContext")); } @Test