2 changed files with 276 additions and 0 deletions
@ -0,0 +1,131 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,145 @@
@@ -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"); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue