Skip to content

Commit ae532c0

Browse files
committed
Add server request cache that uses cookie
Fixes: gh-8033
1 parent 38979b1 commit ae532c0

File tree

2 files changed

+276
-0
lines changed

2 files changed

+276
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.server.savedrequest;
18+
19+
import org.springframework.http.HttpCookie;
20+
import org.springframework.http.HttpMethod;
21+
import org.springframework.http.MediaType;
22+
import org.springframework.http.ResponseCookie;
23+
import org.springframework.http.server.reactive.ServerHttpRequest;
24+
import org.springframework.http.server.reactive.ServerHttpResponse;
25+
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
26+
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
27+
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
28+
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
29+
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
30+
import org.springframework.util.Assert;
31+
import org.springframework.util.MultiValueMap;
32+
import org.springframework.web.server.ServerWebExchange;
33+
import reactor.core.publisher.Mono;
34+
35+
import java.net.URI;
36+
import java.time.Duration;
37+
import java.util.Base64;
38+
import java.util.Collections;
39+
40+
/**
41+
* An implementation of {@link ServerRequestCache} that saves the
42+
* requested URI in a cookie.
43+
*
44+
* @author Eleftheria Stein
45+
* @since 5.4
46+
*/
47+
public class CookieServerRequestCache implements ServerRequestCache {
48+
private static final String REDIRECT_URI_COOKIE_NAME = "REDIRECT_URI";
49+
50+
private static final Duration COOKIE_MAX_AGE = Duration.ofSeconds(-1);
51+
52+
private ServerWebExchangeMatcher saveRequestMatcher = createDefaultRequestMatcher();
53+
54+
/**
55+
* Sets the matcher to determine if the request should be saved. The default is to match
56+
* on any GET request.
57+
*
58+
* @param saveRequestMatcher the {@link ServerWebExchangeMatcher} that determines if
59+
* the request should be saved
60+
*/
61+
public void setSaveRequestMatcher(ServerWebExchangeMatcher saveRequestMatcher) {
62+
Assert.notNull(saveRequestMatcher, "saveRequestMatcher cannot be null");
63+
this.saveRequestMatcher = saveRequestMatcher;
64+
}
65+
66+
@Override
67+
public Mono<Void> saveRequest(ServerWebExchange exchange) {
68+
return this.saveRequestMatcher.matches(exchange)
69+
.filter(m -> m.isMatch())
70+
.map(m -> exchange.getResponse())
71+
.map(ServerHttpResponse::getCookies)
72+
.doOnNext(cookies -> cookies.add(REDIRECT_URI_COOKIE_NAME, createRedirectUriCookie(exchange.getRequest())))
73+
.then();
74+
}
75+
76+
@Override
77+
public Mono<URI> getRedirectUri(ServerWebExchange exchange) {
78+
MultiValueMap<String, HttpCookie> cookieMap = exchange.getRequest().getCookies();
79+
return Mono.justOrEmpty(cookieMap.getFirst(REDIRECT_URI_COOKIE_NAME))
80+
.map(HttpCookie::getValue)
81+
.map(CookieServerRequestCache::decodeCookie)
82+
.onErrorResume(IllegalArgumentException.class, e -> Mono.empty())
83+
.map(URI::create);
84+
}
85+
86+
@Override
87+
public Mono<ServerHttpRequest> removeMatchingRequest(ServerWebExchange exchange) {
88+
return Mono.just(exchange.getResponse())
89+
.map(ServerHttpResponse::getCookies)
90+
.doOnNext(cookies -> cookies.add(REDIRECT_URI_COOKIE_NAME, invalidateRedirectUriCookie(exchange.getRequest())))
91+
.thenReturn(exchange.getRequest());
92+
}
93+
94+
private static ResponseCookie createRedirectUriCookie(ServerHttpRequest request) {
95+
String path = request.getPath().pathWithinApplication().value();
96+
String query = request.getURI().getRawQuery();
97+
String redirectUri = path + (query != null ? "?" + query : "");
98+
99+
return createResponseCookie(request, encodeCookie(redirectUri), COOKIE_MAX_AGE);
100+
}
101+
102+
private static ResponseCookie invalidateRedirectUriCookie(ServerHttpRequest request) {
103+
return createResponseCookie(request, null, Duration.ZERO);
104+
}
105+
106+
private static ResponseCookie createResponseCookie(ServerHttpRequest request, String cookieValue, Duration age) {
107+
return ResponseCookie.from(REDIRECT_URI_COOKIE_NAME, cookieValue)
108+
.path(request.getPath().contextPath().value() + "/")
109+
.maxAge(age)
110+
.httpOnly(true)
111+
.secure("https".equalsIgnoreCase(request.getURI().getScheme()))
112+
.sameSite("Lax")
113+
.build();
114+
}
115+
116+
private static String encodeCookie(String cookieValue) {
117+
return new String(Base64.getEncoder().encode(cookieValue.getBytes()));
118+
}
119+
120+
private static String decodeCookie(String encodedCookieValue) {
121+
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
122+
}
123+
124+
private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
125+
ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
126+
ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers("/favicon.*"));
127+
MediaTypeServerWebExchangeMatcher html = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
128+
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
129+
return new AndServerWebExchangeMatcher(get, notFavicon, html);
130+
}
131+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.server.savedrequest;
18+
19+
import org.junit.Test;
20+
import org.springframework.http.HttpCookie;
21+
import org.springframework.http.MediaType;
22+
import org.springframework.http.ResponseCookie;
23+
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
24+
import org.springframework.mock.web.server.MockServerWebExchange;
25+
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
26+
import org.springframework.util.MultiValueMap;
27+
28+
import java.net.URI;
29+
import java.util.Base64;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
33+
/**
34+
* Tests for {@link CookieServerRequestCache}
35+
*
36+
* @author Eleftheria Stein
37+
*/
38+
public class CookieServerRequestCacheTests {
39+
private CookieServerRequestCache cache = new CookieServerRequestCache();
40+
41+
@Test
42+
public void saveRequestWhenGetRequestThenRequestUriInCookie() {
43+
MockServerWebExchange exchange = MockServerWebExchange
44+
.from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML));
45+
this.cache.saveRequest(exchange).block();
46+
47+
MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
48+
assertThat(cookies.size()).isEqualTo(1);
49+
ResponseCookie cookie = cookies.getFirst("REDIRECT_URI");
50+
assertThat(cookie).isNotNull();
51+
String encodedRedirectUrl = Base64.getEncoder().encodeToString("/secured/".getBytes());
52+
assertThat(cookie.toString()).isEqualTo("REDIRECT_URI=" + encodedRedirectUrl + "; Path=/; HttpOnly; SameSite=Lax");
53+
}
54+
55+
@Test
56+
public void saveRequestWhenGetRequestWithQueryParamsThenRequestUriInCookie() {
57+
MockServerWebExchange exchange = MockServerWebExchange
58+
.from(MockServerHttpRequest.get("/secured/").queryParam("key", "value").accept(MediaType.TEXT_HTML));
59+
this.cache.saveRequest(exchange).block();
60+
61+
MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
62+
assertThat(cookies.size()).isEqualTo(1);
63+
ResponseCookie cookie = cookies.getFirst("REDIRECT_URI");
64+
assertThat(cookie).isNotNull();
65+
String encodedRedirectUrl = Base64.getEncoder().encodeToString("/secured/?key=value".getBytes());
66+
assertThat(cookie.toString()).isEqualTo("REDIRECT_URI=" + encodedRedirectUrl + "; Path=/; HttpOnly; SameSite=Lax");
67+
}
68+
69+
@Test
70+
public void saveRequestWhenGetRequestFaviconThenNoCookie() {
71+
MockServerWebExchange exchange = MockServerWebExchange
72+
.from(MockServerHttpRequest.get("/favicon.png").accept(MediaType.TEXT_HTML));
73+
this.cache.saveRequest(exchange).block();
74+
75+
MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
76+
assertThat(cookies).isEmpty();
77+
}
78+
79+
@Test
80+
public void saveRequestWhenPostRequestThenNoCookie() {
81+
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
82+
this.cache.saveRequest(exchange).block();
83+
84+
MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
85+
assertThat(cookies).isEmpty();
86+
}
87+
88+
@Test
89+
public void saveRequestWhenPostRequestAndCustomMatcherThenRequestUriInCookie() {
90+
this.cache.setSaveRequestMatcher(e -> ServerWebExchangeMatcher.MatchResult.match());
91+
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
92+
this.cache.saveRequest(exchange).block();
93+
94+
MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
95+
ResponseCookie cookie = cookies.getFirst("REDIRECT_URI");
96+
assertThat(cookie).isNotNull();
97+
98+
String encodedRedirectUrl = Base64.getEncoder().encodeToString("/secured/".getBytes());
99+
assertThat(cookie.toString()).isEqualTo("REDIRECT_URI=" + encodedRedirectUrl + "; Path=/; HttpOnly; SameSite=Lax");
100+
}
101+
102+
@Test
103+
public void getRedirectUriWhenCookieThenReturnsRedirectUriFromCookie() {
104+
String encodedRedirectUrl = Base64.getEncoder().encodeToString("/secured/".getBytes());
105+
MockServerWebExchange exchange = MockServerWebExchange
106+
.from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML).cookie(new HttpCookie("REDIRECT_URI", encodedRedirectUrl)));
107+
108+
URI redirectUri = this.cache.getRedirectUri(exchange).block();
109+
110+
assertThat(redirectUri).isEqualTo(URI.create("/secured/"));
111+
}
112+
113+
@Test
114+
public void getRedirectUriWhenCookieValueNotEncodedThenRedirectUriIsNull() {
115+
MockServerWebExchange exchange = MockServerWebExchange
116+
.from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML).cookie(new HttpCookie("REDIRECT_URI", "/secured/")));
117+
118+
URI redirectUri = this.cache.getRedirectUri(exchange).block();
119+
120+
assertThat(redirectUri).isNull();
121+
}
122+
123+
@Test
124+
public void getRedirectUriWhenNoCookieThenRedirectUriIsNull() {
125+
MockServerWebExchange exchange = MockServerWebExchange
126+
.from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML));
127+
128+
URI redirectUri = this.cache.getRedirectUri(exchange).block();
129+
130+
assertThat(redirectUri).isNull();
131+
}
132+
133+
@Test
134+
public void removeMatchingRequestThenRedirectUriCookieExpired() {
135+
MockServerWebExchange exchange = MockServerWebExchange
136+
.from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML).cookie(new HttpCookie("REDIRECT_URI", "/secured/")));
137+
138+
this.cache.removeMatchingRequest(exchange).block();
139+
140+
MultiValueMap<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
141+
ResponseCookie cookie = cookies.getFirst("REDIRECT_URI");
142+
assertThat(cookie).isNotNull();
143+
assertThat(cookie.toString()).isEqualTo("REDIRECT_URI=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Lax");
144+
}
145+
}

0 commit comments

Comments
 (0)