Skip to content

Commit 5dec894

Browse files
committed
Add HttpCookieOAuth2AuthorizationRequestRepository
Fixes spring-projectsgh-6374
1 parent 748538d commit 5dec894

File tree

2 files changed

+372
-0
lines changed

2 files changed

+372
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
package org.springframework.security.oauth2.client.web;
17+
18+
import com.fasterxml.jackson.databind.JsonNode;
19+
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import com.fasterxml.jackson.databind.node.ArrayNode;
21+
import com.fasterxml.jackson.databind.node.ObjectNode;
22+
import org.springframework.core.convert.ConversionService;
23+
import org.springframework.core.convert.support.DefaultConversionService;
24+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
25+
import org.springframework.util.Assert;
26+
27+
import javax.servlet.http.Cookie;
28+
import javax.servlet.http.HttpServletRequest;
29+
import javax.servlet.http.HttpServletResponse;
30+
import java.io.IOException;
31+
import java.util.*;
32+
33+
/**
34+
* An cookie-based implementation of an {@link AuthorizationRequestRepository}
35+
* {@link OAuth2AuthorizationRequest} useful for stateless OAuth2 clients.
36+
*
37+
* @author Gittenburg
38+
* @see AuthorizationRequestRepository
39+
* @see OAuth2AuthorizationRequest
40+
*/
41+
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
42+
private static final String COOKIE_NAME =
43+
HttpCookieOAuth2AuthorizationRequestRepository.class.getName() + ".AUTHORIZATION_REQUEST";
44+
45+
private final ObjectMapper objectMapper = new ObjectMapper();
46+
47+
private final ConversionService conversionService = new DefaultConversionService();
48+
49+
@Override
50+
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
51+
Assert.notNull(request, "request cannot be null");
52+
53+
if (request.getCookies() == null)
54+
return null;
55+
56+
Cookie cookie = Arrays.stream(request.getCookies()).filter(c -> c.getName().equals(COOKIE_NAME)).findFirst().get();
57+
if (cookie == null)
58+
return null;
59+
60+
// We do not use (de)serialization for untrusted data because that can lead to security vulnerabilities.
61+
62+
JsonNode node = null;
63+
try {
64+
node = objectMapper.reader().readTree(Base64.getDecoder().decode(cookie.getValue().getBytes()));
65+
} catch (IOException e) {
66+
return null;
67+
}
68+
69+
Map<String, Object> attributesMap = new HashMap<>();
70+
node.get("attributes").fields().forEachRemaining(entry -> {
71+
attributesMap.put(entry.getKey(), entry.getValue().textValue());
72+
});
73+
Map<String, Object> additionalParamsMap = new HashMap<>();
74+
node.get("additionalParams").fields().forEachRemaining(entry -> {
75+
additionalParamsMap.put(entry.getKey(), entry.getValue().textValue());
76+
});
77+
Set<String> scopes = new HashSet<>();
78+
node.withArray("scopes").forEach(scopeNode -> scopes.add(scopeNode.asText()));
79+
80+
return OAuth2AuthorizationRequest.authorizationCode()
81+
.authorizationUri(node.get("authorizationUri").textValue())
82+
.authorizationRequestUri(node.get("authorizationRequestUri").textValue())
83+
.clientId(node.get("clientId").textValue())
84+
.redirectUri(node.get("redirectUri").textValue())
85+
.state(node.get("state").textValue())
86+
.scopes(scopes)
87+
.attributes(attributesMap)
88+
.additionalParameters(additionalParamsMap)
89+
.build();
90+
}
91+
92+
@Override
93+
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
94+
Assert.notNull(request, "request cannot be null");
95+
Assert.notNull(response, "response cannot be null");
96+
97+
if (authorizationRequest == null){
98+
response.addCookie(expiredCookie(request));
99+
return;
100+
}
101+
102+
ObjectNode node = objectMapper.createObjectNode();
103+
node.put("authorizationUri", authorizationRequest.getAuthorizationUri());
104+
node.put("authorizationRequestUri", authorizationRequest.getAuthorizationRequestUri());
105+
106+
node.put("clientId", authorizationRequest.getClientId());
107+
node.put("redirectUri", authorizationRequest.getRedirectUri());
108+
ArrayNode scopeArrayNode = node.putArray("scopes");
109+
authorizationRequest.getScopes().forEach(scopeArrayNode::add);
110+
node.put("state", authorizationRequest.getState());
111+
112+
ObjectNode attributesNode = node.putObject("attributes");
113+
authorizationRequest.getAttributes().forEach((key, value) -> {
114+
attributesNode.put(key, conversionService.convert(value, String.class));
115+
});
116+
117+
ObjectNode additionalParamsNode = node.putObject("additionalParams");
118+
authorizationRequest.getAdditionalParameters().forEach((key, value) -> {
119+
additionalParamsNode.put(key, conversionService.convert(value, String.class));
120+
});
121+
response.addCookie(buildCookie(Base64.getEncoder().encodeToString(node.toString().getBytes()), request));
122+
}
123+
124+
/**
125+
* Builds a cookie to store the authorization request
126+
* @param value the value to save in the cookie
127+
* @param request the request to which we will respond with the cookie
128+
* @return the created cookie
129+
*/
130+
private Cookie buildCookie(String value, HttpServletRequest request){
131+
Cookie cookie = new Cookie(COOKIE_NAME, value);
132+
cookie.setPath("/");
133+
cookie.setHttpOnly(true);
134+
cookie.setSecure(request.isSecure());
135+
cookie.setMaxAge(120); // expire after two minutes
136+
return cookie;
137+
}
138+
139+
/**
140+
* Builds an expired cookie
141+
* @param request the request to which we will respond with the cookie
142+
* @return the expired cookie
143+
*/
144+
private Cookie expiredCookie(HttpServletRequest request){
145+
Cookie cookie = buildCookie("", request);
146+
cookie.setMaxAge(0);
147+
return cookie;
148+
}
149+
150+
@Override
151+
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
152+
Assert.notNull(request, "request cannot be null");
153+
Assert.notNull(response, "response cannot be null");
154+
response.addCookie(expiredCookie(request));
155+
return loadAuthorizationRequest(request);
156+
}
157+
158+
@Override
159+
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
160+
Assert.notNull(request, "request cannot be null");
161+
// we cannot actually remove the authorizationRequest here because we don't have access to the httpServletResponse
162+
return loadAuthorizationRequest(request);
163+
}
164+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
package org.springframework.security.oauth2.client.web;
17+
18+
import org.junit.Test;
19+
import org.junit.runner.RunWith;
20+
import org.mockito.junit.MockitoJUnitRunner;
21+
import org.springframework.mock.web.MockHttpServletRequest;
22+
import org.springframework.mock.web.MockHttpServletResponse;
23+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
24+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
25+
26+
import javax.servlet.http.Cookie;
27+
import java.util.HashMap;
28+
import java.util.Map;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
32+
33+
/**
34+
* Tests for {@link HttpCookieOAuth2AuthorizationRequestRepository}.
35+
*
36+
* @author Gittenburg
37+
*/
38+
@RunWith(MockitoJUnitRunner.class)
39+
public class HttpCookieOAuth2AuthorizationRequestRepositoryTests {
40+
private HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository =
41+
new HttpCookieOAuth2AuthorizationRequestRepository();
42+
43+
@Test(expected = IllegalArgumentException.class)
44+
public void loadAuthorizationRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() {
45+
this.authorizationRequestRepository.loadAuthorizationRequest(null);
46+
}
47+
48+
@Test
49+
public void loadAuthorizationRequestWhenNotSavedThenReturnNull() {
50+
MockHttpServletRequest request = new MockHttpServletRequest();
51+
request.addParameter(OAuth2ParameterNames.STATE, "state-1234");
52+
OAuth2AuthorizationRequest authorizationRequest =
53+
this.authorizationRequestRepository.loadAuthorizationRequest(request);
54+
55+
assertThat(authorizationRequest).isNull();
56+
}
57+
58+
@Test
59+
public void loadAuthorizationRequestWhenSavedAndStateParameterNullThenReturnNull() {
60+
MockHttpServletRequest request = new MockHttpServletRequest();
61+
62+
OAuth2AuthorizationRequest authorizationRequest = createAuthorizationRequest().build();
63+
this.authorizationRequestRepository.saveAuthorizationRequest(
64+
authorizationRequest, request, new MockHttpServletResponse());
65+
66+
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(request)).isNull();
67+
}
68+
69+
@Test
70+
public void saveAuthorizationRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() {
71+
OAuth2AuthorizationRequest authorizationRequest = createAuthorizationRequest().build();
72+
73+
assertThatThrownBy(() -> this.authorizationRequestRepository.saveAuthorizationRequest(
74+
authorizationRequest, null, new MockHttpServletResponse()))
75+
.isInstanceOf(IllegalArgumentException.class);
76+
}
77+
78+
@Test
79+
public void saveAuthorizationRequestWhenHttpServletResponseIsNullThenThrowIllegalArgumentException() {
80+
OAuth2AuthorizationRequest authorizationRequest = createAuthorizationRequest().build();
81+
82+
assertThatThrownBy(() -> this.authorizationRequestRepository.saveAuthorizationRequest(
83+
authorizationRequest, new MockHttpServletRequest(), null))
84+
.isInstanceOf(IllegalArgumentException.class);
85+
}
86+
87+
@Test
88+
public void saveAuthorizationRequestWhenNotNullThenSaved() {
89+
MockHttpServletRequest request = new MockHttpServletRequest();
90+
MockHttpServletResponse response = new MockHttpServletResponse();
91+
92+
OAuth2AuthorizationRequest authorizationRequest = createAuthorizationRequest().build();
93+
this.authorizationRequestRepository.saveAuthorizationRequest(
94+
authorizationRequest, request, response);
95+
96+
request.setCookies(response.getCookies());
97+
98+
OAuth2AuthorizationRequest loadedAuthorizationRequest =
99+
this.authorizationRequestRepository.loadAuthorizationRequest(request);
100+
101+
assertThat(loadedAuthorizationRequest.getAttributes()).isEqualTo(authorizationRequest.getAttributes());
102+
assertThat(loadedAuthorizationRequest.getAdditionalParameters()).isEqualTo(authorizationRequest.getAdditionalParameters());
103+
assertThat(loadedAuthorizationRequest.getAuthorizationRequestUri()).isEqualTo(authorizationRequest.getAuthorizationRequestUri());
104+
assertThat(loadedAuthorizationRequest.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri());
105+
assertThat(loadedAuthorizationRequest.getGrantType()).isEqualTo(authorizationRequest.getGrantType());
106+
assertThat(loadedAuthorizationRequest.getRedirectUri()).isEqualTo(authorizationRequest.getRedirectUri());
107+
assertThat(loadedAuthorizationRequest.getScopes()).isEqualTo(authorizationRequest.getScopes());
108+
assertThat(loadedAuthorizationRequest.getState()).isEqualTo(authorizationRequest.getState());
109+
assertThat(loadedAuthorizationRequest.getClientId()).isEqualTo(authorizationRequest.getClientId());
110+
}
111+
112+
@Test
113+
public void saveAuthorizationRequestWhenRequestInsecureThenCookiesInsecure(){
114+
MockHttpServletRequest request = new MockHttpServletRequest();
115+
MockHttpServletResponse response = new MockHttpServletResponse();
116+
request.setSecure(false);
117+
118+
OAuth2AuthorizationRequest authorizationRequest = createAuthorizationRequest().build();
119+
this.authorizationRequestRepository.saveAuthorizationRequest(
120+
authorizationRequest, request, response);
121+
for (Cookie cookie: response.getCookies()){
122+
assertThat(!cookie.getSecure());
123+
}
124+
}
125+
126+
@Test
127+
public void saveAuthorizationRequestWhenRequestSecureThenCookiesSecure(){
128+
MockHttpServletRequest request = new MockHttpServletRequest();
129+
MockHttpServletResponse response = new MockHttpServletResponse();
130+
request.setSecure(true);
131+
132+
OAuth2AuthorizationRequest authorizationRequest = createAuthorizationRequest().build();
133+
this.authorizationRequestRepository.saveAuthorizationRequest(
134+
authorizationRequest, request, response);
135+
136+
assertThat(response.getCookies().length).isEqualTo(1);
137+
assertThat(response.getCookies()[0].getSecure());
138+
}
139+
140+
@Test
141+
public void saveAuthorizationRequestWhenNullThenCookiesExpired() {
142+
MockHttpServletRequest request = new MockHttpServletRequest();
143+
MockHttpServletResponse response = new MockHttpServletResponse();
144+
145+
OAuth2AuthorizationRequest authorizationRequest = createAuthorizationRequest().build();
146+
147+
this.authorizationRequestRepository.saveAuthorizationRequest(
148+
null, request, response);
149+
150+
assertThat(response.getCookies().length).isEqualTo(1);
151+
assertThat(response.getCookies()[0].getMaxAge()).isEqualTo(0);
152+
}
153+
154+
@Test
155+
public void removeAuthorizationRequestWhenNotNullThenCookiesExpired() {
156+
MockHttpServletRequest request = new MockHttpServletRequest();
157+
MockHttpServletResponse response = new MockHttpServletResponse();
158+
159+
this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
160+
161+
assertThat(response.getCookies().length).isEqualTo(1);
162+
assertThat(response.getCookies()[0].getMaxAge()).isEqualTo(0);
163+
}
164+
165+
@Test
166+
public void removeAuthorizationRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() {
167+
assertThatThrownBy(() -> this.authorizationRequestRepository.removeAuthorizationRequest(
168+
null, new MockHttpServletResponse())).isInstanceOf(IllegalArgumentException.class);
169+
}
170+
171+
@Test
172+
public void removeAuthorizationRequestWhenHttpServletResponseIsNullThenThrowIllegalArgumentException() {
173+
assertThatThrownBy(() -> this.authorizationRequestRepository.removeAuthorizationRequest(
174+
new MockHttpServletRequest(), null)).isInstanceOf(IllegalArgumentException.class);
175+
}
176+
177+
@Test
178+
public void removeAuthorizationRequestWhenNotSavedThenNotRemoved() {
179+
MockHttpServletRequest request = new MockHttpServletRequest();
180+
request.addParameter(OAuth2ParameterNames.STATE, "state-1234");
181+
182+
MockHttpServletResponse response = new MockHttpServletResponse();
183+
184+
OAuth2AuthorizationRequest removedAuthorizationRequest =
185+
this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
186+
187+
assertThat(removedAuthorizationRequest).isNull();
188+
}
189+
190+
private OAuth2AuthorizationRequest.Builder createAuthorizationRequest() {
191+
Map<String, Object> additionalParams = new HashMap<>();
192+
additionalParams.put("param1", "value1");
193+
additionalParams.put("param2", "value2");
194+
195+
Map<String, Object> attributes = new HashMap<>();
196+
attributes.put("attr1", "value1");
197+
attributes.put("attr2", "value2");
198+
199+
return OAuth2AuthorizationRequest.authorizationCode()
200+
.authorizationUri("https://example.com/oauth2/authorize")
201+
.authorizationRequestUri("https://example.com/example")
202+
.scope("one", "two", "three")
203+
.clientId("client-id-1234")
204+
.additionalParameters(additionalParams)
205+
.attributes(attributes)
206+
.state("state-1234");
207+
}
208+
}

0 commit comments

Comments
 (0)