diff --git a/web/src/main/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServices.java b/web/src/main/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServices.java index ffe38b217b..1de12f4f44 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServices.java +++ b/web/src/main/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServices.java @@ -1,5 +1,7 @@ package org.springframework.security.web.authentication.rememberme; +import java.lang.reflect.Method; + import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -25,12 +27,14 @@ import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** * Base class for RememberMeServices implementations. * * @author Luke Taylor + * @author Rob Winch * @since 2.0 */ public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler { @@ -57,6 +61,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices, private String key; private int tokenValiditySeconds = TWO_WEEKS_S; private Boolean useSecureCookie = null; + private Method setHttpOnlyMethod; private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); /** @@ -64,6 +69,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices, */ @Deprecated protected AbstractRememberMeServices() { + this.setHttpOnlyMethod = ReflectionUtils.findMethod(Cookie.class,"setHttpOnly", boolean.class); } protected AbstractRememberMeServices(String key, UserDetailsService userDetailsService) { @@ -71,6 +77,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices, Assert.notNull(userDetailsService, "UserDetailsService cannot be null"); this.key = key; this.userDetailsService = userDetailsService; + this.setHttpOnlyMethod = ReflectionUtils.findMethod(Cookie.class,"setHttpOnly", boolean.class); } public void afterPropertiesSet() throws Exception { @@ -325,7 +332,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices, * * By default a secure cookie will be used if the connection is secure. You can set the {@code useSecureCookie} * property to {@code false} to override this. If you set it to {@code true}, the cookie will always be flagged - * as secure. + * as secure. If Servlet 3.0 is used, the cookie will be marked as HttpOnly. * * @param tokens the tokens which will be encoded to make the cookie value. * @param maxAge the value passed to {@link Cookie#setMaxAge(int)} @@ -344,6 +351,12 @@ public abstract class AbstractRememberMeServices implements RememberMeServices, cookie.setSecure(useSecureCookie); } + if(setHttpOnlyMethod != null) { + ReflectionUtils.invokeMethod(setHttpOnlyMethod, cookie, Boolean.TRUE); + } else if (logger.isDebugEnabled()) { + logger.debug("Note: Cookie will not be marked as HttpOnly because you are not using Servlet 3.0 (Cookie#setHttpOnly(boolean) was not found)."); + } + response.addCookie(cookie); } diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesServlet3Tests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesServlet3Tests.java new file mode 100644 index 0000000000..7453357163 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesServlet3Tests.java @@ -0,0 +1,50 @@ +package org.springframework.security.web.authentication.rememberme; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServicesTests.MockRememberMeServices; +import org.springframework.util.ReflectionUtils; + +/** + * Note: This test will fail in the IDE since it needs to be ran with servlet 3.0 and servlet 2.5 is also on the classpath. + * + * @author Rob Winch + */ +public class AbstractRememberMeServicesServlet3Tests { + + @Test + public void httpOnlySetInServlet30DefaultConstructor() throws Exception { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getContextPath()).thenReturn("/contextpath"); + HttpServletResponse response = mock(HttpServletResponse.class); + ArgumentCaptor cookie = ArgumentCaptor.forClass(Cookie.class); + MockRememberMeServices services = new MockRememberMeServices(); + services.setCookie(new String[] {"mycookie"}, 1000, request, response); + verify(response).addCookie(cookie.capture()); + Cookie rememberme = cookie.getValue(); + assertTrue((Boolean)ReflectionUtils.invokeMethod(rememberme.getClass().getMethod("isHttpOnly"),rememberme)); + } + + @Test + public void httpOnlySetInServlet30() throws Exception { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getContextPath()).thenReturn("/contextpath"); + HttpServletResponse response = mock(HttpServletResponse.class); + ArgumentCaptor cookie = ArgumentCaptor.forClass(Cookie.class); + MockRememberMeServices services = new MockRememberMeServices("key",mock(UserDetailsService.class)); + services.setCookie(new String[] {"mycookie"}, 1000, request, response); + verify(response).addCookie(cookie.capture()); + Cookie rememberme = cookie.getValue(); + assertTrue((Boolean)ReflectionUtils.invokeMethod(rememberme.getClass().getMethod("isHttpOnly"),rememberme)); + } +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java index 402fcf2cf6..5fca1d7c5e 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java @@ -23,6 +23,7 @@ import org.springframework.security.web.authentication.rememberme.AbstractRememb import org.springframework.security.web.authentication.rememberme.CookieTheftException; import org.springframework.security.web.authentication.rememberme.InvalidCookieException; import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.StringUtils; /** @@ -330,6 +331,15 @@ public class AbstractRememberMeServicesTests { assertTrue(cookie.getSecure()); } + @Test + public void setHttpOnlyIgnoredForServlet25() throws Exception { + MockRememberMeServices services = new MockRememberMeServices(); + assertNull(ReflectionTestUtils.getField(services, "setHttpOnlyMethod")); + + services = new MockRememberMeServices("key",new MockUserDetailsService(joe, false)); + assertNull(ReflectionTestUtils.getField(services, "setHttpOnlyMethod")); + } + private Cookie[] createLoginCookie(String cookieToken) { MockRememberMeServices services = new MockRememberMeServices(); Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, @@ -346,10 +356,14 @@ public class AbstractRememberMeServicesTests { //~ Inner Classes ================================================================================================== - private class MockRememberMeServices extends AbstractRememberMeServices { + static class MockRememberMeServices extends AbstractRememberMeServices { boolean loginSuccessCalled; - private MockRememberMeServices() { + MockRememberMeServices(String key, UserDetailsService userDetailsService) { + super(key,userDetailsService); + } + + MockRememberMeServices() { setKey("key"); } diff --git a/web/web.gradle b/web/web.gradle index 2cc1a35a73..5a1b01af3a 100644 --- a/web/web.gradle +++ b/web/web.gradle @@ -1,4 +1,8 @@ // Web module build file +configurations { + servlet3Test + servlet3Test.exclude group: 'javax.servlet', name: 'sevlet-api' +} dependencies { compile project(':spring-security-core'), @@ -13,8 +17,30 @@ dependencies { provided 'javax.servlet:servlet-api:2.5' + servlet3Test 'org.jboss.spec.javax.servlet:jboss-servlet-api_3.0_spec:1.0.0.Final' + testCompile project(':spring-security-core').sourceSets.test.classes, 'commons-codec:commons-codec:1.3', "org.springframework:spring-test:$springVersion" testRuntime "hsqldb:hsqldb:$hsqlVersion" } + +configurations.testRuntime.allDependencies.each { + if( !(it.group == 'javax.servlet' && it.name == 'servlet-api') ) { + configurations.servlet3Test.addDependency it + } +} + +test { + exclude '**/*Servlet3Tests.class' +} + +task servlet3Test(type: Test, dependsOn: testClasses) { + testClassesDir = sourceSets.test.classesDir + logging.captureStandardOutput(LogLevel.INFO) + classpath = sourceSets.main.classes + sourceSets.test.classes + configurations.servlet3Test + maxParallelForks = 1 + testReport = false + include '**/*Servlet3Tests.class' +} +check.dependsOn servlet3Test \ No newline at end of file