Add 2LO support for OAuth 1.0.
Reviewed in http://codereview.appspot.com/4517087/
This commit is contained in:
@@ -218,6 +218,7 @@ class HttpMockSequence(object):
|
||||
'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 @@ class HttpMockSequence(object):
|
||||
content = simplejson.dumps(headers)
|
||||
elif content == 'echo_request_body':
|
||||
content = body
|
||||
elif content == 'echo_request_uri':
|
||||
content = uri
|
||||
return httplib2.Response(resp), content
|
||||
|
||||
|
||||
|
||||
@@ -170,7 +170,8 @@ class OAuthCredentials(Credentials):
|
||||
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 @@ class OAuthCredentials(Credentials):
|
||||
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 @@ class OAuthCredentials(Credentials):
|
||||
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.
|
||||
"""
|
||||
|
||||
76
tests/test_oauth.py
Normal file
76
tests/test_oauth.py
Normal file
@@ -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()
|
||||
@@ -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)'
|
||||
|
||||
Reference in New Issue
Block a user