blob: 723de8af47e1284faa65c67d16d425cfa2cb2d99 [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
28try: # pragma: no cover
29 import simplejson
30except ImportError: # pragma: no cover
31 try:
32 # Try to import from django, should work on App Engine
33 from django.utils import simplejson
34 except ImportError:
35 # Should work for Python2.6 and higher.
36 import json as simplejson
Joe Gregorio695fdc12011-01-16 16:46:55 -050037
Joe Gregoriof08a4982011-10-07 13:11:16 -040038import clientsecrets
39
Joe Gregorio432f17e2011-05-22 23:18:00 -040040from client import AccessTokenRefreshError
JacobMoshenko8e905102011-06-20 09:53:10 -040041from client import AssertionCredentials
Joe Gregorio695fdc12011-01-16 16:46:55 -050042from client import Credentials
43from client import Flow
Joe Gregorio432f17e2011-05-22 23:18:00 -040044from client import OAuth2WebServerFlow
Joe Gregoriodeeb0202011-02-15 14:49:57 -050045from client import Storage
Joe Gregorio432f17e2011-05-22 23:18:00 -040046from google.appengine.api import memcache
47from google.appengine.api import users
JacobMoshenko8e905102011-06-20 09:53:10 -040048from google.appengine.api.app_identity import app_identity
Joe Gregorio432f17e2011-05-22 23:18:00 -040049from google.appengine.ext import db
50from google.appengine.ext import webapp
51from google.appengine.ext.webapp.util import login_required
52from google.appengine.ext.webapp.util import run_wsgi_app
Joe Gregorio695fdc12011-01-16 16:46:55 -050053
Joe Gregorio432f17e2011-05-22 23:18:00 -040054OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050055
JacobMoshenko8e905102011-06-20 09:53:10 -040056
Joe Gregoriof08a4982011-10-07 13:11:16 -040057class InvalidClientSecretsError(Exception):
58 """The client_secrets.json file is malformed or missing required fields."""
59 pass
60
61
JacobMoshenko8e905102011-06-20 09:53:10 -040062class AppAssertionCredentials(AssertionCredentials):
63 """Credentials object for App Engine Assertion Grants
64
65 This object will allow an App Engine application to identify itself to Google
66 and other OAuth 2.0 servers that can verify assertions. It can be used for
67 the purpose of accessing data stored under an account assigned to the App
68 Engine application itself. The algorithm used for generating the assertion is
69 the Signed JSON Web Token (JWT) algorithm. Additional details can be found at
70 the following link:
71
72 http://self-issued.info/docs/draft-jones-json-web-token.html
73
74 This credential does not require a flow to instantiate because it represents
75 a two legged flow, and therefore has all of the required information to
76 generate and refresh its own access tokens.
77
JacobMoshenko8e905102011-06-20 09:53:10 -040078 """
79
JacobMoshenkocb6d8912011-07-08 13:35:15 -040080 def __init__(self, scope,
JacobMoshenko8e905102011-06-20 09:53:10 -040081 audience='https://accounts.google.com/o/oauth2/token',
82 assertion_type='http://oauth.net/grant_type/jwt/1.0/bearer',
83 token_uri='https://accounts.google.com/o/oauth2/token', **kwargs):
84 """Constructor for AppAssertionCredentials
85
86 Args:
87 scope: string, scope of the credentials being requested.
JacobMoshenko8e905102011-06-20 09:53:10 -040088 audience: string, The audience, or verifier of the assertion. For
89 convenience defaults to Google's audience.
90 assertion_type: string, Type name that will identify the format of the
91 assertion string. For convience, defaults to the JSON Web Token (JWT)
92 assertion type string.
93 token_uri: string, URI for token endpoint. For convenience
94 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
95 """
96 self.scope = scope
97 self.audience = audience
98 self.app_name = app_identity.get_service_account_name()
99
100 super(AppAssertionCredentials, self).__init__(
101 assertion_type,
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400102 None,
JacobMoshenko8e905102011-06-20 09:53:10 -0400103 token_uri)
104
Joe Gregorio562b7312011-09-15 09:06:38 -0400105 @classmethod
106 def from_json(cls, json):
107 data = simplejson.loads(json)
108 retval = AccessTokenCredentials(
109 data['scope'],
110 data['audience'],
111 data['assertion_type'],
112 data['token_uri'])
113 return retval
114
JacobMoshenko8e905102011-06-20 09:53:10 -0400115 def _generate_assertion(self):
116 header = {
117 'typ': 'JWT',
118 'alg': 'RS256',
119 }
120
121 now = int(time.time())
122 claims = {
123 'aud': self.audience,
124 'scope': self.scope,
125 'iat': now,
126 'exp': now + 3600,
127 'iss': self.app_name,
128 }
129
130 jwt_components = [base64.b64encode(simplejson.dumps(seg))
131 for seg in [header, claims]]
132
133 base_str = ".".join(jwt_components)
134 key_name, signature = app_identity.sign_blob(base_str)
135 jwt_components.append(base64.b64encode(signature))
136 return ".".join(jwt_components)
137
138
Joe Gregorio695fdc12011-01-16 16:46:55 -0500139class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500140 """App Engine datastore Property for Flow.
141
142 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500143 oauth2client.Flow"""
144
145 # Tell what the user type is.
146 data_type = Flow
147
148 # For writing to datastore.
149 def get_value_for_datastore(self, model_instance):
150 flow = super(FlowProperty,
151 self).get_value_for_datastore(model_instance)
152 return db.Blob(pickle.dumps(flow))
153
154 # For reading from datastore.
155 def make_value_from_datastore(self, value):
156 if value is None:
157 return None
158 return pickle.loads(value)
159
160 def validate(self, value):
161 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400162 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500163 'to a FlowThreeLegged instance (%s)' %
164 (self.name, value))
165 return super(FlowProperty, self).validate(value)
166
167 def empty(self, value):
168 return not value
169
170
171class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500172 """App Engine datastore Property for Credentials.
173
174 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500175 oath2client.Credentials
176 """
177
178 # Tell what the user type is.
179 data_type = Credentials
180
181 # For writing to datastore.
182 def get_value_for_datastore(self, model_instance):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400183 logging.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500184 cred = super(CredentialsProperty,
185 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400186 if cred is None:
187 cred = ''
188 else:
189 cred = cred.to_json()
190 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500191
192 # For reading from datastore.
193 def make_value_from_datastore(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400194 logging.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500195 if value is None:
196 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400197 if len(value) == 0:
198 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400199 try:
200 credentials = Credentials.new_from_json(value)
201 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400202 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400203 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500204
205 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400206 value = super(CredentialsProperty, self).validate(value)
207 logging.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500208 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400209 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400210 'to a Credentials instance (%s)' %
211 (self.name, value))
212 #if value is not None and not isinstance(value, Credentials):
213 # return None
214 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500215
216
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500217class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500218 """Store and retrieve a single credential to and from
219 the App Engine datastore.
220
221 This Storage helper presumes the Credentials
222 have been stored as a CredenialsProperty
223 on a datastore model class, and that entities
224 are stored by key_name.
225 """
226
Joe Gregorio432f17e2011-05-22 23:18:00 -0400227 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500228 """Constructor for Storage.
229
230 Args:
231 model: db.Model, model class
232 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400233 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400234 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500235 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500236 self._model = model
237 self._key_name = key_name
238 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400239 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500240
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400241 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500242 """Retrieve Credential from datastore.
243
244 Returns:
245 oauth2client.Credentials
246 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400247 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400248 json = self._cache.get(self._key_name)
249 if json:
250 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500251
252 credential = None
253 entity = self._model.get_by_key_name(self._key_name)
254 if entity is not None:
255 credential = getattr(entity, self._property_name)
256 if credential and hasattr(credential, 'set_store'):
257 credential.set_store(self)
258 if self._cache:
259 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400260
Joe Gregorio695fdc12011-01-16 16:46:55 -0500261 return credential
262
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400263 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500264 """Write a Credentials to the datastore.
265
266 Args:
267 credentials: Credentials, the credentials to store.
268 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500269 entity = self._model.get_or_insert(self._key_name)
270 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500271 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400272 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400273 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400274
275
276class CredentialsModel(db.Model):
277 """Storage for OAuth 2.0 Credentials
278
279 Storage of the model is keyed by the user.user_id().
280 """
281 credentials = CredentialsProperty()
282
283
284class OAuth2Decorator(object):
285 """Utility for making OAuth 2.0 easier.
286
287 Instantiate and then use with oauth_required or oauth_aware
288 as decorators on webapp.RequestHandler methods.
289
290 Example:
291
292 decorator = OAuth2Decorator(
293 client_id='837...ent.com',
294 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500295 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400296
297
298 class MainHandler(webapp.RequestHandler):
299
300 @decorator.oauth_required
301 def get(self):
302 http = decorator.http()
303 # http is authorized with the user's Credentials and can be used
304 # in API calls
305
306 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400307
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400308 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400309 auth_uri='https://accounts.google.com/o/oauth2/auth',
Joe Gregoriof08a4982011-10-07 13:11:16 -0400310 token_uri='https://accounts.google.com/o/oauth2/token',
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500311 message=None, **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400312
313 """Constructor for OAuth2Decorator
314
315 Args:
316 client_id: string, client identifier.
317 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400318 scope: string or list of strings, scope(s) of the credentials being
319 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400320 auth_uri: string, URI for authorization endpoint. For convenience
321 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
322 token_uri: string, URI for token endpoint. For convenience
323 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400324 message: Message to display if there are problems with the OAuth 2.0
325 configuration. The message may contain HTML and will be presented on the
326 web interface for any method that uses the decorator.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500327 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
328 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400329 """
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400330 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, None,
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500331 auth_uri, token_uri, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400332 self.credentials = None
333 self._request_handler = None
Joe Gregoriof08a4982011-10-07 13:11:16 -0400334 self._message = message
335 self._in_error = False
336
337 def _display_error_message(self, request_handler):
338 request_handler.response.out.write('<html><body>')
339 request_handler.response.out.write(self._message)
340 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400341
342 def oauth_required(self, method):
343 """Decorator that starts the OAuth 2.0 dance.
344
345 Starts the OAuth dance for the logged in user if they haven't already
346 granted access for this application.
347
348 Args:
349 method: callable, to be decorated method of a webapp.RequestHandler
350 instance.
351 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400352
Joe Gregorio432f17e2011-05-22 23:18:00 -0400353 def check_oauth(request_handler, *args):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400354 if self._in_error:
355 self._display_error_message(request_handler)
356 return
357
Joe Gregoriof427c532011-06-13 09:35:26 -0400358 user = users.get_current_user()
359 # Don't use @login_decorator as this could be used in a POST request.
360 if not user:
361 request_handler.redirect(users.create_login_url(
362 request_handler.request.uri))
363 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400364 # Store the request URI in 'state' so we can use it later
365 self.flow.params['state'] = request_handler.request.url
366 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400367 self.credentials = StorageByKeyName(
368 CredentialsModel, user.user_id(), 'credentials').get()
369
370 if not self.has_credentials():
371 return request_handler.redirect(self.authorize_url())
372 try:
373 method(request_handler, *args)
374 except AccessTokenRefreshError:
375 return request_handler.redirect(self.authorize_url())
376
377 return check_oauth
378
379 def oauth_aware(self, method):
380 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
381
382 Does all the setup for the OAuth dance, but doesn't initiate it.
383 This decorator is useful if you want to create a page that knows
384 whether or not the user has granted access to this application.
385 From within a method decorated with @oauth_aware the has_credentials()
386 and authorize_url() methods can be called.
387
388 Args:
389 method: callable, to be decorated method of a webapp.RequestHandler
390 instance.
391 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400392
Joe Gregorio432f17e2011-05-22 23:18:00 -0400393 def setup_oauth(request_handler, *args):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400394 if self._in_error:
395 self._display_error_message(request_handler)
396 return
397
Joe Gregoriof427c532011-06-13 09:35:26 -0400398 user = users.get_current_user()
399 # Don't use @login_decorator as this could be used in a POST request.
400 if not user:
401 request_handler.redirect(users.create_login_url(
402 request_handler.request.uri))
403 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400404
405
Joe Gregorio432f17e2011-05-22 23:18:00 -0400406 self.flow.params['state'] = request_handler.request.url
407 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400408 self.credentials = StorageByKeyName(
409 CredentialsModel, user.user_id(), 'credentials').get()
410 method(request_handler, *args)
411 return setup_oauth
412
413 def has_credentials(self):
414 """True if for the logged in user there are valid access Credentials.
415
416 Must only be called from with a webapp.RequestHandler subclassed method
417 that had been decorated with either @oauth_required or @oauth_aware.
418 """
419 return self.credentials is not None and not self.credentials.invalid
420
421 def authorize_url(self):
422 """Returns the URL to start the OAuth dance.
423
424 Must only be called from with a webapp.RequestHandler subclassed method
425 that had been decorated with either @oauth_required or @oauth_aware.
426 """
427 callback = self._request_handler.request.relative_url('/oauth2callback')
428 url = self.flow.step1_get_authorize_url(callback)
429 user = users.get_current_user()
430 memcache.set(user.user_id(), pickle.dumps(self.flow),
431 namespace=OAUTH2CLIENT_NAMESPACE)
432 return url
433
434 def http(self):
435 """Returns an authorized http instance.
436
437 Must only be called from within an @oauth_required decorated method, or
438 from within an @oauth_aware decorated method where has_credentials()
439 returns True.
440 """
441 return self.credentials.authorize(httplib2.Http())
442
443
Joe Gregoriof08a4982011-10-07 13:11:16 -0400444class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
445 """An OAuth2Decorator that builds from a clientsecrets file.
446
447 Uses a clientsecrets file as the source for all the information when
448 constructing an OAuth2Decorator.
449
450 Example:
451
452 decorator = OAuth2DecoratorFromClientSecrets(
453 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500454 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400455
456
457 class MainHandler(webapp.RequestHandler):
458
459 @decorator.oauth_required
460 def get(self):
461 http = decorator.http()
462 # http is authorized with the user's Credentials and can be used
463 # in API calls
464 """
465
466 def __init__(self, filename, scope, message=None):
467 """Constructor
468
469 Args:
470 filename: string, File name of client secrets.
471 scope: string, Space separated list of scopes.
472 message: string, A friendly string to display to the user if the
473 clientsecrets file is missing or invalid. The message may contain HTML and
474 will be presented on the web interface for any method that uses the
475 decorator.
476 """
477 try:
478 client_type, client_info = clientsecrets.loadfile(filename)
479 if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
480 raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
481 super(OAuth2DecoratorFromClientSecrets,
482 self).__init__(
483 client_info['client_id'],
484 client_info['client_secret'],
485 scope,
486 client_info['auth_uri'],
487 client_info['token_uri'],
488 message)
489 except clientsecrets.InvalidClientSecretsError:
490 self._in_error = True
491 if message is not None:
492 self._message = message
493 else:
494 self._message = "Please configure your application for OAuth 2.0"
495
496
497def oauth2decorator_from_clientsecrets(filename, scope, message=None):
498 """Creates an OAuth2Decorator populated from a clientsecrets file.
499
500 Args:
501 filename: string, File name of client secrets.
502 scope: string, Space separated list of scopes.
503 message: string, A friendly string to display to the user if the
504 clientsecrets file is missing or invalid. The message may contain HTML and
505 will be presented on the web interface for any method that uses the
506 decorator.
507
508 Returns: An OAuth2Decorator
509
510 """
511 return OAuth2DecoratorFromClientSecrets(filename, scope, message)
512
513
Joe Gregorio432f17e2011-05-22 23:18:00 -0400514class OAuth2Handler(webapp.RequestHandler):
515 """Handler for the redirect_uri of the OAuth 2.0 dance."""
516
517 @login_required
518 def get(self):
519 error = self.request.get('error')
520 if error:
521 errormsg = self.request.get('error_description', error)
JacobMoshenko8e905102011-06-20 09:53:10 -0400522 self.response.out.write(
523 'The authorization request failed: %s' % errormsg)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400524 else:
525 user = users.get_current_user()
JacobMoshenko8e905102011-06-20 09:53:10 -0400526 flow = pickle.loads(memcache.get(user.user_id(),
527 namespace=OAUTH2CLIENT_NAMESPACE))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400528 # This code should be ammended with application specific error
529 # handling. The following cases should be considered:
530 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
531 # 2. What if the step2_exchange fails?
532 if flow:
533 credentials = flow.step2_exchange(self.request.params)
534 StorageByKeyName(
535 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorioa07cb9c2011-12-07 10:51:45 -0500536 self.redirect(str(self.request.get('state')))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400537 else:
538 # TODO Add error handling here.
539 pass
540
541
542application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
543
JacobMoshenko8e905102011-06-20 09:53:10 -0400544
Joe Gregorio432f17e2011-05-22 23:18:00 -0400545def main():
546 run_wsgi_app(application)