Flows no longer need to be saved between uses.
Also introduces util.positional declarations.
Reviewed in http://codereview.appspot.com/6441056/.
Fixes issue #136.
diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py
index 12ba04d..6f63831 100644
--- a/oauth2client/appengine.py
+++ b/oauth2client/appengine.py
@@ -27,21 +27,20 @@
import clientsecrets
-from anyjson import simplejson
-from client import AccessTokenRefreshError
-from client import AssertionCredentials
-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.api import app_identity
+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
-
+from oauth2client import util
+from oauth2client.anyjson import simplejson
+from oauth2client.client import AccessTokenRefreshError
+from oauth2client.client import AssertionCredentials
+from oauth2client.client import Credentials
+from oauth2client.client import Flow
+from oauth2client.client import OAuth2WebServerFlow
+from oauth2client.client import Storage
logger = logging.getLogger(__name__)
@@ -66,6 +65,7 @@
generate and refresh its own access tokens.
"""
+ @util.positional(2)
def __init__(self, scope, **kwargs):
"""Constructor for AppAssertionCredentials
@@ -77,9 +77,8 @@
self.scope = scope
super(AppAssertionCredentials, self).__init__(
- None,
- None,
- None)
+ 'ignored' # assertion_type is ignore in this subclass.
+ )
@classmethod
def from_json(cls, json):
@@ -195,6 +194,7 @@
are stored by key_name.
"""
+ @util.positional(4)
def __init__(self, model, key_name, property_name, cache=None):
"""Constructor for Storage.
@@ -286,11 +286,14 @@
"""
+ @util.positional(4)
def __init__(self, client_id, client_secret, scope,
auth_uri='https://accounts.google.com/o/oauth2/auth',
token_uri='https://accounts.google.com/o/oauth2/token',
user_agent=None,
- message=None, **kwargs):
+ message=None,
+ callback_path='/oauth2callback',
+ **kwargs):
"""Constructor for OAuth2Decorator
@@ -307,15 +310,24 @@
message: Message to display if there are problems with the OAuth 2.0
configuration. The message may contain HTML and will be presented on the
web interface for any method that uses the decorator.
+ callback_path: string, The absolute path to use as the callback URI. Note
+ that this must match up with the URI given when registering the
+ application in the APIs Console.
**kwargs: dict, Keyword arguments are be passed along as kwargs to the
OAuth2WebServerFlow constructor.
"""
- self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
- auth_uri, token_uri, **kwargs)
+ self.flow = None
self.credentials = None
- self._request_handler = None
+ self._client_id = client_id
+ self._client_secret = client_secret
+ self._scope = scope
+ self._auth_uri = auth_uri
+ self._token_uri = token_uri
+ self._user_agent = user_agent
+ self._kwargs = kwargs
self._message = message
self._in_error = False
+ self._callback_path = callback_path
def _display_error_message(self, request_handler):
request_handler.response.out.write('<html><body>')
@@ -344,9 +356,11 @@
request_handler.redirect(users.create_login_url(
request_handler.request.uri))
return
+
+ self._create_flow(request_handler)
+
# 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
self.credentials = StorageByKeyName(
CredentialsModel, user.user_id(), 'credentials').get()
@@ -359,6 +373,26 @@
return check_oauth
+ def _create_flow(self, request_handler):
+ """Create the Flow object.
+
+ The Flow is calculated lazily since we don't know where this app is
+ running until it receives a request, at which point redirect_uri can be
+ calculated and then the Flow object can be constructed.
+
+ Args:
+ request_handler: webapp.RequestHandler, the request handler.
+ """
+ if self.flow is None:
+ redirect_uri = request_handler.request.relative_url(
+ self._callback_path) # Usually /oauth2callback
+ self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
+ self._scope, redirect_uri=redirect_uri,
+ user_agent=self._user_agent,
+ auth_uri=self._auth_uri,
+ token_uri=self._token_uri, **self._kwargs)
+
+
def oauth_aware(self, method):
"""Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
@@ -385,9 +419,9 @@
request_handler.request.uri))
return
+ self._create_flow(request_handler)
self.flow.params['state'] = request_handler.request.url
- self._request_handler = request_handler
self.credentials = StorageByKeyName(
CredentialsModel, user.user_id(), 'credentials').get()
method(request_handler, *args, **kwargs)
@@ -407,11 +441,7 @@
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)
+ url = self.flow.step1_get_authorize_url()
return str(url)
def http(self):
@@ -423,6 +453,70 @@
"""
return self.credentials.authorize(httplib2.Http())
+ @property
+ def callback_path(self):
+ """The absolute path where the callback will occur.
+
+ Note this is the absolute path, not the absolute URI, that will be
+ calculated by the decorator at runtime. See callback_handler() for how this
+ should be used.
+
+ Returns:
+ The callback path as a string.
+ """
+ return self._callback_path
+
+
+ def callback_handler(self):
+ """RequestHandler for the OAuth 2.0 redirect callback.
+
+ Usage:
+ app = webapp.WSGIApplication([
+ ('/index', MyIndexHandler),
+ ...,
+ (decorator.callback_path, decorator.callback_handler())
+ ])
+
+ Returns:
+ A webapp.RequestHandler that handles the redirect back from the
+ server during the OAuth 2.0 dance.
+ """
+ decorator = self
+
+ 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()
+ decorator._create_flow(self)
+ credentials = decorator.flow.step2_exchange(self.request.params)
+ StorageByKeyName(
+ CredentialsModel, user.user_id(), 'credentials').put(credentials)
+ self.redirect(str(self.request.get('state')))
+
+ return OAuth2Handler
+
+ def callback_application(self):
+ """WSGI application for handling the OAuth 2.0 redirect callback.
+
+ If you need finer grained control use `callback_handler` which returns just
+ the webapp.RequestHandler.
+
+ Returns:
+ A webapp.WSGIApplication that handles the redirect back from the
+ server during the OAuth 2.0 dance.
+ """
+ return webapp.WSGIApplication([
+ (self.callback_path, self.callback_handler())
+ ])
+
class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
"""An OAuth2Decorator that builds from a clientsecrets file.
@@ -446,6 +540,7 @@
# in API calls
"""
+ @util.positional(3)
def __init__(self, filename, scope, message=None, cache=None):
"""Constructor
@@ -457,7 +552,7 @@
clientsecrets file is missing or invalid. The message may contain HTML and
will be presented on the web interface for any method that uses the
decorator.
- cache: An optional cache service client that implements get() and set()
+ cache: An optional cache service client that implements get() and set()
methods. See clientsecrets.loadfile() for details.
"""
try:
@@ -469,9 +564,9 @@
client_info['client_id'],
client_info['client_secret'],
scope,
- client_info['auth_uri'],
- client_info['token_uri'],
- message)
+ auth_uri=client_info['auth_uri'],
+ token_uri=client_info['token_uri'],
+ message=message)
except clientsecrets.InvalidClientSecretsError:
self._in_error = True
if message is not None:
@@ -480,7 +575,8 @@
self._message = "Please configure your application for OAuth 2.0"
-def oauth2decorator_from_clientsecrets(filename, scope,
+@util.positional(2)
+def oauth2decorator_from_clientsecrets(filename, scope,
message=None, cache=None):
"""Creates an OAuth2Decorator populated from a clientsecrets file.
@@ -492,46 +588,11 @@
clientsecrets file is missing or invalid. The message may contain HTML and
will be presented on the web interface for any method that uses the
decorator.
- cache: An optional cache service client that implements get() and set()
+ cache: An optional cache service client that implements get() and set()
methods. See clientsecrets.loadfile() for details.
Returns: An OAuth2Decorator
"""
- return OAuth2DecoratorFromClientSecrets(filename, scope,
+ return OAuth2DecoratorFromClientSecrets(filename, scope,
message=message, cache=cache)
-
-
-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(str(self.request.get('state')))
- else:
- # TODO Add error handling here.
- pass
-
-
-application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
-
-
-def main():
- run_wsgi_app(application)