From 289ac496e00c46aa125096d7d0342d77d93cb0f6 Mon Sep 17 00:00:00 2001 From: Ricky Rosario Date: Sat, 13 Mar 2010 18:29:36 -0500 Subject: [PATCH 1/8] dont assume urlparse.parse_qs, we might be using cgi.parse_qs --- oauth2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2/__init__.py b/oauth2/__init__.py index b1ae2b97..153e82b8 100644 --- a/oauth2/__init__.py +++ b/oauth2/__init__.py @@ -316,7 +316,7 @@ def to_postdata(self): def to_url(self): """Serialize as a URL for a GET request.""" base_url = urlparse.urlparse(self.url) - query = urlparse.parse_qs(base_url.query) + query = parse_qs(base_url.query) for k, v in self.items(): query.setdefault(k, []).append(v) url = (base_url.scheme, base_url.netloc, base_url.path, base_url.params, From d86cdc7105514b26d499e2d0c46a683c420e890a Mon Sep 17 00:00:00 2001 From: Joe Stump Date: Fri, 19 Mar 2010 06:44:19 +0800 Subject: [PATCH 2/8] Fixed a deprecation warning pointed out by admp on GitHub. --- oauth2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2/__init__.py b/oauth2/__init__.py index 153e82b8..62841150 100644 --- a/oauth2/__init__.py +++ b/oauth2/__init__.py @@ -692,7 +692,7 @@ def sign(self, request, consumer, token): # HMAC object. try: - import hashlib.sha1 as sha # 2.5 + from hashlib import sha1 as sha except ImportError: import sha # Deprecated From a60a203b2c9becec1c0bc9cb03aa14b613cd18fc Mon Sep 17 00:00:00 2001 From: Joe Stump Date: Fri, 19 Mar 2010 06:47:31 +0800 Subject: [PATCH 3/8] Upped version for new push. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bbdb04c8..48b1b780 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup(name="oauth2", - version="1.1.1", + version="1.1.2", description="Library for OAuth version 1.0a.", author="Joe Stump", author_email="joe@simplegeo.com", From 73d02e526f594b59a910dbd9071b42e01c936c28 Mon Sep 17 00:00:00 2001 From: Justin Plock Date: Mon, 22 Mar 2010 23:44:47 +0800 Subject: [PATCH 4/8] Added an example for using two-legged OAuth on Google's AppEngine --- example/appengine_oauth.py | 108 +++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 example/appengine_oauth.py diff --git a/example/appengine_oauth.py b/example/appengine_oauth.py new file mode 100644 index 00000000..12aed520 --- /dev/null +++ b/example/appengine_oauth.py @@ -0,0 +1,108 @@ +""" +The MIT License + +Copyright (c) 2010 Justin Plock + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import os + +from google.appengine.ext import webapp +from google.appengine.ext import db +from google.appengine.ext.webapp import util +import oauth2 as oauth + +class Client(db.Model): + # oauth_key is the Model's key_name field + oauth_secret = db.StringProperty() # str(uuid.uuid4()) works well for this + first_name = db.StringProperty() + last_name = db.StringProperty() + email_address = db.EmailProperty(required=True) + password = db.StringProperty(required=True) + + @property + def secret(self): + return self.oauth_secret + +class OAuthHandler(webapp.RequestHandler): + + def __init__(self): + self._server = oauth.Server() + self._server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) + self._server.add_signature_method(oauth.SignatureMethod_PLAINTEXT()) + + def get_oauth_request(self): + """Return an OAuth Request object for the current request.""" + + try: + method = os.environ['REQUEST_METHOD'] + except: + method = 'GET' + + postdata = None + if method in ('POST', 'PUT'): + postdata = self.request.body + + return oauth.Request.from_request(method, self.request.uri, + headers=self.request.headers, query_string=postdata) + + def get_client(self, request=None): + """Return the client from the OAuth parameters.""" + + if not isinstance(request, oauth.Request): + request = self.get_oauth_request() + client_key = request.get_parameter('oauth_consumer_key') + if not client_key: + raise Exception('Missing "oauth_consumer_key" parameter in ' \ + 'OAuth "Authorization" header') + + client = models.Client.get_by_key_name(client_key) + if not client: + raise Exception('Client "%s" not found.' % client_key) + + return client + + def is_valid(self): + """Returns a Client object if this is a valid OAuth request.""" + + try: + request = self.get_oauth_request() + client = self.get_client(request) + params = self._server.verify_request(request, client, None) + except Exception, e: + raise e + + return client + +class SampleHandler(OAuthHandler): + def get(self): + try: + client = self.is_valid() + except Exception, e: + self.error(500) + self.response.out.write(e) + +def main(): + application = webapp.WSGIApplication([(r'/sample', SampleHandler)], + debug=False) + util.run_wsgi_app(application) + +if __name__ == '__main__': + main() From c37034c5d615c78119ccef4d62eb4bcd20cbe9d0 Mon Sep 17 00:00:00 2001 From: Justin Plock Date: Mon, 22 Mar 2010 23:49:01 +0800 Subject: [PATCH 5/8] Added a note about requiring httplib2 --- example/appengine_oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/appengine_oauth.py b/example/appengine_oauth.py index 12aed520..814f9b6f 100644 --- a/example/appengine_oauth.py +++ b/example/appengine_oauth.py @@ -27,7 +27,7 @@ from google.appengine.ext import webapp from google.appengine.ext import db from google.appengine.ext.webapp import util -import oauth2 as oauth +import oauth2 as oauth # httplib2 is required for this to work on AppEngine class Client(db.Model): # oauth_key is the Model's key_name field From f3781c7e449918f67ced86044973e7bd1515443e Mon Sep 17 00:00:00 2001 From: mmalone Date: Sun, 28 Mar 2010 02:12:10 +0800 Subject: [PATCH 6/8] Properly sign query string parameters included in request URI. --- oauth2/__init__.py | 55 ++++++++++++++++------------------ tests/test_oauth.py | 72 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 40 deletions(-) diff --git a/oauth2/__init__.py b/oauth2/__init__.py index 62841150..43c09c23 100644 --- a/oauth2/__init__.py +++ b/oauth2/__init__.py @@ -250,35 +250,33 @@ class Request(dict): """ - http_method = HTTP_METHOD - http_url = None version = VERSION def __init__(self, method=HTTP_METHOD, url=None, parameters=None): - if method is not None: - self.method = method - - if url is not None: - self.url = url - + self.method = method + self.url = url if parameters is not None: self.update(parameters) @setter def url(self, value): - scheme, netloc, path, params, query, fragment = urlparse.urlparse(value) - - # Exclude default port numbers. - if scheme == 'http' and netloc[-3:] == ':80': - netloc = netloc[:-3] - elif scheme == 'https' and netloc[-4:] == ':443': - netloc = netloc[:-4] - - if scheme != 'http' and scheme != 'https': - raise ValueError("Unsupported URL %s (%s)." % (value, scheme)) - - value = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) self.__dict__['url'] = value + if value is not None: + scheme, netloc, path, params, query, fragment = urlparse.urlparse(value) + + # Exclude default port numbers. + if scheme == 'http' and netloc[-3:] == ':80': + netloc = netloc[:-3] + elif scheme == 'https' and netloc[-4:] == ':443': + netloc = netloc[:-4] + if scheme not in ('http', 'https'): + raise ValueError("Unsupported URL %s (%s)." % (value, scheme)) + + # Normalized URL excludes params, query, and fragment. + self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None)) + else: + self.normalized_url = None + self.__dict__['url'] = None @setter def method(self, value): @@ -342,6 +340,11 @@ def get_normalized_parameters(self): items.extend((key, item) for item in value) else: items.append((key, value)) + + # Include any query string parameters from the provided URL + query = urlparse.urlparse(self.url)[4] + items.extend(self._split_url_string(query).items()) + encoded_str = urllib.urlencode(sorted(items)) # Encode signature parameters per Oauth Core 1.0 protocol # spec draft 7, section 3.6 @@ -600,15 +603,6 @@ def request(self, uri, method="GET", body=None, headers=None, if body and method == "POST" and not is_multipart: parameters = dict(parse_qsl(body)) - elif method == "GET": - parsed = urlparse.urlparse(uri) - - try: - query = parsed.query - except AttributeError: - query = parsed[4] - - parameters = parse_qsl(query) else: parameters = None @@ -676,7 +670,7 @@ class SignatureMethod_HMAC_SHA1(SignatureMethod): def signing_base(self, request, consumer, token): sig = ( escape(request.method), - escape(request.url), + escape(request.normalized_url), escape(request.get_normalized_parameters()), ) @@ -684,6 +678,7 @@ def signing_base(self, request, consumer, token): if token: key += escape(token.secret) raw = '&'.join(sig) + print key, raw return key, raw def sign(self, request, consumer, token): diff --git a/tests/test_oauth.py b/tests/test_oauth.py index efcf96ab..100f8038 100644 --- a/tests/test_oauth.py +++ b/tests/test_oauth.py @@ -222,14 +222,8 @@ def test_setter(self): url = "http://example.com" method = "GET" req = oauth.Request(method) - - try: - url = req.url - self.fail("AttributeError should have been raised on empty url.") - except AttributeError: - pass - except Exception, e: - self.fail(str(e)) + self.assertTrue(req.url is None) + self.assertTrue(req.normalized_url is None) def test_deleter(self): url = "http://example.com" @@ -253,17 +247,21 @@ def test_url(self): method = "GET" req = oauth.Request(method, url1) - self.assertEquals(req.url, exp1) + self.assertEquals(req.normalized_url, exp1) + self.assertEquals(req.url, url1) req = oauth.Request(method, url2) - self.assertEquals(req.url, exp2) + self.assertEquals(req.normalized_url, exp2) + self.assertEquals(req.url, url2) def test_url_query(self): url = "https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10" + normalized_url = urlparse.urlunparse(urlparse.urlparse(url)[:3] + (None, None, None)) method = "GET" req = oauth.Request(method, url) self.assertEquals(req.url, url) + self.assertEquals(req.normalized_url, normalized_url) def test_get_parameter(self): url = "http://example.com" @@ -400,6 +398,30 @@ def test_to_url_with_query(self): self.assertEquals(b['max-contacts'], ['10']) self.assertEquals(a, b) + def test_signature_base_string_with_query(self): + url = "https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10" + params = { + 'oauth_version': "1.0", + 'oauth_nonce': "4572616e48616d6d65724c61686176", + 'oauth_timestamp': "137131200", + 'oauth_consumer_key': "0685bd9184jfhq22", + 'oauth_signature_method': "HMAC-SHA1", + 'oauth_token': "ad180jjd733klru7", + 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", + } + req = oauth.Request("GET", url, params) + self.assertEquals(req.normalized_url, 'https://www.google.com/m8/feeds/contacts/default/full/') + self.assertEquals(req.url, 'https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10') + normalized_params = parse_qsl(req.get_normalized_parameters()) + self.assertTrue(len(normalized_params), len(params) + 2) + normalized_params = dict(normalized_params) + for key, value in params.iteritems(): + if key == 'oauth_signature': + continue + self.assertEquals(value, normalized_params[key]) + self.assertEquals(normalized_params['alt'], 'json') + self.assertEquals(normalized_params['max-contacts'], '10') + def test_get_normalized_parameters(self): url = "http://sp.example.com/" @@ -871,6 +893,36 @@ def test_multipart_post_does_not_alter_body(self): self.assertEqual(result, random_result) self.mox.VerifyAll() + def test_url_with_query_string(self): + self.mox.StubOutWithMock(httplib2.Http, 'request') + uri = 'http://example.com/foo/bar/?show=thundercats&character=snarf' + client = oauth.Client(self.consumer, None) + expected_kwargs = { + 'method': 'GET', + 'body': None, + 'redirections': httplib2.DEFAULT_MAX_REDIRECTS, + 'connection_type': None, + 'headers': mox.IsA(dict), + } + def oauth_verifier(url): + req = oauth.Request.from_consumer_and_token(self.consumer, None, + http_method='GET', http_url=uri, parameters={}) + req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), self.consumer, None) + expected = parse_qsl(urlparse.urlparse(req.to_url()).query) + actual = parse_qsl(urlparse.urlparse(url).query) + if len(expected) != len(actual): + return False + actual = dict(actual) + for key, value in expected: + if key not in ('oauth_signature', 'oauth_nonce', 'oauth_timestamp'): + if actual[key] != value: + return False + return True + httplib2.Http.request(client, mox.Func(oauth_verifier), **expected_kwargs) + self.mox.ReplayAll() + client.request(uri, 'GET') + self.mox.VerifyAll() + if __name__ == "__main__": unittest.main() From 5d8890c70b512f4fa63a5d7c8da7dae9e9abecb1 Mon Sep 17 00:00:00 2001 From: mmalone Date: Sun, 28 Mar 2010 02:13:33 +0800 Subject: [PATCH 7/8] Stray print. --- oauth2/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/oauth2/__init__.py b/oauth2/__init__.py index 43c09c23..5f60b6e5 100644 --- a/oauth2/__init__.py +++ b/oauth2/__init__.py @@ -678,7 +678,6 @@ def signing_base(self, request, consumer, token): if token: key += escape(token.secret) raw = '&'.join(sig) - print key, raw return key, raw def sign(self, request, consumer, token): From 5f7548d14e0409c27ce6f87b0c51296af5e8c4d8 Mon Sep 17 00:00:00 2001 From: Joe Stump Date: Sun, 28 Mar 2010 02:18:08 +0800 Subject: [PATCH 8/8] Upped version for release. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 48b1b780..cb73460c 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup(name="oauth2", - version="1.1.2", + version="1.1.3", description="Library for OAuth version 1.0a.", author="Joe Stump", author_email="joe@simplegeo.com",