oauth2decorator, reviewed in http://codereview.appspot.com/4524063/
diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py
index f939bc0..5600387 100644
--- a/oauth2client/appengine.py
+++ b/oauth2client/appengine.py
@@ -19,13 +19,22 @@
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+import httplib2
import pickle
-from google.appengine.ext import db
+from client import AccessTokenRefreshError
from client import Credentials
from client import Flow
+from client import OAuth2WebServerFlow
from client import Storage
+from google.appengine.api import memcache
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp.util import login_required
+from google.appengine.ext.webapp.util import run_wsgi_app
+OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
class FlowProperty(db.Property):
"""App Engine datastore Property for Flow.
@@ -102,17 +111,19 @@
are stored by key_name.
"""
- def __init__(self, model, key_name, property_name):
+ def __init__(self, model, key_name, property_name, cache=None):
"""Constructor for Storage.
Args:
model: db.Model, model class
key_name: string, key name for the entity that has the credentials
property_name: string, name of the property that is an CredentialsProperty
+ cache: memcache, a write-through cache to put in front of the datastore
"""
self._model = model
self._key_name = key_name
self._property_name = property_name
+ self._cache = cache
def get(self):
"""Retrieve Credential from datastore.
@@ -120,10 +131,17 @@
Returns:
oauth2client.Credentials
"""
+ if self._cache:
+ credential = self._cache.get(self._key_name)
+ if credential:
+ return pickle.loads(credential)
entity = self._model.get_or_insert(self._key_name)
credential = getattr(entity, self._property_name)
if credential and hasattr(credential, 'set_store'):
credential.set_store(self.put)
+ if self._cache:
+ self._cache.set(self._key_name, pickle.dumps(credentials))
+
return credential
def put(self, credentials):
@@ -135,3 +153,174 @@
entity = self._model.get_or_insert(self._key_name)
setattr(entity, self._property_name, credentials)
entity.put()
+ if self._cache:
+ self._cache.set(self._key_name, pickle.dumps(credentials))
+
+
+class CredentialsModel(db.Model):
+ """Storage for OAuth 2.0 Credentials
+
+ Storage of the model is keyed by the user.user_id().
+ """
+ credentials = CredentialsProperty()
+
+
+class OAuth2Decorator(object):
+ """Utility for making OAuth 2.0 easier.
+
+ Instantiate and then use with oauth_required or oauth_aware
+ as decorators on webapp.RequestHandler methods.
+
+ Example:
+
+ decorator = OAuth2Decorator(
+ client_id='837...ent.com',
+ client_secret='Qh...wwI',
+ scope='https://www.googleapis.com/auth/buzz',
+ user_agent='my-sample-app/1.0')
+
+
+ class MainHandler(webapp.RequestHandler):
+
+ @decorator.oauth_required
+ def get(self):
+ http = decorator.http()
+ # http is authorized with the user's Credentials and can be used
+ # in API calls
+
+ """
+ def __init__(self, client_id, client_secret, scope, user_agent,
+ auth_uri='https://accounts.google.com/o/oauth2/auth',
+ token_uri='https://accounts.google.com/o/oauth2/token'):
+
+ """Constructor for OAuth2Decorator
+
+ Args:
+ client_id: string, client identifier.
+ client_secret: string client secret.
+ scope: string, scope of the credentials being requested.
+ user_agent: string, HTTP User-Agent to provide for this application.
+ auth_uri: string, URI for authorization endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ token_uri: string, URI for token endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ """
+ self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
+ auth_uri, token_uri)
+ self.credentials = None
+ self._request_handler = None
+
+ def oauth_required(self, method):
+ """Decorator that starts the OAuth 2.0 dance.
+
+ Starts the OAuth dance for the logged in user if they haven't already
+ granted access for this application.
+
+ Args:
+ method: callable, to be decorated method of a webapp.RequestHandler
+ instance.
+ """
+ @login_required
+ def check_oauth(request_handler, *args):
+ # Store the request URI in 'state' so we can use it later
+ self.flow.params['state'] = request_handler.request.url
+ self._request_handler = request_handler
+ user = users.get_current_user()
+ self.credentials = StorageByKeyName(
+ CredentialsModel, user.user_id(), 'credentials').get()
+
+ if not self.has_credentials():
+ return request_handler.redirect(self.authorize_url())
+ try:
+ method(request_handler, *args)
+ except AccessTokenRefreshError:
+ return request_handler.redirect(self.authorize_url())
+
+ return check_oauth
+
+ def oauth_aware(self, method):
+ """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
+
+ Does all the setup for the OAuth dance, but doesn't initiate it.
+ This decorator is useful if you want to create a page that knows
+ whether or not the user has granted access to this application.
+ From within a method decorated with @oauth_aware the has_credentials()
+ and authorize_url() methods can be called.
+
+ Args:
+ method: callable, to be decorated method of a webapp.RequestHandler
+ instance.
+ """
+ @login_required
+ def setup_oauth(request_handler, *args):
+ self.flow.params['state'] = request_handler.request.url
+ self._request_handler = request_handler
+ user = users.get_current_user()
+ self.credentials = StorageByKeyName(
+ CredentialsModel, user.user_id(), 'credentials').get()
+ method(request_handler, *args)
+ return setup_oauth
+
+ def has_credentials(self):
+ """True if for the logged in user there are valid access Credentials.
+
+ Must only be called from with a webapp.RequestHandler subclassed method
+ that had been decorated with either @oauth_required or @oauth_aware.
+ """
+ return self.credentials is not None and not self.credentials.invalid
+
+ def authorize_url(self):
+ """Returns the URL to start the OAuth dance.
+
+ Must only be called from with a webapp.RequestHandler subclassed method
+ that had been decorated with either @oauth_required or @oauth_aware.
+ """
+ callback = self._request_handler.request.relative_url('/oauth2callback')
+ url = self.flow.step1_get_authorize_url(callback)
+ user = users.get_current_user()
+ memcache.set(user.user_id(), pickle.dumps(self.flow),
+ namespace=OAUTH2CLIENT_NAMESPACE)
+ return url
+
+ def http(self):
+ """Returns an authorized http instance.
+
+ Must only be called from within an @oauth_required decorated method, or
+ from within an @oauth_aware decorated method where has_credentials()
+ returns True.
+ """
+ return self.credentials.authorize(httplib2.Http())
+
+
+class OAuth2Handler(webapp.RequestHandler):
+ """Handler for the redirect_uri of the OAuth 2.0 dance."""
+
+ @login_required
+ def get(self):
+ error = self.request.get('error')
+ if error:
+ errormsg = self.request.get('error_description', error)
+ self.response.out.write('The authorization request failed: %s' % errormsg)
+ else:
+ user = users.get_current_user()
+ flow = pickle.loads(memcache.get(user.user_id(), namespace=OAUTH2CLIENT_NAMESPACE))
+ # This code should be ammended with application specific error
+ # handling. The following cases should be considered:
+ # 1. What if the flow doesn't exist in memcache? Or is corrupt?
+ # 2. What if the step2_exchange fails?
+ if flow:
+ credentials = flow.step2_exchange(self.request.params)
+ StorageByKeyName(
+ CredentialsModel, user.user_id(), 'credentials').put(credentials)
+ self.redirect(self.request.get('state'))
+ else:
+ # TODO Add error handling here.
+ pass
+
+
+application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
+
+def main():
+ run_wsgi_app(application)
+
+#