blob: f97ce6946ef53328886fbc2f5814d57373e51bba [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
JacobMoshenko8e905102011-06-20 09:53:10 -040039from google.appengine.api.app_identity 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
59 Engine application itself. The algorithm used for generating the assertion is
60 the Signed JSON Web Token (JWT) algorithm. Additional details can be found at
61 the following link:
62
63 http://self-issued.info/docs/draft-jones-json-web-token.html
64
65 This credential does not require a flow to instantiate because it represents
66 a two legged flow, and therefore has all of the required information to
67 generate and refresh its own access tokens.
68
JacobMoshenko8e905102011-06-20 09:53:10 -040069 """
70
JacobMoshenkocb6d8912011-07-08 13:35:15 -040071 def __init__(self, scope,
JacobMoshenko8e905102011-06-20 09:53:10 -040072 audience='https://accounts.google.com/o/oauth2/token',
73 assertion_type='http://oauth.net/grant_type/jwt/1.0/bearer',
74 token_uri='https://accounts.google.com/o/oauth2/token', **kwargs):
75 """Constructor for AppAssertionCredentials
76
77 Args:
78 scope: string, scope of the credentials being requested.
JacobMoshenko8e905102011-06-20 09:53:10 -040079 audience: string, The audience, or verifier of the assertion. For
80 convenience defaults to Google's audience.
81 assertion_type: string, Type name that will identify the format of the
82 assertion string. For convience, defaults to the JSON Web Token (JWT)
83 assertion type string.
84 token_uri: string, URI for token endpoint. For convenience
85 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
86 """
87 self.scope = scope
88 self.audience = audience
89 self.app_name = app_identity.get_service_account_name()
90
91 super(AppAssertionCredentials, self).__init__(
92 assertion_type,
JacobMoshenkocb6d8912011-07-08 13:35:15 -040093 None,
JacobMoshenko8e905102011-06-20 09:53:10 -040094 token_uri)
95
Joe Gregorio562b7312011-09-15 09:06:38 -040096 @classmethod
97 def from_json(cls, json):
98 data = simplejson.loads(json)
99 retval = AccessTokenCredentials(
100 data['scope'],
101 data['audience'],
102 data['assertion_type'],
103 data['token_uri'])
104 return retval
105
JacobMoshenko8e905102011-06-20 09:53:10 -0400106 def _generate_assertion(self):
107 header = {
108 'typ': 'JWT',
109 'alg': 'RS256',
110 }
111
112 now = int(time.time())
113 claims = {
114 'aud': self.audience,
115 'scope': self.scope,
116 'iat': now,
117 'exp': now + 3600,
118 'iss': self.app_name,
119 }
120
121 jwt_components = [base64.b64encode(simplejson.dumps(seg))
122 for seg in [header, claims]]
123
124 base_str = ".".join(jwt_components)
125 key_name, signature = app_identity.sign_blob(base_str)
126 jwt_components.append(base64.b64encode(signature))
127 return ".".join(jwt_components)
128
129
Joe Gregorio695fdc12011-01-16 16:46:55 -0500130class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500131 """App Engine datastore Property for Flow.
132
133 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500134 oauth2client.Flow"""
135
136 # Tell what the user type is.
137 data_type = Flow
138
139 # For writing to datastore.
140 def get_value_for_datastore(self, model_instance):
141 flow = super(FlowProperty,
142 self).get_value_for_datastore(model_instance)
143 return db.Blob(pickle.dumps(flow))
144
145 # For reading from datastore.
146 def make_value_from_datastore(self, value):
147 if value is None:
148 return None
149 return pickle.loads(value)
150
151 def validate(self, value):
152 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400153 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500154 'to a FlowThreeLegged instance (%s)' %
155 (self.name, value))
156 return super(FlowProperty, self).validate(value)
157
158 def empty(self, value):
159 return not value
160
161
162class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500163 """App Engine datastore Property for Credentials.
164
165 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500166 oath2client.Credentials
167 """
168
169 # Tell what the user type is.
170 data_type = Credentials
171
172 # For writing to datastore.
173 def get_value_for_datastore(self, model_instance):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400174 logging.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500175 cred = super(CredentialsProperty,
176 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400177 if cred is None:
178 cred = ''
179 else:
180 cred = cred.to_json()
181 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500182
183 # For reading from datastore.
184 def make_value_from_datastore(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400185 logging.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500186 if value is None:
187 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400188 if len(value) == 0:
189 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400190 try:
191 credentials = Credentials.new_from_json(value)
192 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400193 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400194 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500195
196 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400197 value = super(CredentialsProperty, self).validate(value)
198 logging.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500199 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400200 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400201 'to a Credentials instance (%s)' %
202 (self.name, value))
203 #if value is not None and not isinstance(value, Credentials):
204 # return None
205 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500206
207
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500208class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500209 """Store and retrieve a single credential to and from
210 the App Engine datastore.
211
212 This Storage helper presumes the Credentials
213 have been stored as a CredenialsProperty
214 on a datastore model class, and that entities
215 are stored by key_name.
216 """
217
Joe Gregorio432f17e2011-05-22 23:18:00 -0400218 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500219 """Constructor for Storage.
220
221 Args:
222 model: db.Model, model class
223 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400224 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400225 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500226 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500227 self._model = model
228 self._key_name = key_name
229 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400230 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500231
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400232 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500233 """Retrieve Credential from datastore.
234
235 Returns:
236 oauth2client.Credentials
237 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400238 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400239 json = self._cache.get(self._key_name)
240 if json:
241 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500242
243 credential = None
244 entity = self._model.get_by_key_name(self._key_name)
245 if entity is not None:
246 credential = getattr(entity, self._property_name)
247 if credential and hasattr(credential, 'set_store'):
248 credential.set_store(self)
249 if self._cache:
250 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400251
Joe Gregorio695fdc12011-01-16 16:46:55 -0500252 return credential
253
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400254 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500255 """Write a Credentials to the datastore.
256
257 Args:
258 credentials: Credentials, the credentials to store.
259 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500260 entity = self._model.get_or_insert(self._key_name)
261 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500262 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400263 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400264 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400265
Joe Gregorioec75dc12012-02-06 13:40:42 -0500266 def locked_delete(self):
267 """Delete Credential from datastore."""
268
269 if self._cache:
270 self._cache.delete(self._key_name)
271
272 entity = self._model.get_by_key_name(self._key_name)
273 if entity is not None:
274 entity.delete()
275
Joe Gregorio432f17e2011-05-22 23:18:00 -0400276
277class CredentialsModel(db.Model):
278 """Storage for OAuth 2.0 Credentials
279
280 Storage of the model is keyed by the user.user_id().
281 """
282 credentials = CredentialsProperty()
283
284
285class OAuth2Decorator(object):
286 """Utility for making OAuth 2.0 easier.
287
288 Instantiate and then use with oauth_required or oauth_aware
289 as decorators on webapp.RequestHandler methods.
290
291 Example:
292
293 decorator = OAuth2Decorator(
294 client_id='837...ent.com',
295 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500296 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400297
298
299 class MainHandler(webapp.RequestHandler):
300
301 @decorator.oauth_required
302 def get(self):
303 http = decorator.http()
304 # http is authorized with the user's Credentials and can be used
305 # in API calls
306
307 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400308
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400309 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400310 auth_uri='https://accounts.google.com/o/oauth2/auth',
Joe Gregoriof08a4982011-10-07 13:11:16 -0400311 token_uri='https://accounts.google.com/o/oauth2/token',
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500312 message=None, **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400313
314 """Constructor for OAuth2Decorator
315
316 Args:
317 client_id: string, client identifier.
318 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400319 scope: string or list of strings, scope(s) of the credentials being
320 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400321 auth_uri: string, URI for authorization endpoint. For convenience
322 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
323 token_uri: string, URI for token endpoint. For convenience
324 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400325 message: Message to display if there are problems with the OAuth 2.0
326 configuration. The message may contain HTML and will be presented on the
327 web interface for any method that uses the decorator.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500328 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
329 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400330 """
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400331 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, None,
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500332 auth_uri, token_uri, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400333 self.credentials = None
334 self._request_handler = None
Joe Gregoriof08a4982011-10-07 13:11:16 -0400335 self._message = message
336 self._in_error = False
337
338 def _display_error_message(self, request_handler):
339 request_handler.response.out.write('<html><body>')
340 request_handler.response.out.write(self._message)
341 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400342
343 def oauth_required(self, method):
344 """Decorator that starts the OAuth 2.0 dance.
345
346 Starts the OAuth dance for the logged in user if they haven't already
347 granted access for this application.
348
349 Args:
350 method: callable, to be decorated method of a webapp.RequestHandler
351 instance.
352 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400353
Joe Gregorio432f17e2011-05-22 23:18:00 -0400354 def check_oauth(request_handler, *args):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400355 if self._in_error:
356 self._display_error_message(request_handler)
357 return
358
Joe Gregoriof427c532011-06-13 09:35:26 -0400359 user = users.get_current_user()
360 # Don't use @login_decorator as this could be used in a POST request.
361 if not user:
362 request_handler.redirect(users.create_login_url(
363 request_handler.request.uri))
364 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400365 # Store the request URI in 'state' so we can use it later
366 self.flow.params['state'] = request_handler.request.url
367 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400368 self.credentials = StorageByKeyName(
369 CredentialsModel, user.user_id(), 'credentials').get()
370
371 if not self.has_credentials():
372 return request_handler.redirect(self.authorize_url())
373 try:
374 method(request_handler, *args)
375 except AccessTokenRefreshError:
376 return request_handler.redirect(self.authorize_url())
377
378 return check_oauth
379
380 def oauth_aware(self, method):
381 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
382
383 Does all the setup for the OAuth dance, but doesn't initiate it.
384 This decorator is useful if you want to create a page that knows
385 whether or not the user has granted access to this application.
386 From within a method decorated with @oauth_aware the has_credentials()
387 and authorize_url() methods can be called.
388
389 Args:
390 method: callable, to be decorated method of a webapp.RequestHandler
391 instance.
392 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400393
Joe Gregorio432f17e2011-05-22 23:18:00 -0400394 def setup_oauth(request_handler, *args):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400395 if self._in_error:
396 self._display_error_message(request_handler)
397 return
398
Joe Gregoriof427c532011-06-13 09:35:26 -0400399 user = users.get_current_user()
400 # Don't use @login_decorator as this could be used in a POST request.
401 if not user:
402 request_handler.redirect(users.create_login_url(
403 request_handler.request.uri))
404 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400405
406
Joe Gregorio432f17e2011-05-22 23:18:00 -0400407 self.flow.params['state'] = request_handler.request.url
408 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400409 self.credentials = StorageByKeyName(
410 CredentialsModel, user.user_id(), 'credentials').get()
411 method(request_handler, *args)
412 return setup_oauth
413
414 def has_credentials(self):
415 """True if for the logged in user there are valid access Credentials.
416
417 Must only be called from with a webapp.RequestHandler subclassed method
418 that had been decorated with either @oauth_required or @oauth_aware.
419 """
420 return self.credentials is not None and not self.credentials.invalid
421
422 def authorize_url(self):
423 """Returns the URL to start the OAuth dance.
424
425 Must only be called from with a webapp.RequestHandler subclassed method
426 that had been decorated with either @oauth_required or @oauth_aware.
427 """
428 callback = self._request_handler.request.relative_url('/oauth2callback')
429 url = self.flow.step1_get_authorize_url(callback)
430 user = users.get_current_user()
431 memcache.set(user.user_id(), pickle.dumps(self.flow),
432 namespace=OAUTH2CLIENT_NAMESPACE)
433 return url
434
435 def http(self):
436 """Returns an authorized http instance.
437
438 Must only be called from within an @oauth_required decorated method, or
439 from within an @oauth_aware decorated method where has_credentials()
440 returns True.
441 """
442 return self.credentials.authorize(httplib2.Http())
443
444
Joe Gregoriof08a4982011-10-07 13:11:16 -0400445class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
446 """An OAuth2Decorator that builds from a clientsecrets file.
447
448 Uses a clientsecrets file as the source for all the information when
449 constructing an OAuth2Decorator.
450
451 Example:
452
453 decorator = OAuth2DecoratorFromClientSecrets(
454 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500455 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400456
457
458 class MainHandler(webapp.RequestHandler):
459
460 @decorator.oauth_required
461 def get(self):
462 http = decorator.http()
463 # http is authorized with the user's Credentials and can be used
464 # in API calls
465 """
466
467 def __init__(self, filename, scope, message=None):
468 """Constructor
469
470 Args:
471 filename: string, File name of client secrets.
472 scope: string, Space separated list of scopes.
473 message: string, A friendly string to display to the user if the
474 clientsecrets file is missing or invalid. The message may contain HTML and
475 will be presented on the web interface for any method that uses the
476 decorator.
477 """
478 try:
479 client_type, client_info = clientsecrets.loadfile(filename)
480 if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
481 raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
482 super(OAuth2DecoratorFromClientSecrets,
483 self).__init__(
484 client_info['client_id'],
485 client_info['client_secret'],
486 scope,
487 client_info['auth_uri'],
488 client_info['token_uri'],
489 message)
490 except clientsecrets.InvalidClientSecretsError:
491 self._in_error = True
492 if message is not None:
493 self._message = message
494 else:
495 self._message = "Please configure your application for OAuth 2.0"
496
497
498def oauth2decorator_from_clientsecrets(filename, scope, message=None):
499 """Creates an OAuth2Decorator populated from a clientsecrets file.
500
501 Args:
502 filename: string, File name of client secrets.
503 scope: string, Space separated list of scopes.
504 message: string, A friendly string to display to the user if the
505 clientsecrets file is missing or invalid. The message may contain HTML and
506 will be presented on the web interface for any method that uses the
507 decorator.
508
509 Returns: An OAuth2Decorator
510
511 """
512 return OAuth2DecoratorFromClientSecrets(filename, scope, message)
513
514
Joe Gregorio432f17e2011-05-22 23:18:00 -0400515class OAuth2Handler(webapp.RequestHandler):
516 """Handler for the redirect_uri of the OAuth 2.0 dance."""
517
518 @login_required
519 def get(self):
520 error = self.request.get('error')
521 if error:
522 errormsg = self.request.get('error_description', error)
JacobMoshenko8e905102011-06-20 09:53:10 -0400523 self.response.out.write(
524 'The authorization request failed: %s' % errormsg)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400525 else:
526 user = users.get_current_user()
JacobMoshenko8e905102011-06-20 09:53:10 -0400527 flow = pickle.loads(memcache.get(user.user_id(),
528 namespace=OAUTH2CLIENT_NAMESPACE))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400529 # This code should be ammended with application specific error
530 # handling. The following cases should be considered:
531 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
532 # 2. What if the step2_exchange fails?
533 if flow:
534 credentials = flow.step2_exchange(self.request.params)
535 StorageByKeyName(
536 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorioa07cb9c2011-12-07 10:51:45 -0500537 self.redirect(str(self.request.get('state')))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400538 else:
539 # TODO Add error handling here.
540 pass
541
542
543application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
544
JacobMoshenko8e905102011-06-20 09:53:10 -0400545
Joe Gregorio432f17e2011-05-22 23:18:00 -0400546def main():
547 run_wsgi_app(application)