From 7fd3aa2b459bfef2c7fd63cfe3caeea68f473524 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Thu, 6 Jan 2011 13:02:38 +0000 Subject: [PATCH] SEC-1603: Add support for injecting an AuthenticationSuccessHandler into RememberMeAuthenticationFilter. --- .../RememberMeAuthenticationFilter.java | 63 +++++++--- .../RememberMeAuthenticationFilterTests.java | 114 ++++++++---------- 2 files changed, 98 insertions(+), 79 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java index 31d74e418f..8f2124b1b8 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java +++ b/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 Authentication object in the SecurityContext, and populates it - * with a remember-me authentication token if a {@link org.springframework.security.web.authentication.RememberMeServices} - * implementation so requests.

Concrete RememberMeServices implementations will have their {@link - * org.springframework.security.web.authentication.RememberMeServices#autoLogin(HttpServletRequest, HttpServletResponse)} method - * called by this filter. The Authentication or null returned by that method will be placed - * into the SecurityContext. The AuthenticationManager 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 AuthenticationManager as part of their contract.

- *

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 AuthenticationManager-specific application event.

+ * 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. + *

+ * 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}. + *

+ * 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. + *

+ * 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 RememberMeServices - * autoLogin method and the AuthenticationManager. + * 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 AuthenticationManager rejects the authentication object returned from the - * RememberMeServices autoLogin method. This method will not be called when no remember-me - * token is present in the request and autoLogin 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; + } + } diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilterTests.java index 33586ca878..c8ebe36bd9 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilterTests.java +++ b/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;