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
|
'echo_request_headers_as_json' means return the request headers in
|
||||||
the response body
|
the response body
|
||||||
'echo_request_body' means return the request body 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):
|
def __init__(self, iterable):
|
||||||
@@ -240,6 +241,8 @@ class HttpMockSequence(object):
|
|||||||
content = simplejson.dumps(headers)
|
content = simplejson.dumps(headers)
|
||||||
elif content == 'echo_request_body':
|
elif content == 'echo_request_body':
|
||||||
content = body
|
content = body
|
||||||
|
elif content == 'echo_request_uri':
|
||||||
|
content = uri
|
||||||
return httplib2.Response(resp), content
|
return httplib2.Response(resp), content
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,8 @@ class OAuthCredentials(Credentials):
|
|||||||
self.store = None
|
self.store = None
|
||||||
|
|
||||||
def authorize(self, http):
|
def authorize(self, http):
|
||||||
"""
|
"""Authorize an httplib2.Http instance with these Credentials
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
http - An instance of httplib2.Http
|
http - An instance of httplib2.Http
|
||||||
or something that acts like it.
|
or something that acts like it.
|
||||||
@@ -213,6 +214,7 @@ class OAuthCredentials(Credentials):
|
|||||||
headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
|
headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
|
||||||
else:
|
else:
|
||||||
headers['user-agent'] = self.user_agent
|
headers['user-agent'] = self.user_agent
|
||||||
|
|
||||||
resp, content = request_orig(uri, method, body, headers,
|
resp, content = request_orig(uri, method, body, headers,
|
||||||
redirections, connection_type)
|
redirections, connection_type)
|
||||||
response_code = resp.status
|
response_code = resp.status
|
||||||
@@ -233,6 +235,149 @@ class OAuthCredentials(Credentials):
|
|||||||
return http
|
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):
|
class FlowThreeLegged(Flow):
|
||||||
"""Does the Three Legged Dance for OAuth 1.0a.
|
"""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.
|
# 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)'
|
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||||
|
|||||||
Reference in New Issue
Block a user