Cleaned up oauth code based on feedback. Created FlowThreeLegged and OAuthCredentials classes.
diff --git a/apiclient/contrib/__init__.py b/apiclient/contrib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apiclient/contrib/__init__.py
diff --git a/apiclient/ext/__init__.py b/apiclient/ext/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apiclient/ext/__init__.py
diff --git a/apiclient/ext/appengine.py b/apiclient/ext/appengine.py
new file mode 100644
index 0000000..a780d0e
--- /dev/null
+++ b/apiclient/ext/appengine.py
@@ -0,0 +1,90 @@
+# Copyright (C) 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.
+
+"""Utilities for Google App Engine
+
+Utilities for making it easier to use the
+Google API Client for Python on Google App Engine.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import pickle
+
+from google.appengine.ext import db
+from apiclient.oauth import OAuthCredentials
+from apiclient.oauth import FlowThreeLegged
+
+
+class FlowThreeLeggedProperty(db.Property):
+ """Utility property that allows easy
+ storage and retreival of an
+ apiclient.oauth.FlowThreeLegged"""
+
+ # Tell what the user type is.
+ data_type = FlowThreeLegged
+
+ # For writing to datastore.
+ def get_value_for_datastore(self, model_instance):
+ flow = super(FlowThreeLeggedProperty,
+ self).get_value_for_datastore(model_instance)
+ return db.Blob(pickle.dumps(flow))
+
+ # For reading from datastore.
+ def make_value_from_datastore(self, value):
+ if value is None:
+ return None
+ return pickle.loads(value)
+
+ def validate(self, value):
+ if value is not None and not isinstance(value, FlowThreeLegged):
+ raise BadValueError('Property %s must be convertible '
+ 'to a FlowThreeLegged instance (%s)' %
+ (self.name, value))
+ return super(FlowThreeLeggedProperty, self).validate(value)
+
+ def empty(self, value):
+ return not value
+
+
+class OAuthCredentialsProperty(db.Property):
+ """Utility property that allows easy
+ storage and retrieval of
+ apiclient.oath.OAuthCredentials
+ """
+
+ # Tell what the user type is.
+ data_type = OAuthCredentials
+
+ # For writing to datastore.
+ def get_value_for_datastore(self, model_instance):
+ cred = super(OAuthCredentialsProperty,
+ self).get_value_for_datastore(model_instance)
+ return db.Blob(pickle.dumps(cred))
+
+ # For reading from datastore.
+ def make_value_from_datastore(self, value):
+ if value is None:
+ return None
+ return pickle.loads(value)
+
+ def validate(self, value):
+ if value is not None and not isinstance(value, OAuthCredentials):
+ raise BadValueError('Property %s must be convertible '
+ 'to an OAuthCredentials instance (%s)' %
+ (self.name, value))
+ return super(OAuthCredentialsProperty, self).validate(value)
+
+ def empty(self, value):
+ return not value
diff --git a/apiclient/oauth.py b/apiclient/oauth.py
index c1908d1..63f896b 100644
--- a/apiclient/oauth.py
+++ b/apiclient/oauth.py
@@ -10,30 +10,25 @@
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
import copy
-import urllib
+import httplib2
import oauth2 as oauth
+import urllib
+import logging
try:
from urlparse import parse_qs, parse_qsl
except ImportError:
from cgi import parse_qs, parse_qsl
+
+
class MissingParameter(Exception):
pass
-def abstract():
+
+def _abstract():
raise NotImplementedError("You need to override this function")
-class TokenStore(object):
- def get(user, service):
- """Returns an oauth.Token based on the (user, service) returning
- None if there is no Token for that (user, service).
- """
- abstract()
-
- def set(user, service, token):
- abstract()
-
buzz_discovery = {
'required': ['domain', 'scope'],
'request': {
@@ -50,7 +45,19 @@
},
}
+
def _oauth_uri(name, discovery, params):
+ """Look up the OAuth UR from the discovery
+ document and add query parameters based on
+ params.
+
+ name - The name of the OAuth URI to lookup, one
+ of 'request', 'access', or 'authorize'.
+ discovery - Portion of discovery document the describes
+ the OAuth endpoints.
+ params - Dictionary that is used to form the query parameters
+ for the specified URI.
+ """
if name not in ['request', 'access', 'authorize']:
raise KeyError(name)
keys = []
@@ -62,9 +69,100 @@
query[key] = params[key]
return discovery[name]['url'] + '?' + urllib.urlencode(query)
-class Flow3LO(object):
+
+class Credentials(object):
+ """Base class for all Credentials objects.
+
+ Subclasses must define an authorize() method
+ that applies the credentials to an HTTP transport.
+ """
+
+ def authorize(self, http):
+ """Take an httplib2.Http instance (or equivalent) and
+ authorizes it for the set of credentials, usually by
+ replacing http.request() with a method that adds in
+ the appropriate headers and then delegates to the original
+ Http.request() method.
+ """
+ _abstract()
+
+
+class OAuthCredentials(Credentials):
+ """Credentials object for OAuth 1.0a
+ """
+
+ def __init__(self, consumer, token, user_agent):
+ """
+ consumer - An instance of oauth.Consumer.
+ token - An instance of oauth.Token constructed with
+ the access token and secret.
+ user_agent - The HTTP User-Agent to provide for this application.
+ """
+ self.consumer = consumer
+ self.token = token
+ self.user_agent = user_agent
+
+ def authorize(self, http):
+ """
+ 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."""
+ req = oauth.Request.from_consumer_and_token(
+ self.consumer, self.token, http_method=method, http_url=uri)
+ req.sign_request(signer, self.consumer, self.token)
+ if headers == None:
+ headers = {}
+ headers.update(req.to_header())
+ if 'user-agent' not in headers:
+ headers['user-agent'] = self.user_agent
+ return request_orig(uri, method, body, headers,
+ redirections, connection_type)
+
+ http.request = new_request
+ return http
+
+
+class FlowThreeLegged(object):
+ """Does the Three Legged Dance for OAuth 1.0a.
+ """
+
def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
**kwargs):
+ """
+ discovery - Section of the API discovery document that describes
+ the OAuth endpoints.
+ consumer_key - OAuth consumer key
+ consumer_secret - OAuth consumer secret
+ user_agent - The HTTP User-Agent that identifies the application.
+ **kwargs - The keyword arguments are all optional and required
+ parameters for the OAuth calls.
+ """
self.discovery = discovery
self.consumer_key = consumer_key
self.consumer_secret = consumer_secret
@@ -75,14 +173,17 @@
if key not in self.params:
raise MissingParameter('Required parameter %s not supplied' % key)
- def step1(self, oauth_callback='oob'):
+ def step1_get_authorize_url(self, oauth_callback='oob'):
"""Returns a URI to redirect to the provider.
- If oauth_callback is 'oob' then the next call
- should be to step2_pin, otherwise oauth_callback
- is a URI and the next call should be to
- step2_callback() with the query parameters
- received at that callback.
+ oauth_callback - Either the string 'oob' for a non-web-based application,
+ or a URI that handles the callback from the authorization
+ server.
+
+ If oauth_callback is 'oob' then pass in the
+ generated verification code to step2_exchange,
+ otherwise pass in the query parameters received
+ at the callback uri to step2_exchange.
"""
consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
client = oauth.Client(consumer)
@@ -93,10 +194,11 @@
}
body = urllib.urlencode({'oauth_callback': oauth_callback})
uri = _oauth_uri('request', self.discovery, self.params)
+
resp, content = client.request(uri, 'POST', headers=headers,
body=body)
if resp['status'] != '200':
- print content
+ logging.error('Failed to retrieve temporary authorization: %s' % content)
raise Exception('Invalid response %s.' % resp['status'])
self.request_token = dict(parse_qsl(content))
@@ -104,15 +206,24 @@
auth_params = copy.copy(self.params)
auth_params['oauth_token'] = self.request_token['oauth_token']
- uri = _oauth_uri('authorize', self.discovery, auth_params)
- return uri
+ return _oauth_uri('authorize', self.discovery, auth_params)
- def step2_pin(self, pin):
- """Returns an oauth_token and oauth_token_secret in a dictionary"""
+ def step2_exchange(self, verifier):
+ """Exhanges an authorized request token
+ for OAuthCredentials.
- token = oauth.Token(self.request_token['oauth_token'],
+ verifier - either the verifier token, or a dictionary
+ of the query parameters to the callback, which contains
+ the oauth_verifier.
+ """
+
+ if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
+ verifier = verifier['oauth_verifier']
+
+ token = oauth.Token(
+ self.request_token['oauth_token'],
self.request_token['oauth_token_secret'])
- token.set_verifier(pin)
+ token.set_verifier(verifier)
consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
client = oauth.Client(consumer, token)
@@ -123,8 +234,13 @@
uri = _oauth_uri('access', self.discovery, self.params)
resp, content = client.request(uri, 'POST', headers=headers)
- return dict(parse_qsl(content))
+ if resp['status'] != '200':
+ logging.error('Failed to retrieve access token: %s' % content)
+ raise Exception('Invalid response %s.' % resp['status'])
- def step2_callback(self, query_params):
- """Returns an access token via oauth.Token"""
- pass
+ oauth_params = dict(parse_qsl(content))
+ token = oauth.Token(
+ oauth_params['oauth_token'],
+ oauth_params['oauth_token_secret'])
+
+ return OAuthCredentials(consumer, token, self.user_agent)