blob: 3b7ca3d79c9fbe68e3968ac1c7467ed58e590e8b [file] [log] [blame]
Joe Gregorio695fdc12011-01-16 16:46:55 -05001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Utilities for Google App Engine
16
Joe Gregorio7c22ab22011-02-16 15:32:39 -050017Utilities for making it easier to use OAuth 2.0 on Google App Engine.
Joe Gregorio695fdc12011-01-16 16:46:55 -050018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
Joe Gregorio1daa71b2011-09-15 18:12:14 -040022import base64
Joe Gregorio432f17e2011-05-22 23:18:00 -040023import httplib2
Joe Gregorio1daa71b2011-09-15 18:12:14 -040024import logging
Joe Gregorio695fdc12011-01-16 16:46:55 -050025import pickle
JacobMoshenko8e905102011-06-20 09:53:10 -040026import time
JacobMoshenko8e905102011-06-20 09:53:10 -040027
Joe Gregoriof08a4982011-10-07 13:11:16 -040028import clientsecrets
29
Joe Gregorio549230c2012-01-11 10:38:05 -050030from anyjson import simplejson
Joe Gregorio432f17e2011-05-22 23:18:00 -040031from client import AccessTokenRefreshError
JacobMoshenko8e905102011-06-20 09:53:10 -040032from client import AssertionCredentials
Joe Gregorio695fdc12011-01-16 16:46:55 -050033from client import Credentials
34from client import Flow
Joe Gregorio432f17e2011-05-22 23:18:00 -040035from client import OAuth2WebServerFlow
Joe Gregoriodeeb0202011-02-15 14:49:57 -050036from client import Storage
Joe Gregorio432f17e2011-05-22 23:18:00 -040037from google.appengine.api import memcache
38from google.appengine.api import users
Joe Gregoriod84d6b82012-02-28 14:53:00 -050039from google.appengine.api import app_identity
Joe Gregorio432f17e2011-05-22 23:18:00 -040040from google.appengine.ext import db
41from google.appengine.ext import webapp
42from google.appengine.ext.webapp.util import login_required
43from google.appengine.ext.webapp.util import run_wsgi_app
Joe Gregorio695fdc12011-01-16 16:46:55 -050044
Joe Gregorio432f17e2011-05-22 23:18:00 -040045OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050046
JacobMoshenko8e905102011-06-20 09:53:10 -040047
Joe Gregoriof08a4982011-10-07 13:11:16 -040048class InvalidClientSecretsError(Exception):
49 """The client_secrets.json file is malformed or missing required fields."""
50 pass
51
52
JacobMoshenko8e905102011-06-20 09:53:10 -040053class AppAssertionCredentials(AssertionCredentials):
54 """Credentials object for App Engine Assertion Grants
55
56 This object will allow an App Engine application to identify itself to Google
57 and other OAuth 2.0 servers that can verify assertions. It can be used for
58 the purpose of accessing data stored under an account assigned to the App
Joe Gregoriod84d6b82012-02-28 14:53:00 -050059 Engine application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -040060
61 This credential does not require a flow to instantiate because it represents
62 a two legged flow, and therefore has all of the required information to
63 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -040064 """
65
Joe Gregoriod84d6b82012-02-28 14:53:00 -050066 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -040067 """Constructor for AppAssertionCredentials
68
69 Args:
Joe Gregoriod84d6b82012-02-28 14:53:00 -050070 scope: string or list of strings, scope(s) of the credentials being requested.
JacobMoshenko8e905102011-06-20 09:53:10 -040071 """
Joe Gregoriod84d6b82012-02-28 14:53:00 -050072 if type(scope) is list:
73 scope = ' '.join(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -040074 self.scope = scope
JacobMoshenko8e905102011-06-20 09:53:10 -040075
76 super(AppAssertionCredentials, self).__init__(
JacobMoshenkocb6d8912011-07-08 13:35:15 -040077 None,
Joe Gregoriod84d6b82012-02-28 14:53:00 -050078 None,
79 None)
JacobMoshenko8e905102011-06-20 09:53:10 -040080
Joe Gregorio562b7312011-09-15 09:06:38 -040081 @classmethod
82 def from_json(cls, json):
83 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -050084 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -040085
Joe Gregoriod84d6b82012-02-28 14:53:00 -050086 def _refresh(self, http_request):
87 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -040088
Joe Gregoriod84d6b82012-02-28 14:53:00 -050089 Since the underlying App Engine app_identity implementation does its own
90 caching we can skip all the storage hoops and just to a refresh using the
91 API.
JacobMoshenko8e905102011-06-20 09:53:10 -040092
Joe Gregoriod84d6b82012-02-28 14:53:00 -050093 Args:
94 http_request: callable, a callable that matches the method signature of
95 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -040096
Joe Gregoriod84d6b82012-02-28 14:53:00 -050097 Raises:
98 AccessTokenRefreshError: When the refresh fails.
99 """
100 try:
101 (token, _) = app_identity.get_access_token(self.scope)
102 except app_identity.Error, e:
103 raise AccessTokenRefreshError(str(e))
104 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400105
106
Joe Gregorio695fdc12011-01-16 16:46:55 -0500107class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500108 """App Engine datastore Property for Flow.
109
110 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500111 oauth2client.Flow"""
112
113 # Tell what the user type is.
114 data_type = Flow
115
116 # For writing to datastore.
117 def get_value_for_datastore(self, model_instance):
118 flow = super(FlowProperty,
119 self).get_value_for_datastore(model_instance)
120 return db.Blob(pickle.dumps(flow))
121
122 # For reading from datastore.
123 def make_value_from_datastore(self, value):
124 if value is None:
125 return None
126 return pickle.loads(value)
127
128 def validate(self, value):
129 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400130 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500131 'to a FlowThreeLegged instance (%s)' %
132 (self.name, value))
133 return super(FlowProperty, self).validate(value)
134
135 def empty(self, value):
136 return not value
137
138
139class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500140 """App Engine datastore Property for Credentials.
141
142 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500143 oath2client.Credentials
144 """
145
146 # Tell what the user type is.
147 data_type = Credentials
148
149 # For writing to datastore.
150 def get_value_for_datastore(self, model_instance):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400151 logging.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500152 cred = super(CredentialsProperty,
153 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400154 if cred is None:
155 cred = ''
156 else:
157 cred = cred.to_json()
158 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500159
160 # For reading from datastore.
161 def make_value_from_datastore(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400162 logging.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500163 if value is None:
164 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400165 if len(value) == 0:
166 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400167 try:
168 credentials = Credentials.new_from_json(value)
169 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400170 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400171 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500172
173 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400174 value = super(CredentialsProperty, self).validate(value)
175 logging.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500176 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400177 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400178 'to a Credentials instance (%s)' %
179 (self.name, value))
180 #if value is not None and not isinstance(value, Credentials):
181 # return None
182 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500183
184
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500185class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500186 """Store and retrieve a single credential to and from
187 the App Engine datastore.
188
189 This Storage helper presumes the Credentials
190 have been stored as a CredenialsProperty
191 on a datastore model class, and that entities
192 are stored by key_name.
193 """
194
Joe Gregorio432f17e2011-05-22 23:18:00 -0400195 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500196 """Constructor for Storage.
197
198 Args:
199 model: db.Model, model class
200 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400201 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400202 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500203 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500204 self._model = model
205 self._key_name = key_name
206 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400207 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500208
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400209 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500210 """Retrieve Credential from datastore.
211
212 Returns:
213 oauth2client.Credentials
214 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400215 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400216 json = self._cache.get(self._key_name)
217 if json:
218 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500219
220 credential = None
221 entity = self._model.get_by_key_name(self._key_name)
222 if entity is not None:
223 credential = getattr(entity, self._property_name)
224 if credential and hasattr(credential, 'set_store'):
225 credential.set_store(self)
226 if self._cache:
227 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400228
Joe Gregorio695fdc12011-01-16 16:46:55 -0500229 return credential
230
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400231 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500232 """Write a Credentials to the datastore.
233
234 Args:
235 credentials: Credentials, the credentials to store.
236 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500237 entity = self._model.get_or_insert(self._key_name)
238 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500239 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400240 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400241 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400242
Joe Gregorioec75dc12012-02-06 13:40:42 -0500243 def locked_delete(self):
244 """Delete Credential from datastore."""
245
246 if self._cache:
247 self._cache.delete(self._key_name)
248
249 entity = self._model.get_by_key_name(self._key_name)
250 if entity is not None:
251 entity.delete()
252
Joe Gregorio432f17e2011-05-22 23:18:00 -0400253
254class CredentialsModel(db.Model):
255 """Storage for OAuth 2.0 Credentials
256
257 Storage of the model is keyed by the user.user_id().
258 """
259 credentials = CredentialsProperty()
260
261
262class OAuth2Decorator(object):
263 """Utility for making OAuth 2.0 easier.
264
265 Instantiate and then use with oauth_required or oauth_aware
266 as decorators on webapp.RequestHandler methods.
267
268 Example:
269
270 decorator = OAuth2Decorator(
271 client_id='837...ent.com',
272 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500273 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400274
275
276 class MainHandler(webapp.RequestHandler):
277
278 @decorator.oauth_required
279 def get(self):
280 http = decorator.http()
281 # http is authorized with the user's Credentials and can be used
282 # in API calls
283
284 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400285
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400286 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400287 auth_uri='https://accounts.google.com/o/oauth2/auth',
Joe Gregoriof08a4982011-10-07 13:11:16 -0400288 token_uri='https://accounts.google.com/o/oauth2/token',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100289 user_agent=None,
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500290 message=None, **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400291
292 """Constructor for OAuth2Decorator
293
294 Args:
295 client_id: string, client identifier.
296 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400297 scope: string or list of strings, scope(s) of the credentials being
298 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400299 auth_uri: string, URI for authorization endpoint. For convenience
300 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
301 token_uri: string, URI for token endpoint. For convenience
302 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100303 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400304 message: Message to display if there are problems with the OAuth 2.0
305 configuration. The message may contain HTML and will be presented on the
306 web interface for any method that uses the decorator.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500307 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
308 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400309 """
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100310 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500311 auth_uri, token_uri, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400312 self.credentials = None
313 self._request_handler = None
Joe Gregoriof08a4982011-10-07 13:11:16 -0400314 self._message = message
315 self._in_error = False
316
317 def _display_error_message(self, request_handler):
318 request_handler.response.out.write('<html><body>')
319 request_handler.response.out.write(self._message)
320 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400321
322 def oauth_required(self, method):
323 """Decorator that starts the OAuth 2.0 dance.
324
325 Starts the OAuth dance for the logged in user if they haven't already
326 granted access for this application.
327
328 Args:
329 method: callable, to be decorated method of a webapp.RequestHandler
330 instance.
331 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400332
Joe Gregorio432f17e2011-05-22 23:18:00 -0400333 def check_oauth(request_handler, *args):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400334 if self._in_error:
335 self._display_error_message(request_handler)
336 return
337
Joe Gregoriof427c532011-06-13 09:35:26 -0400338 user = users.get_current_user()
339 # Don't use @login_decorator as this could be used in a POST request.
340 if not user:
341 request_handler.redirect(users.create_login_url(
342 request_handler.request.uri))
343 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400344 # Store the request URI in 'state' so we can use it later
345 self.flow.params['state'] = request_handler.request.url
346 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400347 self.credentials = StorageByKeyName(
348 CredentialsModel, user.user_id(), 'credentials').get()
349
350 if not self.has_credentials():
351 return request_handler.redirect(self.authorize_url())
352 try:
353 method(request_handler, *args)
354 except AccessTokenRefreshError:
355 return request_handler.redirect(self.authorize_url())
356
357 return check_oauth
358
359 def oauth_aware(self, method):
360 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
361
362 Does all the setup for the OAuth dance, but doesn't initiate it.
363 This decorator is useful if you want to create a page that knows
364 whether or not the user has granted access to this application.
365 From within a method decorated with @oauth_aware the has_credentials()
366 and authorize_url() methods can be called.
367
368 Args:
369 method: callable, to be decorated method of a webapp.RequestHandler
370 instance.
371 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400372
Joe Gregorio432f17e2011-05-22 23:18:00 -0400373 def setup_oauth(request_handler, *args):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400374 if self._in_error:
375 self._display_error_message(request_handler)
376 return
377
Joe Gregoriof427c532011-06-13 09:35:26 -0400378 user = users.get_current_user()
379 # Don't use @login_decorator as this could be used in a POST request.
380 if not user:
381 request_handler.redirect(users.create_login_url(
382 request_handler.request.uri))
383 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400384
385
Joe Gregorio432f17e2011-05-22 23:18:00 -0400386 self.flow.params['state'] = request_handler.request.url
387 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400388 self.credentials = StorageByKeyName(
389 CredentialsModel, user.user_id(), 'credentials').get()
390 method(request_handler, *args)
391 return setup_oauth
392
393 def has_credentials(self):
394 """True if for the logged in user there are valid access Credentials.
395
396 Must only be called from with a webapp.RequestHandler subclassed method
397 that had been decorated with either @oauth_required or @oauth_aware.
398 """
399 return self.credentials is not None and not self.credentials.invalid
400
401 def authorize_url(self):
402 """Returns the URL to start the OAuth dance.
403
404 Must only be called from with a webapp.RequestHandler subclassed method
405 that had been decorated with either @oauth_required or @oauth_aware.
406 """
407 callback = self._request_handler.request.relative_url('/oauth2callback')
408 url = self.flow.step1_get_authorize_url(callback)
409 user = users.get_current_user()
410 memcache.set(user.user_id(), pickle.dumps(self.flow),
411 namespace=OAUTH2CLIENT_NAMESPACE)
412 return url
413
414 def http(self):
415 """Returns an authorized http instance.
416
417 Must only be called from within an @oauth_required decorated method, or
418 from within an @oauth_aware decorated method where has_credentials()
419 returns True.
420 """
421 return self.credentials.authorize(httplib2.Http())
422
423
Joe Gregoriof08a4982011-10-07 13:11:16 -0400424class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
425 """An OAuth2Decorator that builds from a clientsecrets file.
426
427 Uses a clientsecrets file as the source for all the information when
428 constructing an OAuth2Decorator.
429
430 Example:
431
432 decorator = OAuth2DecoratorFromClientSecrets(
433 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500434 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400435
436
437 class MainHandler(webapp.RequestHandler):
438
439 @decorator.oauth_required
440 def get(self):
441 http = decorator.http()
442 # http is authorized with the user's Credentials and can be used
443 # in API calls
444 """
445
446 def __init__(self, filename, scope, message=None):
447 """Constructor
448
449 Args:
450 filename: string, File name of client secrets.
451 scope: string, Space separated list of scopes.
452 message: string, A friendly string to display to the user if the
453 clientsecrets file is missing or invalid. The message may contain HTML and
454 will be presented on the web interface for any method that uses the
455 decorator.
456 """
457 try:
458 client_type, client_info = clientsecrets.loadfile(filename)
459 if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
460 raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
461 super(OAuth2DecoratorFromClientSecrets,
462 self).__init__(
463 client_info['client_id'],
464 client_info['client_secret'],
465 scope,
466 client_info['auth_uri'],
467 client_info['token_uri'],
468 message)
469 except clientsecrets.InvalidClientSecretsError:
470 self._in_error = True
471 if message is not None:
472 self._message = message
473 else:
474 self._message = "Please configure your application for OAuth 2.0"
475
476
477def oauth2decorator_from_clientsecrets(filename, scope, message=None):
478 """Creates an OAuth2Decorator populated from a clientsecrets file.
479
480 Args:
481 filename: string, File name of client secrets.
482 scope: string, Space separated list of scopes.
483 message: string, A friendly string to display to the user if the
484 clientsecrets file is missing or invalid. The message may contain HTML and
485 will be presented on the web interface for any method that uses the
486 decorator.
487
488 Returns: An OAuth2Decorator
489
490 """
491 return OAuth2DecoratorFromClientSecrets(filename, scope, message)
492
493
Joe Gregorio432f17e2011-05-22 23:18:00 -0400494class OAuth2Handler(webapp.RequestHandler):
495 """Handler for the redirect_uri of the OAuth 2.0 dance."""
496
497 @login_required
498 def get(self):
499 error = self.request.get('error')
500 if error:
501 errormsg = self.request.get('error_description', error)
JacobMoshenko8e905102011-06-20 09:53:10 -0400502 self.response.out.write(
503 'The authorization request failed: %s' % errormsg)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400504 else:
505 user = users.get_current_user()
JacobMoshenko8e905102011-06-20 09:53:10 -0400506 flow = pickle.loads(memcache.get(user.user_id(),
507 namespace=OAUTH2CLIENT_NAMESPACE))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400508 # This code should be ammended with application specific error
509 # handling. The following cases should be considered:
510 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
511 # 2. What if the step2_exchange fails?
512 if flow:
513 credentials = flow.step2_exchange(self.request.params)
514 StorageByKeyName(
515 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorioa07cb9c2011-12-07 10:51:45 -0500516 self.redirect(str(self.request.get('state')))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400517 else:
518 # TODO Add error handling here.
519 pass
520
521
522application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
523
JacobMoshenko8e905102011-06-20 09:53:10 -0400524
Joe Gregorio432f17e2011-05-22 23:18:00 -0400525def main():
526 run_wsgi_app(application)