Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed space encoding in base string URI used in the signature base st… #651

Merged
merged 5 commits into from Feb 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Expand Up @@ -3,6 +3,7 @@ Changelog

TBD
------------------
* #650 Fixed space encoding in base string URI used in the signature base string.
* #652: Fixed OIDC /token response which wrongly returned "&state=None"

3.0.1 (2019-01-24)
Expand Down
5 changes: 2 additions & 3 deletions oauthlib/oauth1/rfc5849/__init__.py
Expand Up @@ -133,12 +133,11 @@ def get_oauth_signature(self, request):
log.debug("Collected params: {0}".format(collected_params))

normalized_params = signature.normalize_parameters(collected_params)
normalized_uri = signature.normalize_base_string_uri(uri,
headers.get('Host', None))
normalized_uri = signature.base_string_uri(uri, headers.get('Host', None))
log.debug("Normalized params: {0}".format(normalized_params))
log.debug("Normalized URI: {0}".format(normalized_uri))

base_string = signature.construct_base_string(request.http_method,
base_string = signature.signature_base_string(request.http_method,
normalized_uri, normalized_params)

log.debug("Signing: signature base string: {0}".format(base_string))
Expand Down
57 changes: 43 additions & 14 deletions oauthlib/oauth1/rfc5849/signature.py
Expand Up @@ -40,9 +40,10 @@

log = logging.getLogger(__name__)

def construct_base_string(http_method, base_string_uri,

def signature_base_string(http_method, base_str_uri,
normalized_encoded_request_parameters):
"""**String Construction**
"""**Construct the signature base string.**
Per `section 3.4.1.1`_ of the spec.

For example, the HTTP request::
Expand Down Expand Up @@ -90,7 +91,7 @@ def construct_base_string(http_method, base_string_uri,
#
# .. _`Section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
# .. _`Section 3.4.6`: https://tools.ietf.org/html/rfc5849#section-3.4.6
base_string += utils.escape(base_string_uri)
base_string += utils.escape(base_str_uri)

# 4. An "&" character (ASCII code 38).
base_string += '&'
Expand All @@ -105,9 +106,9 @@ def construct_base_string(http_method, base_string_uri,
return base_string


def normalize_base_string_uri(uri, host=None):
def base_string_uri(uri, host=None):
"""**Base String URI**
Per `section 3.4.1.2`_ of the spec.
Per `section 3.4.1.2`_ of RFC 5849.

For example, the HTTP request::

Expand Down Expand Up @@ -177,7 +178,31 @@ def normalize_base_string_uri(uri, host=None):
if (scheme, port) in default_ports:
netloc = host

return urlparse.urlunparse((scheme, netloc, path, params, '', ''))
v = urlparse.urlunparse((scheme, netloc, path, params, '', ''))

# RFC 5849 does not specify which characters are encoded in the
# "base string URI", nor how they are encoded - which is very bad, since
# the signatures won't match if there are any differences. Fortunately,
# most URIs only use characters that are clearly not encoded (e.g. digits
# and A-Z, a-z), so have avoided any differences between implementations.
#
# The example from its section 3.4.1.2 illustrates that spaces in
# the path are percent encoded. But it provides no guidance as to what other
# characters (if any) must be encoded (nor how); nor if characters in the
# other components are to be encoded or not.
#
# This implementation **assumes** that **only** the space is percent-encoded
# and it is done to the entire value (not just to spaces in the path).
#
# This code may need to be changed if it is discovered that other characters
# are expected to be encoded.
#
# Note: the "base string URI" returned by this function will be encoded
# again before being concatenated into the "signature base string". So any
# spaces in the URI will actually appear in the "signature base string"
# as "%2520" (the "%20" further encoded according to section 3.6).

return v.replace(' ', '%20')


# ** Request Parameters **
Expand Down Expand Up @@ -624,13 +649,15 @@ def verify_hmac_sha1(request, client_secret=None,

"""
norm_params = normalize_parameters(request.params)
uri = normalize_base_string_uri(request.uri)
base_string = construct_base_string(request.http_method, uri, norm_params)
signature = sign_hmac_sha1(base_string, client_secret,
bs_uri = base_string_uri(request.uri)
sig_base_str = signature_base_string(request.http_method, bs_uri,
norm_params)
signature = sign_hmac_sha1(sig_base_str, client_secret,
resource_owner_secret)
match = safe_string_equals(signature, request.signature)
if not match:
log.debug('Verify HMAC-SHA1 failed: sig base string: %s', base_string)
log.debug('Verify HMAC-SHA1 failed: signature base string: %s',
sig_base_str)
return match


Expand All @@ -657,16 +684,18 @@ def verify_rsa_sha1(request, rsa_public_key):
.. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
"""
norm_params = normalize_parameters(request.params)
uri = normalize_base_string_uri(request.uri)
message = construct_base_string(request.http_method, uri, norm_params).encode('utf-8')
bs_uri = base_string_uri(request.uri)
sig_base_str = signature_base_string(request.http_method, bs_uri,
norm_params).encode('utf-8')
sig = binascii.a2b_base64(request.signature.encode('utf-8'))

alg = _jwt_rs1_signing_algorithm()
key = _prepare_key_plus(alg, rsa_public_key)

verify_ok = alg.verify(message, key, sig)
verify_ok = alg.verify(sig_base_str, key, sig)
if not verify_ok:
log.debug('Verify RSA-SHA1 failed: sig base string: %s', message)
log.debug('Verify RSA-SHA1 failed: signature base string: %s',
sig_base_str)
return verify_ok


Expand Down
39 changes: 25 additions & 14 deletions tests/oauth1/rfc5849/test_signatures.py
Expand Up @@ -3,8 +3,8 @@

from oauthlib.common import unicode_type
from oauthlib.oauth1.rfc5849.signature import (collect_parameters,
construct_base_string,
normalize_base_string_uri,
signature_base_string,
base_string_uri,
normalize_parameters,
sign_hmac_sha1,
sign_hmac_sha1_with_client,
Expand Down Expand Up @@ -79,7 +79,7 @@ def setUp(self):
resource_owner_secret = self.resource_owner_secret
)

def test_construct_base_string(self):
def test_signature_base_string(self):
"""
Example text to be turned into a base string::

Expand All @@ -104,28 +104,28 @@ def test_construct_base_string(self):
D%2522137131201%2522%252Coauth_nonce%253D%25227d8f3e4a%2522%252Coau
th_signature%253D%2522bYT5CMsGcbgUdFHObYMEfcx6bsw%25253D%2522
"""
self.assertRaises(ValueError, construct_base_string,
self.assertRaises(ValueError, signature_base_string,
self.http_method,
self.base_string_url,
self.normalized_encoded_request_parameters)
self.assertRaises(ValueError, construct_base_string,
self.assertRaises(ValueError, signature_base_string,
self.http_method.decode('utf-8'),
self.base_string_url,
self.normalized_encoded_request_parameters)
self.assertRaises(ValueError, construct_base_string,
self.assertRaises(ValueError, signature_base_string,
self.http_method.decode('utf-8'),
self.base_string_url.decode('utf-8'),
self.normalized_encoded_request_parameters)

base_string = construct_base_string(
base_string = signature_base_string(
self.http_method.decode('utf-8'),
self.base_string_url.decode('utf-8'),
self.normalized_encoded_request_parameters.decode('utf-8')
)

self.assertEqual(self.control_base_string, base_string)

def test_normalize_base_string_uri(self):
def test_base_string_uri(self):
"""
Example text to be turned into a normalized base string uri::

Expand All @@ -137,33 +137,44 @@ def test_normalize_base_string_uri(self):
https://www.example.net:8080/
"""

# test first example from RFC 5849 section 3.4.1.2.
# Note: there is a space between "r" and "v"
uri = 'http://EXAMPLE.COM:80/r v/X?id=123'
self.assertEqual(base_string_uri(uri),
'http://example.com/r%20v/X')

# test second example from RFC 5849 section 3.4.1.2.
uri = 'https://www.example.net:8080/?q=1'
self.assertEqual(base_string_uri(uri),
'https://www.example.net:8080/')

# test for unicode failure
uri = b"www.example.com:8080"
self.assertRaises(ValueError, normalize_base_string_uri, uri)
self.assertRaises(ValueError, base_string_uri, uri)

# test for missing scheme
uri = "www.example.com:8080"
self.assertRaises(ValueError, normalize_base_string_uri, uri)
self.assertRaises(ValueError, base_string_uri, uri)

# test a URI with the default port
uri = "http://www.example.com:80/"
self.assertEqual(normalize_base_string_uri(uri),
self.assertEqual(base_string_uri(uri),
"http://www.example.com/")

# test a URI missing a path
uri = "http://www.example.com"
self.assertEqual(normalize_base_string_uri(uri),
self.assertEqual(base_string_uri(uri),
"http://www.example.com/")

# test a relative URI
uri = "/a-host-relative-uri"
host = "www.example.com"
self.assertRaises(ValueError, normalize_base_string_uri, (uri, host))
self.assertRaises(ValueError, base_string_uri, (uri, host))

# test overriding the URI's netloc with a host argument
uri = "http://www.example.com/a-path"
host = "alternatehost.example.com"
self.assertEqual(normalize_base_string_uri(uri, host),
self.assertEqual(base_string_uri(uri, host),
"http://alternatehost.example.com/a-path")

def test_collect_parameters(self):
Expand Down