Skip to content

Commit ebe9ed0

Browse files
committed
Correct query loss when using parse_qsl to dict
1 parent 4c7b3be commit ebe9ed0

File tree

5 files changed

+104
-49
lines changed

5 files changed

+104
-49
lines changed

oauth2client/_helpers.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,54 @@ def string_to_scopes(scopes):
179179
return scopes
180180

181181

182+
def parse_unique_urlencoded(content):
183+
"""Parses unique key-value parameters from urlencoded content.
184+
185+
Args:
186+
content: string, URL-encoded key-value pairs.
187+
188+
Returns:
189+
dict, The key-value pairs from ``content``.
190+
191+
Raises:
192+
ValueError: if one of the keys is repeated.
193+
"""
194+
urlencoded_params = urllib.parse.parse_qs(content)
195+
params = {}
196+
for key, value in six.iteritems(urlencoded_params):
197+
if len(value) != 1:
198+
msg = ('URL-encoded content contains a repeated value:'
199+
'%s -> %s' % (key, ', '.join(value)))
200+
raise ValueError(msg)
201+
params[key] = value[0]
202+
return params
203+
204+
205+
def update_query_params(uri, params):
206+
"""Updates a URI with new query parameters.
207+
208+
If a given key from ``params`` is repeated in the ``uri``, then
209+
the URI will be considered invalid and an error will occur.
210+
211+
If the URI is valid, then each value from ``params`` will
212+
replace the corresponding value in the query parameters (if
213+
it exists).
214+
215+
Args:
216+
uri: string, A valid URI, with potential existing query parameters.
217+
params: dict, A dictionary of query parameters.
218+
219+
Returns:
220+
The same URI but with the new query parameters added.
221+
"""
222+
parts = urllib.parse.urlparse(uri)
223+
query_params = parse_unique_urlencoded(parts.query)
224+
query_params.update(params)
225+
new_query = urllib.parse.urlencode(query_params)
226+
new_parts = parts._replace(query=new_query)
227+
return urllib.parse.urlunparse(new_parts)
228+
229+
182230
def _add_query_parameter(url, name, value):
183231
"""Adds a query parameter to a url.
184232
@@ -195,11 +243,7 @@ def _add_query_parameter(url, name, value):
195243
if value is None:
196244
return url
197245
else:
198-
parsed = list(urllib.parse.urlparse(url))
199-
query = dict(urllib.parse.parse_qsl(parsed[4]))
200-
query[name] = value
201-
parsed[4] = urllib.parse.urlencode(query)
202-
return urllib.parse.urlunparse(parsed)
246+
return update_query_params(url, {name: value})
203247

204248

205249
def validate_file(filename):

oauth2client/client.py

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -438,23 +438,6 @@ def delete(self):
438438
self.release_lock()
439439

440440

441-
def _update_query_params(uri, params):
442-
"""Updates a URI with new query parameters.
443-
444-
Args:
445-
uri: string, A valid URI, with potential existing query parameters.
446-
params: dict, A dictionary of query parameters.
447-
448-
Returns:
449-
The same URI but with the new query parameters added.
450-
"""
451-
parts = urllib.parse.urlparse(uri)
452-
query_params = dict(urllib.parse.parse_qsl(parts.query))
453-
query_params.update(params)
454-
new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
455-
return urllib.parse.urlunparse(new_parts)
456-
457-
458441
class OAuth2Credentials(Credentials):
459442
"""Credentials object for OAuth 2.0.
460443
@@ -850,7 +833,8 @@ def _do_revoke(self, http, token):
850833
"""
851834
logger.info('Revoking token')
852835
query_params = {'token': token}
853-
token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
836+
token_revoke_uri = _helpers.update_query_params(
837+
self.revoke_uri, query_params)
854838
resp, content = transport.request(http, token_revoke_uri)
855839
if resp.status == http_client.OK:
856840
self.invalid = True
@@ -889,8 +873,8 @@ def _do_retrieve_scopes(self, http, token):
889873
"""
890874
logger.info('Refreshing scopes')
891875
query_params = {'access_token': token, 'fields': 'scope'}
892-
token_info_uri = _update_query_params(self.token_info_uri,
893-
query_params)
876+
token_info_uri = _helpers.update_query_params(
877+
self.token_info_uri, query_params)
894878
resp, content = transport.request(http, token_info_uri)
895879
content = _helpers._from_bytes(content)
896880
if resp.status == http_client.OK:
@@ -1610,7 +1594,7 @@ def _parse_exchange_token_response(content):
16101594
except Exception:
16111595
# different JSON libs raise different exceptions,
16121596
# so we just do a catch-all here
1613-
resp = dict(urllib.parse.parse_qsl(content))
1597+
resp = _helpers.parse_unique_urlencoded(content)
16141598

16151599
# some providers respond with 'expires', others with 'expires_in'
16161600
if resp and 'expires' in resp:
@@ -1943,7 +1927,7 @@ def step1_get_authorize_url(self, redirect_uri=None, state=None):
19431927
query_params['code_challenge_method'] = 'S256'
19441928

19451929
query_params.update(self.params)
1946-
return _update_query_params(self.auth_uri, query_params)
1930+
return _helpers.update_query_params(self.auth_uri, query_params)
19471931

19481932
@_helpers.positional(1)
19491933
def step1_get_device_and_user_codes(self, http=None):

oauth2client/tools.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,16 @@ def do_GET(self):
122122
if an error occurred.
123123
"""
124124
self.send_response(http_client.OK)
125-
self.send_header("Content-type", "text/html")
125+
self.send_header('Content-type', 'text/html')
126126
self.end_headers()
127-
query = self.path.split('?', 1)[-1]
128-
query = dict(urllib.parse.parse_qsl(query))
127+
parts = urllib.parse.urlparse(self.path)
128+
query = _helpers.parse_unique_urlencoded(parts.query)
129129
self.server.query_params = query
130130
self.wfile.write(
131-
b"<html><head><title>Authentication Status</title></head>")
131+
b'<html><head><title>Authentication Status</title></head>')
132132
self.wfile.write(
133-
b"<body><p>The authentication flow has completed.</p>")
134-
self.wfile.write(b"</body></html>")
133+
b'<body><p>The authentication flow has completed.</p>')
134+
self.wfile.write(b'</body></html>')
135135

136136
def log_message(self, format, *args):
137137
"""Do not log messages to stdout while running as cmd. line program."""

tests/test__helpers.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import mock
2020

2121
from oauth2client import _helpers
22+
from tests import test_client
2223

2324

2425
class PositionalTests(unittest.TestCase):
@@ -242,3 +243,42 @@ def test_bad_input(self):
242243
bad_string = b'+'
243244
with self.assertRaises((TypeError, binascii.Error)):
244245
_helpers._urlsafe_b64decode(bad_string)
246+
247+
248+
class Test_update_query_params(unittest.TestCase):
249+
250+
def test_update_query_params_no_params(self):
251+
uri = 'http://www.google.com'
252+
updated = _helpers.update_query_params(uri, {'a': 'b'})
253+
self.assertEqual(updated, uri + '?a=b')
254+
255+
def test_update_query_params_existing_params(self):
256+
uri = 'http://www.google.com?x=y'
257+
updated = _helpers.update_query_params(uri, {'a': 'b', 'c': 'd&'})
258+
hardcoded_update = uri + '&a=b&c=d%26'
259+
test_client.assertUrisEqual(self, updated, hardcoded_update)
260+
261+
def test_update_query_params_replace_param(self):
262+
base_uri = 'http://www.google.com'
263+
uri = base_uri + '?x=a'
264+
updated = _helpers.update_query_params(uri, {'x': 'b', 'y': 'c'})
265+
hardcoded_update = base_uri + '?x=b&y=c'
266+
test_client.assertUrisEqual(self, updated, hardcoded_update)
267+
268+
def test_update_query_params_repeated_params(self):
269+
uri = 'http://www.google.com?x=a&x=b'
270+
with self.assertRaises(ValueError):
271+
_helpers.update_query_params(uri, {'a': 'c'})
272+
273+
274+
class Test_parse_unique_urlencoded(unittest.TestCase):
275+
276+
def test_without_repeats(self):
277+
content = 'a=b&c=d'
278+
result = _helpers.parse_unique_urlencoded(content)
279+
self.assertEqual(result, {'a': 'b', 'c': 'd'})
280+
281+
def test_with_repeats(self):
282+
content = 'a=b&a=d'
283+
with self.assertRaises(ValueError):
284+
_helpers.parse_unique_urlencoded(content)

tests/test_client.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,7 +1364,7 @@ def _do_retrieve_scopes_test_helper(self, response, content,
13641364
self.assertEqual(credentials.scopes, set())
13651365
self.assertEqual(exc_manager.exception.args, (error_msg,))
13661366

1367-
token_uri = client._update_query_params(
1367+
token_uri = _helpers.update_query_params(
13681368
oauth2client.GOOGLE_TOKEN_INFO_URI,
13691369
{'fields': 'scope', 'access_token': token})
13701370

@@ -1558,19 +1558,6 @@ def test_sign_blob_abstract(self):
15581558
credentials.sign_blob(b'blob')
15591559

15601560

1561-
class UpdateQueryParamsTest(unittest.TestCase):
1562-
def test_update_query_params_no_params(self):
1563-
uri = 'http://www.google.com'
1564-
updated = client._update_query_params(uri, {'a': 'b'})
1565-
self.assertEqual(updated, uri + '?a=b')
1566-
1567-
def test_update_query_params_existing_params(self):
1568-
uri = 'http://www.google.com?x=y'
1569-
updated = client._update_query_params(uri, {'a': 'b', 'c': 'd&'})
1570-
hardcoded_update = uri + '&a=b&c=d%26'
1571-
assertUrisEqual(self, updated, hardcoded_update)
1572-
1573-
15741561
class ExtractIdTokenTest(unittest.TestCase):
15751562
"""Tests client._extract_id_token()."""
15761563

@@ -1670,7 +1657,7 @@ def test_step1_get_authorize_url_redirect_override(self, logger):
16701657
'access_type': 'offline',
16711658
'response_type': 'code',
16721659
}
1673-
expected = client._update_query_params(flow.auth_uri, query_params)
1660+
expected = _helpers.update_query_params(flow.auth_uri, query_params)
16741661
assertUrisEqual(self, expected, result)
16751662
# Check stubs.
16761663
self.assertEqual(logger.warning.call_count, 1)
@@ -1735,7 +1722,7 @@ def test_step1_get_authorize_url_without_login_hint(self):
17351722
'access_type': 'offline',
17361723
'response_type': 'code',
17371724
}
1738-
expected = client._update_query_params(flow.auth_uri, query_params)
1725+
expected = _helpers.update_query_params(flow.auth_uri, query_params)
17391726
assertUrisEqual(self, expected, result)
17401727

17411728
def test_step1_get_device_and_user_codes_wo_device_uri(self):

0 commit comments

Comments
 (0)