diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/CookieRequestCache.java b/web/src/main/java/org/springframework/security/web/savedrequest/CookieRequestCache.java new file mode 100644 index 0000000000..1c9c2515d9 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/savedrequest/CookieRequestCache.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.web.savedrequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.WebUtils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Base64; + + +/** + * An Implementation of {@code RequestCache} which saves the original request URI in a cookie. + * + * @author Zeeshan Adnan + */ +public class CookieRequestCache implements RequestCache { + + private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE; + protected final Log logger = LogFactory.getLog(this.getClass()); + + private static final String COOKIE_NAME = "REDIRECT_URI"; + private static final int COOKIE_MAX_AGE = -1; + + @Override + public void saveRequest(HttpServletRequest request, HttpServletResponse response) { + if (requestMatcher.matches(request)) { + String redirectUrl = UrlUtils.buildFullRequestUrl(request); + Cookie savedCookie = new Cookie(COOKIE_NAME, encodeCookie(redirectUrl)); + savedCookie.setMaxAge(COOKIE_MAX_AGE); + savedCookie.setSecure(request.isSecure()); + savedCookie.setPath(request.getContextPath()); + savedCookie.setHttpOnly(true); + + response.addCookie(savedCookie); + } else { + logger.debug("Request not saved as configured RequestMatcher did not match"); + } + } + + @Override + public SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response) { + Cookie savedRequestCookie = WebUtils.getCookie(request, COOKIE_NAME); + if (savedRequestCookie != null) { + String originalURI = decodeCookie(savedRequestCookie.getValue()); + UriComponents uriComponents = UriComponentsBuilder.fromUriString(originalURI).build(); + DefaultSavedRequest.Builder builder = new DefaultSavedRequest.Builder(); + + int port = uriComponents.getPort(); + if (port == -1) { + if ("https".equalsIgnoreCase(uriComponents.getScheme())) { + port = 443; + } else { + port = 80; + } + } + return builder.setScheme(uriComponents.getScheme()) + .setServerName(uriComponents.getHost()) + .setRequestURI(uriComponents.getPath()) + .setQueryString(uriComponents.getQuery()) + .setServerPort(port) + .build(); + } + return null; + } + + @Override + public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) { + SavedRequest savedRequest = getRequest(request, response); + if (savedRequest != null) { + removeRequest(request, response); + return new SavedRequestAwareWrapper(savedRequest, request); + } + return null; + } + + @Override + public void removeRequest(HttpServletRequest request, HttpServletResponse response) { + Cookie removeSavedRequestCookie = new Cookie(COOKIE_NAME, ""); + removeSavedRequestCookie.setSecure(request.isSecure()); + removeSavedRequestCookie.setHttpOnly(true); + removeSavedRequestCookie.setPath(request.getContextPath()); + removeSavedRequestCookie.setMaxAge(0); + response.addCookie(removeSavedRequestCookie); + } + + private static String encodeCookie(String cookieValue) { + return Base64.getEncoder().encodeToString(cookieValue.getBytes()); + } + + private static String decodeCookie(String encodedCookieValue) { + return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes())); + } + + /** + * Allows selective use of saved requests for a subset of requests. By default any + * request will be cached by the {@code saveRequest} method. + *

+ * If set, only matching requests will be cached. + * + * @param requestMatcher a request matching strategy which defines which requests + * should be cached. + */ + public void setRequestMatcher(RequestMatcher requestMatcher) { + Assert.notNull(requestMatcher, "requestMatcher should not be null"); + this.requestMatcher = requestMatcher; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/savedrequest/CookieRequestCacheTests.java b/web/src/test/java/org/springframework/security/web/savedrequest/CookieRequestCacheTests.java new file mode 100644 index 0000000000..584ac951d6 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/savedrequest/CookieRequestCacheTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.web.savedrequest; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Zeeshan Adnan + */ +public class CookieRequestCacheTests { + + private static final String DEFAULT_COOKIE_NAME = "REDIRECT_URI"; + + @Test + public void saveRequestWhenMatchesThenSavedRequestInACookieOnResponse() { + CookieRequestCache cookieRequestCache = new CookieRequestCache(); + + MockHttpServletRequest request = requestToSave(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + cookieRequestCache.saveRequest(request, response); + + Cookie savedCookie = response.getCookie(DEFAULT_COOKIE_NAME); + assertThat(savedCookie).isNotNull(); + + String redirectUrl = decodeCookie(savedCookie.getValue()); + assertThat(redirectUrl).isEqualTo("https://abc.com/destination?param1=a¶m2=b¶m3=1122"); + + assertThat(savedCookie.getMaxAge()).isEqualTo(-1); + assertThat(savedCookie.getPath()).isEqualTo(request.getContextPath()); + assertThat(savedCookie.isHttpOnly()).isTrue(); + assertThat(savedCookie.getSecure()).isTrue(); + + } + + @Test + public void setRequestMatcherWhenRequestMatcherIsSetNullThenThrowsIllegalArgumentException() { + CookieRequestCache cookieRequestCache = new CookieRequestCache(); + assertThatThrownBy(() -> cookieRequestCache.setRequestMatcher(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void getMatchingRequestWhenRequestMatcherDefinedThenReturnsCorrectSubsetOfCachedRequests() { + CookieRequestCache cookieRequestCache = new CookieRequestCache(); + cookieRequestCache.setRequestMatcher(request -> request.getRequestURI().equals("/expected-destination")); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/destination"); + MockHttpServletResponse response = new MockHttpServletResponse(); + cookieRequestCache.saveRequest(request, response); + + SavedRequest savedRequest = cookieRequestCache.getRequest(request, response); + assertThat(savedRequest).isNull(); + + HttpServletRequest matchingRequest = cookieRequestCache.getMatchingRequest(request, response); + assertThat(matchingRequest).isNull(); + } + + @Test + public void getRequestWhenRequestIsWithoutCookiesThenReturnsNullSavedRequest() { + CookieRequestCache cookieRequestCache = new CookieRequestCache(); + SavedRequest savedRequest = cookieRequestCache.getRequest(new MockHttpServletRequest(), new MockHttpServletResponse()); + assertThat(savedRequest).isNull(); + } + + @Test + public void getRequestWhenRequestDoesNotContainSavedRequestCookieThenReturnsNull() { + CookieRequestCache cookieRequestCache = new CookieRequestCache(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setCookies(new Cookie("abc_cookie", "value")); + SavedRequest savedRequest = cookieRequestCache.getRequest(request, new MockHttpServletResponse()); + assertThat(savedRequest).isNull(); + } + + @Test + public void getRequestWhenRequestContainsSavedRequestCookieThenReturnsSaveRequest() { + + CookieRequestCache cookieRequestCache = new CookieRequestCache(); + MockHttpServletRequest request = new MockHttpServletRequest(); + String redirectUrl = "https://abc.com/destination?param1=a¶m2=b¶m3=1122"; + request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, encodeCookie(redirectUrl))); + + SavedRequest savedRequest = cookieRequestCache.getRequest(request, new MockHttpServletResponse()); + assertThat(savedRequest).isNotNull(); + assertThat(savedRequest.getRedirectUrl()).isEqualTo(redirectUrl); + } + + @Test + public void matchingRequestWhenRequestDoesNotContainSavedRequestCookieThenReturnsNull() { + + CookieRequestCache cookieRequestCache = new CookieRequestCache(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + HttpServletRequest matchingRequest = cookieRequestCache.getMatchingRequest(new MockHttpServletRequest(), response); + assertThat(matchingRequest).isNull(); + assertThat(response.getCookie(DEFAULT_COOKIE_NAME)).isNull(); + + } + + @Test + public void matchingRequestWhenRequestContainsSavedRequestCookieThenSetsAnExpiredCookieInResponse() { + CookieRequestCache cookieRequestCache = new CookieRequestCache(); + MockHttpServletRequest request = new MockHttpServletRequest(); + String redirectUrl = "https://abc.com/destination?param1=a¶m2=b¶m3=1122"; + request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, encodeCookie(redirectUrl))); + MockHttpServletResponse response = new MockHttpServletResponse(); + + cookieRequestCache.getMatchingRequest(request, response); + Cookie expiredCookie = response.getCookie(DEFAULT_COOKIE_NAME); + assertThat(expiredCookie).isNotNull(); + assertThat(expiredCookie.getValue()).isEmpty(); + assertThat(expiredCookie.getMaxAge()).isZero(); + } + + @Test + public void removeRequestWhenInvokedThenSetsAnExpiredCookieOnResponse() { + CookieRequestCache cookieRequestCache = new CookieRequestCache(); + MockHttpServletResponse response = new MockHttpServletResponse(); + cookieRequestCache.removeRequest(new MockHttpServletRequest(), response); + Cookie expiredCookie = response.getCookie(DEFAULT_COOKIE_NAME); + assertThat(expiredCookie).isNotNull(); + assertThat(expiredCookie.getValue()).isEmpty(); + assertThat(expiredCookie.getMaxAge()).isZero(); + } + + private MockHttpServletRequest requestToSave() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setServerPort(443); + request.setSecure(true); + request.setScheme("https"); + request.setServerName("abc.com"); + request.setRequestURI("/destination"); + request.setQueryString("param1=a¶m2=b¶m3=1122"); + return request; + } + + private static String encodeCookie(String cookieValue) { + return Base64.getEncoder().encodeToString(cookieValue.getBytes()); + } + + private static String decodeCookie(String encodedCookieValue) { + return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes())); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/savedrequest/RequestCacheAwareFilterTests.java b/web/src/test/java/org/springframework/security/web/savedrequest/RequestCacheAwareFilterTests.java index c9b7e2f77b..f97f7d2369 100644 --- a/web/src/test/java/org/springframework/security/web/savedrequest/RequestCacheAwareFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/savedrequest/RequestCacheAwareFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,13 @@ import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import javax.servlet.http.Cookie; +import java.util.Base64; + public class RequestCacheAwareFilterTests { @Test - public void savedRequestIsRemovedAfterMatch() throws Exception { + public void doFilterWhenHttpSessionRequestCacheConfiguredThenSavedRequestRemovedAfterMatch() throws Exception { RequestCacheAwareFilter filter = new RequestCacheAwareFilter(); HttpSessionRequestCache cache = new HttpSessionRequestCache(); @@ -40,4 +43,34 @@ public class RequestCacheAwareFilterTests { assertThat(request.getSession().getAttribute( HttpSessionRequestCache.SAVED_REQUEST)).isNull(); } + + @Test + public void doFilterWhenCookieRequestCacheConfiguredThenExpiredSavedRequestCookieSetAfterMatch() throws Exception { + CookieRequestCache cache = new CookieRequestCache(); + RequestCacheAwareFilter filter = new RequestCacheAwareFilter(cache); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setServerName("abc.com"); + request.setRequestURI("/destination"); + request.setScheme("https"); + request.setServerPort(443); + request.setSecure(true); + + String encodedRedirectUrl = Base64.getEncoder().encodeToString("https://abc.com/destination".getBytes()); + Cookie savedRequest = new Cookie("REDIRECT_URI", encodedRedirectUrl); + savedRequest.setMaxAge(-1); + savedRequest.setSecure(request.isSecure()); + savedRequest.setPath("/"); + savedRequest.setHttpOnly(true); + request.setCookies(savedRequest); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + filter.doFilter(request, response, new MockFilterChain()); + + Cookie expiredCookie = response.getCookie("REDIRECT_URI"); + assertThat(expiredCookie).isNotNull(); + assertThat(expiredCookie.getValue()).isEmpty(); + assertThat(expiredCookie.getMaxAge()).isZero(); + } }