Add 2LO support for OAuth 1.0.
Reviewed in http://codereview.appspot.com/4517087/
diff --git a/apiclient/http.py b/apiclient/http.py
index 2481c23..5f4be00 100644
--- a/apiclient/http.py
+++ b/apiclient/http.py
@@ -218,6 +218,7 @@
'echo_request_headers_as_json' means return the request headers in
the response body
'echo_request_body' means return the request body in the response body
+ 'echo_request_uri' means return the request uri in the response body
"""
def __init__(self, iterable):
@@ -240,6 +241,8 @@
content = simplejson.dumps(headers)
elif content == 'echo_request_body':
content = body
+ elif content == 'echo_request_uri':
+ content = uri
return httplib2.Response(resp), content
diff --git a/apiclient/oauth.py b/apiclient/oauth.py
index 430d068..18877b0 100644
--- a/apiclient/oauth.py
+++ b/apiclient/oauth.py
@@ -170,7 +170,8 @@
self.store = None
def authorize(self, http):
- """
+ """Authorize an httplib2.Http instance with these Credentials
+
Args:
http - An instance of httplib2.Http
or something that acts like it.
@@ -213,6 +214,7 @@
headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
else:
headers['user-agent'] = self.user_agent
+
resp, content = request_orig(uri, method, body, headers,
redirections, connection_type)
response_code = resp.status
@@ -233,6 +235,149 @@
return http
+class TwoLeggedOAuthCredentials(Credentials):
+ """Two Legged Credentials object for OAuth 1.0a.
+
+ The Two Legged object is created directly, not from a flow. Once you
+ authorize and httplib2.Http instance you can change the requestor and that
+ change will propogate to the authorized httplib2.Http instance. For example:
+
+ http = httplib2.Http()
+ http = credentials.authorize(http)
+
+ credentials.requestor = 'foo@example.info'
+ http.request(...)
+ credentials.requestor = 'bar@example.info'
+ http.request(...)
+ """
+
+ def __init__(self, consumer_key, consumer_secret, user_agent):
+ """
+ Args:
+ consumer_key: string, An OAuth 1.0 consumer key
+ consumer_secret: string, An OAuth 1.0 consumer secret
+ user_agent: string, The HTTP User-Agent to provide for this application.
+ """
+ self.consumer = oauth.Consumer(consumer_key, consumer_secret)
+ self.user_agent = user_agent
+ self.store = None
+
+ # email address of the user to act on the behalf of.
+ self._requestor = None
+
+ @property
+ def invalid(self):
+ """True if the credentials are invalid, such as being revoked.
+
+ Always returns False for Two Legged Credentials.
+ """
+ return False
+
+ def getrequestor(self):
+ return self._requestor
+
+ def setrequestor(self, email):
+ self._requestor = email
+
+ requestor = property(getrequestor, setrequestor, None,
+ 'The email address of the user to act on behalf of')
+
+ def set_store(self, store):
+ """Set the storage for the credential.
+
+ Args:
+ store: callable, a callable that when passed a Credential
+ will store the credential back to where it came from.
+ This is needed to store the latest access_token if it
+ has been revoked.
+ """
+ self.store = store
+
+ def __getstate__(self):
+ """Trim the state down to something that can be pickled."""
+ d = copy.copy(self.__dict__)
+ del d['store']
+ return d
+
+ def __setstate__(self, state):
+ """Reconstitute the state of the object from being pickled."""
+ self.__dict__.update(state)
+ self.store = None
+
+ def authorize(self, http):
+ """Authorize an httplib2.Http instance with these Credentials
+
+ Args:
+ http - An instance of httplib2.Http
+ or something that acts like it.
+
+ Returns:
+ A modified instance of http that was passed in.
+
+ Example:
+
+ h = httplib2.Http()
+ h = credentials.authorize(h)
+
+ You can't create a new OAuth
+ subclass of httplib2.Authenication because
+ it never gets passed the absolute URI, which is
+ needed for signing. So instead we have to overload
+ 'request' with a closure that adds in the
+ Authorization header and then calls the original version
+ of 'request()'.
+ """
+ request_orig = http.request
+ signer = oauth.SignatureMethod_HMAC_SHA1()
+
+ # The closure that will replace 'httplib2.Http.request'.
+ def new_request(uri, method='GET', body=None, headers=None,
+ redirections=httplib2.DEFAULT_MAX_REDIRECTS,
+ connection_type=None):
+ """Modify the request headers to add the appropriate
+ Authorization header."""
+ response_code = 302
+ http.follow_redirects = False
+ while response_code in [301, 302]:
+ # add in xoauth_requestor_id=self._requestor to the uri
+ if self._requestor is None:
+ raise MissingParameter(
+ 'Requestor must be set before using TwoLeggedOAuthCredentials')
+ parsed = list(urlparse.urlparse(uri))
+ q = parse_qsl(parsed[4])
+ q.append(('xoauth_requestor_id', self._requestor))
+ parsed[4] = urllib.urlencode(q)
+ uri = urlparse.urlunparse(parsed)
+
+ req = oauth.Request.from_consumer_and_token(
+ self.consumer, None, http_method=method, http_url=uri)
+ req.sign_request(signer, self.consumer, None)
+ if headers is None:
+ headers = {}
+ headers.update(req.to_header())
+ if 'user-agent' in headers:
+ headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
+ else:
+ headers['user-agent'] = self.user_agent
+ resp, content = request_orig(uri, method, body, headers,
+ redirections, connection_type)
+ response_code = resp.status
+ if response_code in [301, 302]:
+ uri = resp['location']
+
+ if response_code == 401:
+ logging.info('Access token no longer valid: %s' % content)
+ # Do not store the invalid state of the Credentials because
+ # being 2LO they could be reinstated in the future.
+ raise CredentialsInvalidError("Credentials are invalid.")
+
+ return resp, content
+
+ http.request = new_request
+ return http
+
+
+
class FlowThreeLegged(Flow):
"""Does the Three Legged Dance for OAuth 1.0a.
"""
diff --git a/tests/test_oauth.py b/tests/test_oauth.py
new file mode 100644
index 0000000..4d56c26
--- /dev/null
+++ b/tests/test_oauth.py
@@ -0,0 +1,76 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""Oauth tests
+
+Unit tests for apiclient.oauth.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import unittest
+
+from apiclient.http import HttpMockSequence
+from apiclient.oauth import CredentialsInvalidError
+from apiclient.oauth import MissingParameter
+from apiclient.oauth import TwoLeggedOAuthCredentials
+
+
+class TwoLeggedOAuthCredentialsTests(unittest.TestCase):
+
+ def setUp(self):
+ client_id = "some_client_id"
+ client_secret = "cOuDdkfjxxnv+"
+ user_agent = "sample/1.0"
+ self.credentials = TwoLeggedOAuthCredentials(client_id, client_secret,
+ user_agent)
+ self.credentials.requestor = 'test@example.org'
+
+ def test_invalid_token(self):
+ http = HttpMockSequence([
+ ({'status': '401'}, ''),
+ ])
+ http = self.credentials.authorize(http)
+ try:
+ resp, content = http.request("http://example.com")
+ self.fail('should raise CredentialsInvalidError')
+ except CredentialsInvalidError:
+ pass
+
+ def test_no_requestor(self):
+ self.credentials.requestor = None
+ http = HttpMockSequence([
+ ({'status': '401'}, ''),
+ ])
+ http = self.credentials.authorize(http)
+ try:
+ resp, content = http.request("http://example.com")
+ self.fail('should raise MissingParameter')
+ except MissingParameter:
+ pass
+
+ def test_add_requestor_to_uri(self):
+ http = HttpMockSequence([
+ ({'status': '200'}, 'echo_request_uri'),
+ ])
+ http = self.credentials.authorize(http)
+ resp, content = http.request("http://example.com")
+ self.assertEqual('http://example.com?xoauth_requestor_id=test%40example.org',
+ content)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py
index 6832739..3c6e0ba 100644
--- a/tests/test_oauth2client.py
+++ b/tests/test_oauth2client.py
@@ -15,9 +15,9 @@
# limitations under the License.
-"""Discovery document tests
+"""Oauth2client tests
-Unit tests for objects created from discovery documents.
+Unit tests for oauth2client.
"""
__author__ = 'jcgregorio@google.com (Joe Gregorio)'