Pārlūkot izejas kodu

Add server request cache that uses cookie

Fixes: gh-8033
Eleftheria Stein 5 gadi atpakaļ
vecāks
revīzija
ae532c080c

+ 131 - 0
web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.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.server.savedrequest;
+
+import org.springframework.http.HttpCookie;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseCookie;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
+import org.springframework.util.Assert;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Collections;
+
+/**
+ * An implementation of {@link ServerRequestCache} that saves the
+ * requested URI in a cookie.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ */
+public class CookieServerRequestCache implements ServerRequestCache {
+	private static final String REDIRECT_URI_COOKIE_NAME = "REDIRECT_URI";
+
+	private static final Duration COOKIE_MAX_AGE = Duration.ofSeconds(-1);
+
+	private ServerWebExchangeMatcher saveRequestMatcher = createDefaultRequestMatcher();
+
+	/**
+	 * Sets the matcher to determine if the request should be saved. The default is to match
+	 * on any GET request.
+	 *
+	 * @param saveRequestMatcher the {@link ServerWebExchangeMatcher} that determines if
+	 *                           the request should be saved
+	 */
+	public void setSaveRequestMatcher(ServerWebExchangeMatcher saveRequestMatcher) {
+		Assert.notNull(saveRequestMatcher, "saveRequestMatcher cannot be null");
+		this.saveRequestMatcher = saveRequestMatcher;
+	}
+
+	@Override
+	public Mono<Void> saveRequest(ServerWebExchange exchange) {
+		return this.saveRequestMatcher.matches(exchange)
+				.filter(m -> m.isMatch())
+				.map(m -> exchange.getResponse())
+				.map(ServerHttpResponse::getCookies)
+				.doOnNext(cookies -> cookies.add(REDIRECT_URI_COOKIE_NAME, createRedirectUriCookie(exchange.getRequest())))
+				.then();
+	}
+
+	@Override
+	public Mono<URI> getRedirectUri(ServerWebExchange exchange) {
+		MultiValueMap<String, HttpCookie> cookieMap = exchange.getRequest().getCookies();
+		return Mono.justOrEmpty(cookieMap.getFirst(REDIRECT_URI_COOKIE_NAME))
+				.map(HttpCookie::getValue)
+				.map(CookieServerRequestCache::decodeCookie)
+				.onErrorResume(IllegalArgumentException.class, e -> Mono.empty())
+				.map(URI::create);
+	}
+
+	@Override
+	public Mono<ServerHttpRequest> removeMatchingRequest(ServerWebExchange exchange) {
+		return Mono.just(exchange.getResponse())
+				.map(ServerHttpResponse::getCookies)
+				.doOnNext(cookies -> cookies.add(REDIRECT_URI_COOKIE_NAME, invalidateRedirectUriCookie(exchange.getRequest())))
+				.thenReturn(exchange.getRequest());
+	}
+
+	private static ResponseCookie createRedirectUriCookie(ServerHttpRequest request) {
+		String path = request.getPath().pathWithinApplication().value();
+		String query = request.getURI().getRawQuery();
+		String redirectUri = path + (query != null ? "?" + query : "");
+
+		return createResponseCookie(request, encodeCookie(redirectUri), COOKIE_MAX_AGE);
+	}
+
+	private static ResponseCookie invalidateRedirectUriCookie(ServerHttpRequest request) {
+		return createResponseCookie(request, null, Duration.ZERO);
+	}
+
+	private static ResponseCookie createResponseCookie(ServerHttpRequest request, String cookieValue, Duration age) {
+		return ResponseCookie.from(REDIRECT_URI_COOKIE_NAME, cookieValue)
+				.path(request.getPath().contextPath().value() + "/")
+				.maxAge(age)
+				.httpOnly(true)
+				.secure("https".equalsIgnoreCase(request.getURI().getScheme()))
+				.sameSite("Lax")
+				.build();
+	}
+
+	private static String encodeCookie(String cookieValue) {
+		return new String(Base64.getEncoder().encode(cookieValue.getBytes()));
+	}
+
+	private static String decodeCookie(String encodedCookieValue) {
+		return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
+	}
+
+	private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
+		ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
+		ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers("/favicon.*"));
+		MediaTypeServerWebExchangeMatcher html = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
+		html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
+		return new AndServerWebExchangeMatcher(get, notFavicon, html);
+	}
+}

+ 145 - 0
web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java

@@ -0,0 +1,145 @@
+/*
+ * 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.server.savedrequest;
+
+import org.junit.Test;
+import org.springframework.http.HttpCookie;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseCookie;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.util.MultiValueMap;
+
+import java.net.URI;
+import java.util.Base64;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link CookieServerRequestCache}
+ *
+ * @author Eleftheria Stein
+ */
+public class CookieServerRequestCacheTests {
+	private CookieServerRequestCache cache = new CookieServerRequestCache();
+
+	@Test
+	public void saveRequestWhenGetRequestThenRequestUriInCookie() {
+		MockServerWebExchange exchange = MockServerWebExchange
+				.from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML));
+		this.cache.saveRequest(exchange).block();
+
+		MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
+		assertThat(cookies.size()).isEqualTo(1);
+		ResponseCookie cookie = cookies.getFirst("REDIRECT_URI");
+		assertThat(cookie).isNotNull();
+		String encodedRedirectUrl = Base64.getEncoder().encodeToString("/secured/".getBytes());
+		assertThat(cookie.toString()).isEqualTo("REDIRECT_URI=" + encodedRedirectUrl + "; Path=/; HttpOnly; SameSite=Lax");
+	}
+
+	@Test
+	public void saveRequestWhenGetRequestWithQueryParamsThenRequestUriInCookie() {
+		MockServerWebExchange exchange = MockServerWebExchange
+				.from(MockServerHttpRequest.get("/secured/").queryParam("key", "value").accept(MediaType.TEXT_HTML));
+		this.cache.saveRequest(exchange).block();
+
+		MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
+		assertThat(cookies.size()).isEqualTo(1);
+		ResponseCookie cookie = cookies.getFirst("REDIRECT_URI");
+		assertThat(cookie).isNotNull();
+		String encodedRedirectUrl = Base64.getEncoder().encodeToString("/secured/?key=value".getBytes());
+		assertThat(cookie.toString()).isEqualTo("REDIRECT_URI=" + encodedRedirectUrl + "; Path=/; HttpOnly; SameSite=Lax");
+	}
+
+	@Test
+	public void saveRequestWhenGetRequestFaviconThenNoCookie() {
+		MockServerWebExchange exchange = MockServerWebExchange
+				.from(MockServerHttpRequest.get("/favicon.png").accept(MediaType.TEXT_HTML));
+		this.cache.saveRequest(exchange).block();
+
+		MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
+		assertThat(cookies).isEmpty();
+	}
+
+	@Test
+	public void saveRequestWhenPostRequestThenNoCookie() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
+		this.cache.saveRequest(exchange).block();
+
+		MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
+		assertThat(cookies).isEmpty();
+	}
+
+	@Test
+	public void saveRequestWhenPostRequestAndCustomMatcherThenRequestUriInCookie() {
+		this.cache.setSaveRequestMatcher(e -> ServerWebExchangeMatcher.MatchResult.match());
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
+		this.cache.saveRequest(exchange).block();
+
+		MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
+		ResponseCookie cookie = cookies.getFirst("REDIRECT_URI");
+		assertThat(cookie).isNotNull();
+
+		String encodedRedirectUrl = Base64.getEncoder().encodeToString("/secured/".getBytes());
+		assertThat(cookie.toString()).isEqualTo("REDIRECT_URI=" + encodedRedirectUrl + "; Path=/; HttpOnly; SameSite=Lax");
+	}
+
+	@Test
+	public void getRedirectUriWhenCookieThenReturnsRedirectUriFromCookie() {
+		String encodedRedirectUrl = Base64.getEncoder().encodeToString("/secured/".getBytes());
+		MockServerWebExchange exchange = MockServerWebExchange
+				.from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML).cookie(new HttpCookie("REDIRECT_URI", encodedRedirectUrl)));
+
+		URI redirectUri = this.cache.getRedirectUri(exchange).block();
+
+		assertThat(redirectUri).isEqualTo(URI.create("/secured/"));
+	}
+
+	@Test
+	public void getRedirectUriWhenCookieValueNotEncodedThenRedirectUriIsNull() {
+		MockServerWebExchange exchange = MockServerWebExchange
+				.from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML).cookie(new HttpCookie("REDIRECT_URI", "/secured/")));
+
+		URI redirectUri = this.cache.getRedirectUri(exchange).block();
+
+		assertThat(redirectUri).isNull();
+	}
+
+	@Test
+	public void getRedirectUriWhenNoCookieThenRedirectUriIsNull() {
+		MockServerWebExchange exchange = MockServerWebExchange
+				.from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML));
+
+		URI redirectUri = this.cache.getRedirectUri(exchange).block();
+
+		assertThat(redirectUri).isNull();
+	}
+
+	@Test
+	public void removeMatchingRequestThenRedirectUriCookieExpired() {
+		MockServerWebExchange exchange = MockServerWebExchange
+				.from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML).cookie(new HttpCookie("REDIRECT_URI", "/secured/")));
+
+		this.cache.removeMatchingRequest(exchange).block();
+
+		MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
+		ResponseCookie cookie = cookies.getFirst("REDIRECT_URI");
+		assertThat(cookie).isNotNull();
+		assertThat(cookie.toString()).isEqualTo("REDIRECT_URI=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Lax");
+	}
+}