Browse Source

SEC-1603: Add support for injecting an AuthenticationSuccessHandler into RememberMeAuthenticationFilter.

Luke Taylor 14 years ago
parent
commit
7fd3aa2b45

+ 46 - 17
web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java

@@ -31,32 +31,39 @@ import org.springframework.security.authentication.event.InteractiveAuthenticati
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.RememberMeServices;
 import org.springframework.util.Assert;
 import org.springframework.web.filter.GenericFilterBean;
 
 
 /**
- * Detects if there is no <code>Authentication</code> object in the <code>SecurityContext</code>, and populates it
- * with a remember-me authentication token if a {@link org.springframework.security.web.authentication.RememberMeServices}
- * implementation so requests.<p>Concrete <code>RememberMeServices</code> implementations will have their {@link
- * org.springframework.security.web.authentication.RememberMeServices#autoLogin(HttpServletRequest, HttpServletResponse)} method
- * called by this filter. The <code>Authentication</code> or <code>null</code> returned by that method will be placed
- * into the <code>SecurityContext</code>. The <code>AuthenticationManager</code> will be used, so that any concurrent
- * session management or other authentication-specific behaviour can be achieved. This is the same pattern as with
- * other authentication mechanisms, which call the <code>AuthenticationManager</code> as part of their contract.</p>
- *  <p>If authentication is successful, an {@link
- * org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent} will be published to the application
- * context. No events will be published if authentication was unsuccessful, because this would generally be recorded
- * via an <code>AuthenticationManager</code>-specific application event.</p>
+ * Detects if there is no {@code Authentication} object in the {@code SecurityContext}, and populates the context with
+ * a remember-me authentication token if a {@link RememberMeServices} implementation so requests.
+ * <p>
+ * Concrete {@code RememberMeServices} implementations will have their
+ * {@link RememberMeServices#autoLogin(HttpServletRequest, HttpServletResponse)}
+ * method called by this filter. If this method returns a non-null {@code Authentication} object, it will be passed
+ * to the {@code AuthenticationManager}, so that any authentication-specific behaviour can be achieved.
+ * The resulting {@code Authentication} (if successful) will be placed into the {@code SecurityContext}.
+ * <p>
+ * If authentication is successful, an {@link InteractiveAuthenticationSuccessEvent} will be published
+ * to the application context. No events will be published if authentication was unsuccessful, because this would
+ * generally be recorded via an {@code AuthenticationManager}-specific application event.
+ * <p>
+ * Normally the request will be allowed to proceed regardless of whether authentication succeeds or fails. If
+ * some control over the destination for authenticated users is required, an {@link AuthenticationSuccessHandler}
+ * can be injected
  *
  * @author Ben Alex
+ * @author Luke Taylor
  */
 public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {
 
     //~ Instance fields ================================================================================================
 
     private ApplicationEventPublisher eventPublisher;
+    private AuthenticationSuccessHandler successHandler;
     private AuthenticationManager authenticationManager;
     private RememberMeServices rememberMeServices;
 
@@ -96,6 +103,13 @@ public class RememberMeAuthenticationFilter extends GenericFilterBean implements
                         eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                                 SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
                     }
+
+                    if (successHandler != null) {
+                        successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
+
+                        return;
+                    }
+
                 } catch (AuthenticationException authenticationException) {
                     if (logger.isDebugEnabled()) {
                         logger.debug("SecurityContextHolder not populated with remember-me token, as "
@@ -121,17 +135,17 @@ public class RememberMeAuthenticationFilter extends GenericFilterBean implements
     }
 
     /**
-     * Called if a remember-me token is presented and successfully authenticated by the <tt>RememberMeServices</tt>
-     * <tt>autoLogin</tt> method and the <tt>AuthenticationManager</tt>.
+     * Called if a remember-me token is presented and successfully authenticated by the {@code RememberMeServices}
+     * {@code autoLogin} method and the {@code AuthenticationManager}.
      */
     protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
             Authentication authResult) {
     }
 
     /**
-     * Called if the <tt>AuthenticationManager</tt> rejects the authentication object returned from the
-     * <tt>RememberMeServices</tt> <tt>autoLogin</tt> method. This method will not be called when no remember-me
-     * token is present in the request and <tt>autoLogin</tt> returns null.
+     * Called if the {@code AuthenticationManager} rejects the authentication object returned from the
+     * {@code RememberMeServices} {@code autoLogin} method. This method will not be called when no remember-me
+     * token is present in the request and {@code autoLogin} reurns null.
      */
     protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
             AuthenticationException failed) {
@@ -152,4 +166,19 @@ public class RememberMeAuthenticationFilter extends GenericFilterBean implements
     public void setRememberMeServices(RememberMeServices rememberMeServices) {
         this.rememberMeServices = rememberMeServices;
     }
+
+    /**
+     * Allows control over the destination a remembered user is sent to when they are successfully authenticated.
+     * By default, the filter will just allow the current request to proceed, but if an
+     * {@code AuthenticationSuccessHandler} is set, it will be invoked and the {@code doFilter()} method will return
+     * immediately, thus allowing the application to redirect the user to a specific URL, regardless of whatthe original
+     * request was for.
+     *
+     * @param successHandler the strategy to invoke immediately before returning from {@code doFilter()}.
+     */
+    public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
+        Assert.notNull(successHandler, "successHandler cannot be null");
+        this.successHandler = successHandler;
+    }
+
 }

+ 52 - 62
web/src/test/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilterTests.java

@@ -15,22 +15,11 @@
 
 package org.springframework.security.web.authentication.rememberme;
 
+import static org.junit.Assert.*;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.*;
 
-import java.io.IOException;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import junit.framework.TestCase;
-
+import org.junit.*;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
@@ -42,6 +31,11 @@ import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.authentication.NullRememberMeServices;
 import org.springframework.security.web.authentication.RememberMeServices;
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
 
 /**
@@ -49,27 +43,23 @@ import org.springframework.security.web.authentication.RememberMeServices;
  *
  * @author Ben Alex
  */
-public class RememberMeAuthenticationFilterTests extends TestCase {
+public class RememberMeAuthenticationFilterTests {
     Authentication remembered = new TestingAuthenticationToken("remembered", "password","ROLE_REMEMBERED");
 
     //~ Methods ========================================================================================================
 
-    private void executeFilterInContainerSimulator(FilterConfig filterConfig, Filter filter, ServletRequest request,
-        ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-//        filter.init(filterConfig);
-        filter.doFilter(request, response, filterChain);
-//        filter.destroy();
-    }
-
-    protected void setUp() throws Exception {
+    @Before
+    public void setUp() {
         SecurityContextHolder.clearContext();
     }
 
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() {
         SecurityContextHolder.clearContext();
     }
 
-    public void testDetectsAuthenticationManagerProperty() throws Exception {
+    @Test(expected = IllegalArgumentException.class)
+    public void testDetectsAuthenticationManagerProperty() {
         RememberMeAuthenticationFilter filter = new RememberMeAuthenticationFilter();
         filter.setAuthenticationManager(mock(AuthenticationManager.class));
         filter.setRememberMeServices(new NullRememberMeServices());
@@ -78,15 +68,11 @@ public class RememberMeAuthenticationFilterTests extends TestCase {
 
         filter.setAuthenticationManager(null);
 
-        try {
-            filter.afterPropertiesSet();
-            fail("Should have thrown IllegalArgumentException");
-        } catch (IllegalArgumentException expected) {
-            assertTrue(true);
-        }
+        filter.afterPropertiesSet();
     }
 
-    public void testDetectsRememberMeServicesProperty() throws Exception {
+    @Test(expected = IllegalArgumentException.class)
+    public void testDetectsRememberMeServicesProperty() {
         RememberMeAuthenticationFilter filter = new RememberMeAuthenticationFilter();
         filter.setAuthenticationManager(mock(AuthenticationManager.class));
 
@@ -100,14 +86,10 @@ public class RememberMeAuthenticationFilterTests extends TestCase {
         // check detects if made null
         filter.setRememberMeServices(null);
 
-        try {
-            filter.afterPropertiesSet();
-            fail("Should have thrown IllegalArgumentException");
-        } catch (IllegalArgumentException expected) {
-            assertTrue(true);
-        }
+        filter.afterPropertiesSet();
     }
 
+    @Test
     public void testOperationWhenAuthenticationExistsInContextHolder() throws Exception {
         // Put an Authentication object into the SecurityContextHolder
         Authentication originalAuth = new TestingAuthenticationToken("user", "password","ROLE_A");
@@ -121,14 +103,16 @@ public class RememberMeAuthenticationFilterTests extends TestCase {
 
         // Test
         MockHttpServletRequest request = new MockHttpServletRequest();
+        FilterChain fc = mock(FilterChain.class);
         request.setRequestURI("x");
-        executeFilterInContainerSimulator(mock(FilterConfig.class), filter, request, new MockHttpServletResponse(),
-            new MockFilterChain(true));
+        filter.doFilter(request, new MockHttpServletResponse(), fc);
 
         // Ensure filter didn't change our original object
-        assertEquals(originalAuth, SecurityContextHolder.getContext().getAuthentication());
+        assertSame(originalAuth, SecurityContextHolder.getContext().getAuthentication());
+        verify(fc).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
     }
 
+    @Test
     public void testOperationWhenNoAuthenticationInContextHolder() throws Exception {
 
         RememberMeAuthenticationFilter filter = new RememberMeAuthenticationFilter();
@@ -139,15 +123,17 @@ public class RememberMeAuthenticationFilterTests extends TestCase {
         filter.afterPropertiesSet();
 
         MockHttpServletRequest request = new MockHttpServletRequest();
+        FilterChain fc = mock(FilterChain.class);
         request.setRequestURI("x");
-        executeFilterInContainerSimulator(mock(FilterConfig.class), filter, request, new MockHttpServletResponse(),
-            new MockFilterChain(true));
+        filter.doFilter(request, new MockHttpServletResponse(), fc);
 
         // Ensure filter setup with our remembered authentication object
-        assertEquals(remembered, SecurityContextHolder.getContext().getAuthentication());
+        assertSame(remembered, SecurityContextHolder.getContext().getAuthentication());
+        verify(fc).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
     }
 
-    public void testOnUnsuccessfulLoginIsCalledWhenProviderRejectsAuth() throws Exception {
+    @Test
+    public void onUnsuccessfulLoginIsCalledWhenProviderRejectsAuth() throws Exception {
         final Authentication failedAuth = new TestingAuthenticationToken("failed", "");
 
         RememberMeAuthenticationFilter filter = new RememberMeAuthenticationFilter() {
@@ -164,32 +150,36 @@ public class RememberMeAuthenticationFilterTests extends TestCase {
         filter.afterPropertiesSet();
 
         MockHttpServletRequest request = new MockHttpServletRequest();
+        FilterChain fc = mock(FilterChain.class);
         request.setRequestURI("x");
-        executeFilterInContainerSimulator(mock(FilterConfig.class), filter, request, new MockHttpServletResponse(),
-            new MockFilterChain(true));
+        filter.doFilter(request, new MockHttpServletResponse(), fc);
 
-        assertEquals(failedAuth, SecurityContextHolder.getContext().getAuthentication());
+        assertSame(failedAuth, SecurityContextHolder.getContext().getAuthentication());
+        verify(fc).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
     }
 
-    //~ Inner Classes ==================================================================================================
-
-    private class MockFilterChain implements FilterChain {
-        private boolean expectToProceed;
+    @Test
+    public void authenticationSuccessHandlerIsInvokedOnSuccessfulAuthenticationIfSet() throws Exception {
+        RememberMeAuthenticationFilter filter = new RememberMeAuthenticationFilter();
+        AuthenticationManager am = mock(AuthenticationManager.class);
+        when(am.authenticate(remembered)).thenReturn(remembered);
+        filter.setAuthenticationManager(am);
+        filter.setRememberMeServices(new MockRememberMeServices(remembered));
+        filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/target"));
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        FilterChain fc = mock(FilterChain.class);
+        request.setRequestURI("x");
+        filter.doFilter(request, response, fc);
 
-        public MockFilterChain(boolean expectToProceed) {
-            this.expectToProceed = expectToProceed;
-        }
+        assertEquals("/target", response.getRedirectedUrl());
 
-        public void doFilter(ServletRequest request, ServletResponse response)
-            throws IOException, ServletException {
-            if (expectToProceed) {
-                assertTrue(true);
-            } else {
-                fail("Did not expect filter chain to proceed");
-            }
-        }
+        // Should return after success handler is invoked, so chain should not proceed
+        verifyZeroInteractions(fc);
     }
 
+    //~ Inner Classes ==================================================================================================
+
     private class MockRememberMeServices implements RememberMeServices {
         private Authentication authToReturn;