Browse Source

SEC-1695: Allow customization of the session key under which the SecurityContext is stored.

Luke Taylor 14 years ago
parent
commit
6d04670f87

+ 28 - 27
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;
  * <p>This implementation is backed by a <a
  * href="http://java.sun.com/j2se/1.5.0/docs/guide/security/jaas/JAASRefGuide.html">JAAS</a> configuration that is provided by
  * a subclass's implementation of {@link #createLoginContext(CallbackHandler)}.
- * 
+ *
  * <p>When using JAAS login modules as the authentication source, sometimes the
  * <a href="http://java.sun.com/j2se/1.5.0/docs/api/javax/security/auth/login/LoginContext.html">LoginContext</a> will
  * require <i>CallbackHandler</i>s. The AbstractJaasAuthenticationProvider uses an internal
@@ -91,7 +89,7 @@ import org.springframework.util.ObjectUtils;
  *   &lt;/list&gt;
  *  &lt;/property&gt;
  * </pre>
- * 
+ *
  * @author Ray Krueger
  * @author Rob Winch
  */
@@ -190,7 +188,7 @@ ApplicationEventPublisherAware, InitializingBean, ApplicationListener<SessionDes
 
     /**
      * Creates the LoginContext to be used for authentication.
-     * 
+     *
      * @param handler The CallbackHandler that should be used for the LoginContext (never <code>null</code>).
      * @return the LoginContext to use for authentication.
      * @throws LoginException
@@ -198,39 +196,42 @@ ApplicationEventPublisherAware, InitializingBean, ApplicationListener<SessionDes
     protected abstract LoginContext createLoginContext(CallbackHandler handler) throws LoginException;
 
     /**
-     * Handles the logout by getting the SecurityContext for the session that was destroyed. <b>MUST NOT use
-     * SecurityContextHolder as we are logging out a session that is not related to the current user.</b>
+     * 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<SecurityContext> 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);
             }
         }
     }

+ 7 - 3
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 <tt>SecurityContext</tt> 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 <tt>SecurityContext</tt> 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<SecurityContext> getSecurityContexts();
 
     /**
      * @return the identifier associated with the destroyed session.

+ 14 - 14
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();

+ 4 - 2
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);
 

+ 1 - 1
core/src/test/java/org/springframework/security/core/session/SessionRegistryImplTests.java

@@ -58,7 +58,7 @@ public class SessionRegistryImplTests {
             }
 
             @Override
-            public SecurityContext getSecurityContext() {
+            public List<SecurityContext> getSecurityContexts() {
                 return null;
             }
         });

+ 24 - 8
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.
  * <p>
  * The {@code HttpSession} will be queried to retrieve the {@code SecurityContext} in the <tt>loadContext</tt>
- * 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.
  * <p>
@@ -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 + "'");

+ 18 - 2
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<SecurityContext> getSecurityContexts() {
+        HttpSession session = (HttpSession)getSource();
+
+        Enumeration<String> attributes = session.getAttributeNames();
+
+        ArrayList<SecurityContext> contexts = new ArrayList<SecurityContext>();
+
+        while(attributes.hasMoreElements()) {
+            Object attribute = attributes.nextElement();
+            if (attribute instanceof SecurityContext) {
+                contexts.add((SecurityContext) attribute);
+            }
+        }
+
+        return contexts;
     }
 
     @Override

+ 12 - 8
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