Skip to content
This repository was archived by the owner on May 31, 2022. It is now read-only.

Commit 16d39ad

Browse files
phrinxjgrandja
authored andcommitted
DefaultRedirectResolver matches on userinfo and query
Fixes gh-1585
1 parent 96f85b0 commit 16d39ad

File tree

5 files changed

+201
-33
lines changed

5 files changed

+201
-33
lines changed

spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/DefaultRedirectResolver.java

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,13 +21,16 @@
2121
import org.springframework.security.oauth2.common.exceptions.RedirectMismatchException;
2222
import org.springframework.security.oauth2.provider.ClientDetails;
2323
import org.springframework.util.Assert;
24+
import org.springframework.util.MultiValueMap;
2425
import org.springframework.util.StringUtils;
2526
import org.springframework.web.util.UriComponents;
2627
import org.springframework.web.util.UriComponentsBuilder;
2728

2829
import java.util.Arrays;
2930
import java.util.Collection;
3031
import java.util.HashSet;
32+
import java.util.Iterator;
33+
import java.util.List;
3134
import java.util.Set;
3235

3336
/**
@@ -47,7 +50,7 @@ public class DefaultRedirectResolver implements RedirectResolver {
4750
/**
4851
* Flag to indicate that requested URIs will match if they are a subdomain of the registered value.
4952
*
50-
* @param matchSubdomains the flag value to set (deafult true)
53+
* @param matchSubdomains the flag value to set (default true)
5154
*/
5255
public void setMatchSubdomains(boolean matchSubdomains) {
5356
this.matchSubdomains = matchSubdomains;
@@ -105,7 +108,8 @@ private boolean containsRedirectGrantType(Set<String> grantTypes) {
105108
/**
106109
* Whether the requested redirect URI "matches" the specified redirect URI. For a URL, this implementation tests if
107110
* the user requested redirect starts with the registered redirect, so it would have the same host and root path if
108-
* it is an HTTP URL. The port is also matched.
111+
* it is an HTTP URL. The port, userinfo, query params also matched. Request redirect uri path can include
112+
* additional parameters which are ignored for the match
109113
* <p>
110114
* For other (non-URL) cases, such as for some implicit clients, the redirect_uri must be an exact match.
111115
*
@@ -115,22 +119,65 @@ private boolean containsRedirectGrantType(Set<String> grantTypes) {
115119
*/
116120
protected boolean redirectMatches(String requestedRedirect, String redirectUri) {
117121
UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();
118-
String requestedRedirectUriScheme = (requestedRedirectUri.getScheme() != null ? requestedRedirectUri.getScheme() : "");
119-
String requestedRedirectUriHost = (requestedRedirectUri.getHost() != null ? requestedRedirectUri.getHost() : "");
120-
String requestedRedirectUriPath = (requestedRedirectUri.getPath() != null ? requestedRedirectUri.getPath() : "");
121-
122122
UriComponents registeredRedirectUri = UriComponentsBuilder.fromUriString(redirectUri).build();
123-
String registeredRedirectUriScheme = (registeredRedirectUri.getScheme() != null ? registeredRedirectUri.getScheme() : "");
124-
String registeredRedirectUriHost = (registeredRedirectUri.getHost() != null ? registeredRedirectUri.getHost() : "");
125-
String registeredRedirectUriPath = (registeredRedirectUri.getPath() != null ? registeredRedirectUri.getPath() : "");
126123

127-
boolean portsMatch = this.matchPorts ? (registeredRedirectUri.getPort() == requestedRedirectUri.getPort()) : true;
124+
boolean schemeMatch = isEqual(registeredRedirectUri.getScheme(), requestedRedirectUri.getScheme());
125+
boolean userInfoMatch = isEqual(registeredRedirectUri.getUserInfo(), requestedRedirectUri.getUserInfo());
126+
boolean hostMatch = hostMatches(registeredRedirectUri.getHost(), requestedRedirectUri.getHost());
127+
boolean portMatch = matchPorts ? registeredRedirectUri.getPort() == requestedRedirectUri.getPort() : true;
128+
boolean pathMatch = isEqual(registeredRedirectUri.getPath(),
129+
StringUtils.cleanPath(requestedRedirectUri.getPath()));
130+
boolean queryParamMatch = matchQueryParams(registeredRedirectUri.getQueryParams(),
131+
requestedRedirectUri.getQueryParams());
132+
133+
return schemeMatch && userInfoMatch && hostMatch && portMatch && pathMatch && queryParamMatch;
134+
}
135+
136+
137+
/**
138+
* Checks whether the registered redirect uri query params key and values contains match the requested set
139+
*
140+
* The requested redirect uri query params are allowed to contain additional params which will be retained
141+
*
142+
* @param registeredRedirectUriQueryParams
143+
* @param requestedRedirectUriQueryParams
144+
* @return whether the params match
145+
*/
146+
private boolean matchQueryParams(MultiValueMap<String, String> registeredRedirectUriQueryParams,
147+
MultiValueMap<String, String> requestedRedirectUriQueryParams) {
148+
149+
150+
Iterator<String> iter = registeredRedirectUriQueryParams.keySet().iterator();
151+
while (iter.hasNext()) {
152+
String key = iter.next();
153+
List<String> registeredRedirectUriQueryParamsValues = registeredRedirectUriQueryParams.get(key);
154+
List<String> requestedRedirectUriQueryParamsValues = requestedRedirectUriQueryParams.get(key);
155+
156+
if (!registeredRedirectUriQueryParamsValues.equals(requestedRedirectUriQueryParamsValues)) {
157+
return false;
158+
}
159+
}
160+
161+
return true;
162+
}
163+
164+
128165

129-
return registeredRedirectUriScheme.equals(requestedRedirectUriScheme) &&
130-
hostMatches(registeredRedirectUriHost, requestedRedirectUriHost) &&
131-
portsMatch &&
132-
// Ensure exact path matching
133-
registeredRedirectUriPath.equals(StringUtils.cleanPath(requestedRedirectUriPath));
166+
/**
167+
* Compares two strings but treats empty string or null equal
168+
*
169+
* @param str1
170+
* @param str2
171+
* @return true if strings are equal, false otherwise
172+
*/
173+
private boolean isEqual(String str1, String str2) {
174+
if (StringUtils.isEmpty(str1) && StringUtils.isEmpty(str2)) {
175+
return true;
176+
} else if (!StringUtils.isEmpty(str1)) {
177+
return str1.equals(str2);
178+
} else {
179+
return false;
180+
}
134181
}
135182

136183
/**
@@ -152,7 +199,7 @@ protected boolean hostMatches(String registered, String requested) {
152199
*
153200
* @param redirectUris the set of the registered URIs to try and find a match. This cannot be null or empty.
154201
* @param requestedRedirect the URI used as part of the request
155-
* @return the matching URI
202+
* @return redirect uri
156203
* @throws RedirectMismatchException if no match was found
157204
*/
158205
private String obtainMatchingRedirect(Set<String> redirectUris, String requestedRedirect) {
@@ -161,11 +208,26 @@ private String obtainMatchingRedirect(Set<String> redirectUris, String requested
161208
if (redirectUris.size() == 1 && requestedRedirect == null) {
162209
return redirectUris.iterator().next();
163210
}
211+
164212
for (String redirectUri : redirectUris) {
165213
if (requestedRedirect != null && redirectMatches(requestedRedirect, redirectUri)) {
166-
return requestedRedirect;
214+
// Initialize with the registered redirect-uri
215+
UriComponentsBuilder redirectUriBuilder = UriComponentsBuilder.fromUriString(redirectUri);
216+
217+
UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();
218+
219+
if (this.matchSubdomains) {
220+
redirectUriBuilder.host(requestedRedirectUri.getHost());
221+
}
222+
if (!this.matchPorts) {
223+
redirectUriBuilder.port(requestedRedirectUri.getPort());
224+
}
225+
redirectUriBuilder.replaceQuery(requestedRedirectUri.getQuery()); // retain additional params (if any)
226+
redirectUriBuilder.fragment(null);
227+
return redirectUriBuilder.build().toUriString();
167228
}
168229
}
230+
169231
throw new RedirectMismatchException("Invalid redirect: " + requestedRedirect
170232
+ " does not match one of the registered values: " + redirectUris.toString());
171233
}

spring-security-oauth2/src/test/java/org/springframework/security/oauth2/provider/code/SubdomainRedirectResolverTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class SubdomainRedirectResolverTests
2323

2424

2525
@Test
26-
public void testRedirectWatchdox() throws Exception
26+
public void testRedirectMatch() throws Exception
2727
{
2828
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://watchdox.com"));
2929
client.setRegisteredRedirectUri(redirectUris);
@@ -32,9 +32,9 @@ public void testRedirectWatchdox() throws Exception
3232
}
3333

3434
@Test(expected=RedirectMismatchException.class)
35-
public void testRedirectBadWatchdox() throws Exception
35+
public void testRedirectNoMatch() throws Exception
3636
{
37-
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http//watchdox.com"));
37+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://watchdox.com"));
3838
client.setRegisteredRedirectUri(redirectUris);
3939
String requestedRedirect = "http://anywhere.google.com";
4040
assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));

spring-security-oauth2/src/test/java/org/springframework/security/oauth2/provider/endpoint/DefaultRedirectResolverTests.java

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
/*
2-
* Copyright 2006-2011 the original author or authors.
3-
*
4-
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5-
* the License. You may obtain a copy of the License at
6-
*
7-
* http://www.apache.org/licenses/LICENSE-2.0
8-
*
9-
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10-
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11-
* specific language governing permissions and limitations under the License.
2+
* Copyright 2002-2019 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+
* http://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.
1215
*/
1316
package org.springframework.security.oauth2.provider.endpoint;
1417

@@ -180,4 +183,107 @@ public void testRedirectMatchPortsFalse() throws Exception {
180183
String requestedRedirect = "http://anywhere.com:91";
181184
assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
182185
}
186+
187+
// gh-1566
188+
@Test(expected = RedirectMismatchException.class)
189+
public void testRedirectRegisteredUserInfoNotMatching() throws Exception {
190+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://[email protected]"));
191+
client.setRegisteredRedirectUri(redirectUris);
192+
resolver.resolveRedirect("http://[email protected]", client);
193+
}
194+
195+
// gh-1566
196+
@Test(expected = RedirectMismatchException.class)
197+
public void testRedirectRegisteredNoUserInfoNotMatching() throws Exception {
198+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://[email protected]"));
199+
client.setRegisteredRedirectUri(redirectUris);
200+
resolver.resolveRedirect("http://anywhere.com", client);
201+
}
202+
203+
// gh-1566
204+
@Test()
205+
public void testRedirectRegisteredUserInfoMatching() throws Exception {
206+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://[email protected]"));
207+
client.setRegisteredRedirectUri(redirectUris);
208+
String requestedRedirect = "http://[email protected]";
209+
assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
210+
}
211+
212+
// gh-1566
213+
@Test()
214+
public void testRedirectRegisteredFragmentIgnoredAndStripped() throws Exception {
215+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://[email protected]/foo/bar#baz"));
216+
client.setRegisteredRedirectUri(redirectUris);
217+
String requestedRedirect = "http://[email protected]/foo/bar";
218+
assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect + "#bar", client));
219+
}
220+
221+
// gh-1566
222+
@Test()
223+
public void testRedirectRegisteredQueryParamsMatching() throws Exception {
224+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1&p2=v2"));
225+
client.setRegisteredRedirectUri(redirectUris);
226+
String requestedRedirect = "http://anywhere.com/?p1=v1&p2=v2";
227+
assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
228+
}
229+
230+
// gh-1566
231+
@Test()
232+
public void testRedirectRegisteredQueryParamsMatchingIgnoringAdditionalParams() throws Exception {
233+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1&p2=v2"));
234+
client.setRegisteredRedirectUri(redirectUris);
235+
String requestedRedirect = "http://anywhere.com/?p1=v1&p2=v2&p3=v3";
236+
assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
237+
}
238+
239+
// gh-1566
240+
@Test()
241+
public void testRedirectRegisteredQueryParamsMatchingDifferentOrder() throws Exception {
242+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1&p2=v2"));
243+
client.setRegisteredRedirectUri(redirectUris);
244+
String requestedRedirect = "http://anywhere.com/?p2=v2&p1=v1";
245+
assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
246+
}
247+
248+
// gh-1566
249+
@Test(expected = RedirectMismatchException.class)
250+
public void testRedirectRegisteredQueryParamsWithDifferentValues() throws Exception {
251+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1&p2=v2"));
252+
client.setRegisteredRedirectUri(redirectUris);
253+
resolver.resolveRedirect("http://anywhere.com/?p1=v1&p2=v3", client);
254+
}
255+
256+
// gh-1566
257+
@Test(expected = RedirectMismatchException.class)
258+
public void testRedirectRegisteredQueryParamsNotMatching() throws Exception {
259+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1"));
260+
client.setRegisteredRedirectUri(redirectUris);
261+
resolver.resolveRedirect("http://anywhere.com/?p2=v2", client);
262+
}
263+
264+
// gh-1566
265+
@Test(expected = RedirectMismatchException.class)
266+
public void testRedirectRegisteredQueryParamsPartiallyMatching() throws Exception {
267+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1&p2=v2"));
268+
client.setRegisteredRedirectUri(redirectUris);
269+
resolver.resolveRedirect("http://anywhere.com/?p2=v2&p3=v3", client);
270+
}
271+
272+
// gh-1566
273+
@Test
274+
public void testRedirectRegisteredQueryParamsMatchingWithMultipleValuesInRegistered() throws Exception {
275+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v11&p1=v12"));
276+
client.setRegisteredRedirectUri(redirectUris);
277+
String requestedRedirect = "http://anywhere.com/?p1=v11&p1=v12";
278+
assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
279+
}
280+
281+
// gh-1566
282+
@Test
283+
public void testRedirectRegisteredQueryParamsMatchingWithParamWithNoValue() throws Exception {
284+
Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1&p2=v2"));
285+
client.setRegisteredRedirectUri(redirectUris);
286+
String requestedRedirect = "http://anywhere.com/?p1&p2=v2";
287+
assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
288+
}
183289
}

tests/annotation/jpa/src/test/java/demo/AuthorizationCodeProviderCookieTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public class AuthorizationCodeProviderCookieTests extends AbstractEmptyAuthoriza
3131
@Test
3232
@OAuth2ContextConfiguration(resource = MyClientWithRegisteredRedirect.class, initialize = false)
3333
public void testPostToProtectedResource() throws Exception {
34-
approveAccessTokenGrant("http://anywhere", true);
34+
approveAccessTokenGrant("http://anywhere?key=value", true);
3535
assertNotNull(context.getAccessToken());
3636
LinkedMultiValueMap<String, String> form = new LinkedMultiValueMap<>();
3737
form.set("foo", "bar");

tests/annotation/vanilla/src/test/java/demo/AuthorizationCodeProviderCookieTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public class AuthorizationCodeProviderCookieTests extends AbstractEmptyAuthoriza
3131
@Test
3232
@OAuth2ContextConfiguration(resource = MyClientWithRegisteredRedirect.class, initialize = false)
3333
public void testPostToProtectedResource() throws Exception {
34-
approveAccessTokenGrant("http://anywhere", true);
34+
approveAccessTokenGrant("http://anywhere?key=value", true);
3535
assertNotNull(context.getAccessToken());
3636
LinkedMultiValueMap<String, String> form = new LinkedMultiValueMap<>();
3737
form.set("foo", "bar");

0 commit comments

Comments
 (0)