blob: ca2ff1505887664d33c54a6cb7de5ae272d89776 [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 Gregorioa19f3a72012-07-11 15:35:35 -040045
46logger = logging.getLogger(__name__)
47
Joe Gregorio432f17e2011-05-22 23:18:00 -040048OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050049
JacobMoshenko8e905102011-06-20 09:53:10 -040050
Joe Gregoriof08a4982011-10-07 13:11:16 -040051class InvalidClientSecretsError(Exception):
52 """The client_secrets.json file is malformed or missing required fields."""
53 pass
54
55
JacobMoshenko8e905102011-06-20 09:53:10 -040056class AppAssertionCredentials(AssertionCredentials):
57 """Credentials object for App Engine Assertion Grants
58
59 This object will allow an App Engine application to identify itself to Google
60 and other OAuth 2.0 servers that can verify assertions. It can be used for
61 the purpose of accessing data stored under an account assigned to the App
Joe Gregoriod84d6b82012-02-28 14:53:00 -050062 Engine application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -040063
64 This credential does not require a flow to instantiate because it represents
65 a two legged flow, and therefore has all of the required information to
66 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -040067 """
68
Joe Gregoriod84d6b82012-02-28 14:53:00 -050069 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -040070 """Constructor for AppAssertionCredentials
71
72 Args:
Joe Gregoriod84d6b82012-02-28 14:53:00 -050073 scope: string or list of strings, scope(s) of the credentials being requested.
JacobMoshenko8e905102011-06-20 09:53:10 -040074 """
Joe Gregoriod84d6b82012-02-28 14:53:00 -050075 if type(scope) is list:
76 scope = ' '.join(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -040077 self.scope = scope
JacobMoshenko8e905102011-06-20 09:53:10 -040078
79 super(AppAssertionCredentials, self).__init__(
JacobMoshenkocb6d8912011-07-08 13:35:15 -040080 None,
Joe Gregoriod84d6b82012-02-28 14:53:00 -050081 None,
82 None)
JacobMoshenko8e905102011-06-20 09:53:10 -040083
Joe Gregorio562b7312011-09-15 09:06:38 -040084 @classmethod
85 def from_json(cls, json):
86 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -050087 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -040088
Joe Gregoriod84d6b82012-02-28 14:53:00 -050089 def _refresh(self, http_request):
90 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -040091
Joe Gregoriod84d6b82012-02-28 14:53:00 -050092 Since the underlying App Engine app_identity implementation does its own
93 caching we can skip all the storage hoops and just to a refresh using the
94 API.
JacobMoshenko8e905102011-06-20 09:53:10 -040095
Joe Gregoriod84d6b82012-02-28 14:53:00 -050096 Args:
97 http_request: callable, a callable that matches the method signature of
98 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -040099
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500100 Raises:
101 AccessTokenRefreshError: When the refresh fails.
102 """
103 try:
104 (token, _) = app_identity.get_access_token(self.scope)
105 except app_identity.Error, e:
106 raise AccessTokenRefreshError(str(e))
107 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400108
109
Joe Gregorio695fdc12011-01-16 16:46:55 -0500110class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500111 """App Engine datastore Property for Flow.
112
113 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500114 oauth2client.Flow"""
115
116 # Tell what the user type is.
117 data_type = Flow
118
119 # For writing to datastore.
120 def get_value_for_datastore(self, model_instance):
121 flow = super(FlowProperty,
122 self).get_value_for_datastore(model_instance)
123 return db.Blob(pickle.dumps(flow))
124
125 # For reading from datastore.
126 def make_value_from_datastore(self, value):
127 if value is None:
128 return None
129 return pickle.loads(value)
130
131 def validate(self, value):
132 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400133 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500134 'to a FlowThreeLegged instance (%s)' %
135 (self.name, value))
136 return super(FlowProperty, self).validate(value)
137
138 def empty(self, value):
139 return not value
140
141
142class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500143 """App Engine datastore Property for Credentials.
144
145 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500146 oath2client.Credentials
147 """
148
149 # Tell what the user type is.
150 data_type = Credentials
151
152 # For writing to datastore.
153 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400154 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500155 cred = super(CredentialsProperty,
156 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400157 if cred is None:
158 cred = ''
159 else:
160 cred = cred.to_json()
161 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500162
163 # For reading from datastore.
164 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400165 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500166 if value is None:
167 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400168 if len(value) == 0:
169 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400170 try:
171 credentials = Credentials.new_from_json(value)
172 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400173 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400174 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500175
176 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400177 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400178 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500179 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400180 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400181 'to a Credentials instance (%s)' %
182 (self.name, value))
183 #if value is not None and not isinstance(value, Credentials):
184 # return None
185 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500186
187
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500188class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500189 """Store and retrieve a single credential to and from
190 the App Engine datastore.
191
192 This Storage helper presumes the Credentials
193 have been stored as a CredenialsProperty
194 on a datastore model class, and that entities
195 are stored by key_name.
196 """
197
Joe Gregorio432f17e2011-05-22 23:18:00 -0400198 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500199 """Constructor for Storage.
200
201 Args:
202 model: db.Model, model class
203 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400204 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400205 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500206 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500207 self._model = model
208 self._key_name = key_name
209 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400210 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500211
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400212 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500213 """Retrieve Credential from datastore.
214
215 Returns:
216 oauth2client.Credentials
217 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400218 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400219 json = self._cache.get(self._key_name)
220 if json:
221 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500222
223 credential = None
224 entity = self._model.get_by_key_name(self._key_name)
225 if entity is not None:
226 credential = getattr(entity, self._property_name)
227 if credential and hasattr(credential, 'set_store'):
228 credential.set_store(self)
229 if self._cache:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400230 self._cache.set(self._key_name, credential.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400231
Joe Gregorio695fdc12011-01-16 16:46:55 -0500232 return credential
233
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400234 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500235 """Write a Credentials to the datastore.
236
237 Args:
238 credentials: Credentials, the credentials to store.
239 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500240 entity = self._model.get_or_insert(self._key_name)
241 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500242 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400243 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400244 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400245
Joe Gregorioec75dc12012-02-06 13:40:42 -0500246 def locked_delete(self):
247 """Delete Credential from datastore."""
248
249 if self._cache:
250 self._cache.delete(self._key_name)
251
252 entity = self._model.get_by_key_name(self._key_name)
253 if entity is not None:
254 entity.delete()
255
Joe Gregorio432f17e2011-05-22 23:18:00 -0400256
257class CredentialsModel(db.Model):
258 """Storage for OAuth 2.0 Credentials
259
260 Storage of the model is keyed by the user.user_id().
261 """
262 credentials = CredentialsProperty()
263
264
265class OAuth2Decorator(object):
266 """Utility for making OAuth 2.0 easier.
267
268 Instantiate and then use with oauth_required or oauth_aware
269 as decorators on webapp.RequestHandler methods.
270
271 Example:
272
273 decorator = OAuth2Decorator(
274 client_id='837...ent.com',
275 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500276 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400277
278
279 class MainHandler(webapp.RequestHandler):
280
281 @decorator.oauth_required
282 def get(self):
283 http = decorator.http()
284 # http is authorized with the user's Credentials and can be used
285 # in API calls
286
287 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400288
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400289 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400290 auth_uri='https://accounts.google.com/o/oauth2/auth',
Joe Gregoriof08a4982011-10-07 13:11:16 -0400291 token_uri='https://accounts.google.com/o/oauth2/token',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100292 user_agent=None,
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500293 message=None, **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400294
295 """Constructor for OAuth2Decorator
296
297 Args:
298 client_id: string, client identifier.
299 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400300 scope: string or list of strings, scope(s) of the credentials being
301 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400302 auth_uri: string, URI for authorization endpoint. For convenience
303 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
304 token_uri: string, URI for token endpoint. For convenience
305 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100306 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400307 message: Message to display if there are problems with the OAuth 2.0
308 configuration. The message may contain HTML and will be presented on the
309 web interface for any method that uses the decorator.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500310 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
311 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400312 """
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100313 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500314 auth_uri, token_uri, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400315 self.credentials = None
316 self._request_handler = None
Joe Gregoriof08a4982011-10-07 13:11:16 -0400317 self._message = message
318 self._in_error = False
319
320 def _display_error_message(self, request_handler):
321 request_handler.response.out.write('<html><body>')
322 request_handler.response.out.write(self._message)
323 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400324
325 def oauth_required(self, method):
326 """Decorator that starts the OAuth 2.0 dance.
327
328 Starts the OAuth dance for the logged in user if they haven't already
329 granted access for this application.
330
331 Args:
332 method: callable, to be decorated method of a webapp.RequestHandler
333 instance.
334 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400335
Joe Gregorio17774972012-03-01 11:11:59 -0500336 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400337 if self._in_error:
338 self._display_error_message(request_handler)
339 return
340
Joe Gregoriof427c532011-06-13 09:35:26 -0400341 user = users.get_current_user()
342 # Don't use @login_decorator as this could be used in a POST request.
343 if not user:
344 request_handler.redirect(users.create_login_url(
345 request_handler.request.uri))
346 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400347 # Store the request URI in 'state' so we can use it later
348 self.flow.params['state'] = request_handler.request.url
349 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400350 self.credentials = StorageByKeyName(
351 CredentialsModel, user.user_id(), 'credentials').get()
352
353 if not self.has_credentials():
354 return request_handler.redirect(self.authorize_url())
355 try:
Joe Gregorio17774972012-03-01 11:11:59 -0500356 method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400357 except AccessTokenRefreshError:
358 return request_handler.redirect(self.authorize_url())
359
360 return check_oauth
361
362 def oauth_aware(self, method):
363 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
364
365 Does all the setup for the OAuth dance, but doesn't initiate it.
366 This decorator is useful if you want to create a page that knows
367 whether or not the user has granted access to this application.
368 From within a method decorated with @oauth_aware the has_credentials()
369 and authorize_url() methods can be called.
370
371 Args:
372 method: callable, to be decorated method of a webapp.RequestHandler
373 instance.
374 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400375
Joe Gregorio17774972012-03-01 11:11:59 -0500376 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400377 if self._in_error:
378 self._display_error_message(request_handler)
379 return
380
Joe Gregoriof427c532011-06-13 09:35:26 -0400381 user = users.get_current_user()
382 # Don't use @login_decorator as this could be used in a POST request.
383 if not user:
384 request_handler.redirect(users.create_login_url(
385 request_handler.request.uri))
386 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400387
388
Joe Gregorio432f17e2011-05-22 23:18:00 -0400389 self.flow.params['state'] = request_handler.request.url
390 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400391 self.credentials = StorageByKeyName(
392 CredentialsModel, user.user_id(), 'credentials').get()
Joe Gregorio17774972012-03-01 11:11:59 -0500393 method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400394 return setup_oauth
395
396 def has_credentials(self):
397 """True if for the logged in user there are valid access Credentials.
398
399 Must only be called from with a webapp.RequestHandler subclassed method
400 that had been decorated with either @oauth_required or @oauth_aware.
401 """
402 return self.credentials is not None and not self.credentials.invalid
403
404 def authorize_url(self):
405 """Returns the URL to start the OAuth dance.
406
407 Must only be called from with a webapp.RequestHandler subclassed method
408 that had been decorated with either @oauth_required or @oauth_aware.
409 """
410 callback = self._request_handler.request.relative_url('/oauth2callback')
411 url = self.flow.step1_get_authorize_url(callback)
412 user = users.get_current_user()
413 memcache.set(user.user_id(), pickle.dumps(self.flow),
414 namespace=OAUTH2CLIENT_NAMESPACE)
Joe Gregorio853bcf32012-03-02 15:30:23 -0500415 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400416
417 def http(self):
418 """Returns an authorized http instance.
419
420 Must only be called from within an @oauth_required decorated method, or
421 from within an @oauth_aware decorated method where has_credentials()
422 returns True.
423 """
424 return self.credentials.authorize(httplib2.Http())
425
426
Joe Gregoriof08a4982011-10-07 13:11:16 -0400427class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
428 """An OAuth2Decorator that builds from a clientsecrets file.
429
430 Uses a clientsecrets file as the source for all the information when
431 constructing an OAuth2Decorator.
432
433 Example:
434
435 decorator = OAuth2DecoratorFromClientSecrets(
436 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500437 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400438
439
440 class MainHandler(webapp.RequestHandler):
441
442 @decorator.oauth_required
443 def get(self):
444 http = decorator.http()
445 # http is authorized with the user's Credentials and can be used
446 # in API calls
447 """
448
449 def __init__(self, filename, scope, message=None):
450 """Constructor
451
452 Args:
453 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400454 scope: string or list of strings, scope(s) of the credentials being
455 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400456 message: string, A friendly string to display to the user if the
457 clientsecrets file is missing or invalid. The message may contain HTML and
458 will be presented on the web interface for any method that uses the
459 decorator.
460 """
461 try:
462 client_type, client_info = clientsecrets.loadfile(filename)
463 if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
464 raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
465 super(OAuth2DecoratorFromClientSecrets,
466 self).__init__(
467 client_info['client_id'],
468 client_info['client_secret'],
469 scope,
470 client_info['auth_uri'],
471 client_info['token_uri'],
472 message)
473 except clientsecrets.InvalidClientSecretsError:
474 self._in_error = True
475 if message is not None:
476 self._message = message
477 else:
478 self._message = "Please configure your application for OAuth 2.0"
479
480
481def oauth2decorator_from_clientsecrets(filename, scope, message=None):
482 """Creates an OAuth2Decorator populated from a clientsecrets file.
483
484 Args:
485 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400486 scope: string or list of strings, scope(s) of the credentials being
487 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400488 message: string, A friendly string to display to the user if the
489 clientsecrets file is missing or invalid. The message may contain HTML and
490 will be presented on the web interface for any method that uses the
491 decorator.
492
493 Returns: An OAuth2Decorator
494
495 """
496 return OAuth2DecoratorFromClientSecrets(filename, scope, message)
497
498
Joe Gregorio432f17e2011-05-22 23:18:00 -0400499class OAuth2Handler(webapp.RequestHandler):
500 """Handler for the redirect_uri of the OAuth 2.0 dance."""
501
502 @login_required
503 def get(self):
504 error = self.request.get('error')
505 if error:
506 errormsg = self.request.get('error_description', error)
JacobMoshenko8e905102011-06-20 09:53:10 -0400507 self.response.out.write(
508 'The authorization request failed: %s' % errormsg)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400509 else:
510 user = users.get_current_user()
JacobMoshenko8e905102011-06-20 09:53:10 -0400511 flow = pickle.loads(memcache.get(user.user_id(),
512 namespace=OAUTH2CLIENT_NAMESPACE))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400513 # This code should be ammended with application specific error
514 # handling. The following cases should be considered:
515 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
516 # 2. What if the step2_exchange fails?
517 if flow:
518 credentials = flow.step2_exchange(self.request.params)
519 StorageByKeyName(
520 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorioa07cb9c2011-12-07 10:51:45 -0500521 self.redirect(str(self.request.get('state')))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400522 else:
523 # TODO Add error handling here.
524 pass
525
526
527application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
528
JacobMoshenko8e905102011-06-20 09:53:10 -0400529
Joe Gregorio432f17e2011-05-22 23:18:00 -0400530def main():
531 run_wsgi_app(application)