1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 """Utilities for Google App Engine
16
17 Utilities for making it easier to use OAuth 2.0 on Google App Engine.
18 """
19
20 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
22 import base64
23 import httplib2
24 import logging
25 import pickle
26 import time
27
28 import clientsecrets
29
30 from anyjson import simplejson
31 from client import AccessTokenRefreshError
32 from client import AssertionCredentials
33 from client import Credentials
34 from client import Flow
35 from client import OAuth2WebServerFlow
36 from client import Storage
37 from google.appengine.api import memcache
38 from google.appengine.api import users
39 from google.appengine.api import app_identity
40 from google.appengine.ext import db
41 from google.appengine.ext import webapp
42 from google.appengine.ext.webapp.util import login_required
43 from google.appengine.ext.webapp.util import run_wsgi_app
44
45
46 logger = logging.getLogger(__name__)
47
48 OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
52 """The client_secrets.json file is malformed or missing required fields."""
53 pass
54
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
62 Engine application itself.
63
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.
67 """
68
70 """Constructor for AppAssertionCredentials
71
72 Args:
73 scope: string or list of strings, scope(s) of the credentials being requested.
74 """
75 if type(scope) is list:
76 scope = ' '.join(scope)
77 self.scope = scope
78
79 super(AppAssertionCredentials, self).__init__(
80 None,
81 None,
82 None)
83
84 @classmethod
88
90 """Refreshes the access_token.
91
92 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.
95
96 Args:
97 http_request: callable, a callable that matches the method signature of
98 httplib2.Http.request, used to make the refresh request.
99
100 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
108
111 """App Engine datastore Property for Flow.
112
113 Utility property that allows easy storage and retreival of an
114 oauth2client.Flow"""
115
116
117 data_type = Flow
118
119
124
125
127 if value is None:
128 return None
129 return pickle.loads(value)
130
132 if value is not None and not isinstance(value, Flow):
133 raise db.BadValueError('Property %s must be convertible '
134 'to a FlowThreeLegged instance (%s)' %
135 (self.name, value))
136 return super(FlowProperty, self).validate(value)
137
140
143 """App Engine datastore Property for Credentials.
144
145 Utility property that allows easy storage and retrieval of
146 oath2client.Credentials
147 """
148
149
150 data_type = Credentials
151
152
162
163
175
177 value = super(CredentialsProperty, self).validate(value)
178 logger.info("validate: Got type " + str(type(value)))
179 if value is not None and not isinstance(value, Credentials):
180 raise db.BadValueError('Property %s must be convertible '
181 'to a Credentials instance (%s)' %
182 (self.name, value))
183
184
185 return value
186
189 """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
198 - def __init__(self, model, key_name, property_name, cache=None):
199 """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
204 property_name: string, name of the property that is a CredentialsProperty
205 cache: memcache, a write-through cache to put in front of the datastore
206 """
207 self._model = model
208 self._key_name = key_name
209 self._property_name = property_name
210 self._cache = cache
211
213 """Retrieve Credential from datastore.
214
215 Returns:
216 oauth2client.Credentials
217 """
218 if self._cache:
219 json = self._cache.get(self._key_name)
220 if json:
221 return Credentials.new_from_json(json)
222
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:
230 self._cache.set(self._key_name, credential.to_json())
231
232 return credential
233
235 """Write a Credentials to the datastore.
236
237 Args:
238 credentials: Credentials, the credentials to store.
239 """
240 entity = self._model.get_or_insert(self._key_name)
241 setattr(entity, self._property_name, credentials)
242 entity.put()
243 if self._cache:
244 self._cache.set(self._key_name, credentials.to_json())
245
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
263
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',
276 scope='https://www.googleapis.com/auth/plus')
277
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 """
288
289 - def __init__(self, client_id, client_secret, scope,
290 auth_uri='https://accounts.google.com/o/oauth2/auth',
291 token_uri='https://accounts.google.com/o/oauth2/token',
292 user_agent=None,
293 message=None, **kwargs):
294
295 """Constructor for OAuth2Decorator
296
297 Args:
298 client_id: string, client identifier.
299 client_secret: string client secret.
300 scope: string or list of strings, scope(s) of the credentials being
301 requested.
302 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.
306 user_agent: string, User agent of your application, default to None.
307 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.
310 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
311 OAuth2WebServerFlow constructor.
312 """
313 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
314 auth_uri, token_uri, **kwargs)
315 self.credentials = None
316 self._request_handler = None
317 self._message = message
318 self._in_error = False
319
321 request_handler.response.out.write('<html><body>')
322 request_handler.response.out.write(self._message)
323 request_handler.response.out.write('</body></html>')
324
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 """
335
336 def check_oauth(request_handler, *args, **kwargs):
337 if self._in_error:
338 self._display_error_message(request_handler)
339 return
340
341 user = users.get_current_user()
342
343 if not user:
344 request_handler.redirect(users.create_login_url(
345 request_handler.request.uri))
346 return
347
348 self.flow.params['state'] = request_handler.request.url
349 self._request_handler = request_handler
350 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:
356 method(request_handler, *args, **kwargs)
357 except AccessTokenRefreshError:
358 return request_handler.redirect(self.authorize_url())
359
360 return check_oauth
361
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 """
375
376 def setup_oauth(request_handler, *args, **kwargs):
377 if self._in_error:
378 self._display_error_message(request_handler)
379 return
380
381 user = users.get_current_user()
382
383 if not user:
384 request_handler.redirect(users.create_login_url(
385 request_handler.request.uri))
386 return
387
388
389 self.flow.params['state'] = request_handler.request.url
390 self._request_handler = request_handler
391 self.credentials = StorageByKeyName(
392 CredentialsModel, user.user_id(), 'credentials').get()
393 method(request_handler, *args, **kwargs)
394 return setup_oauth
395
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
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)
415 return str(url)
416
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
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')
437 scope='https://www.googleapis.com/auth/plus')
438
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, cache=None):
450 """Constructor
451
452 Args:
453 filename: string, File name of client secrets.
454 scope: string or list of strings, scope(s) of the credentials being
455 requested.
456 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 cache: An optional cache service client that implements get() and set()
461 methods. See clientsecrets.loadfile() for details.
462 """
463 try:
464 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
465 if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
466 raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
467 super(OAuth2DecoratorFromClientSecrets,
468 self).__init__(
469 client_info['client_id'],
470 client_info['client_secret'],
471 scope,
472 client_info['auth_uri'],
473 client_info['token_uri'],
474 message)
475 except clientsecrets.InvalidClientSecretsError:
476 self._in_error = True
477 if message is not None:
478 self._message = message
479 else:
480 self._message = "Please configure your application for OAuth 2.0"
481
485 """Creates an OAuth2Decorator populated from a clientsecrets file.
486
487 Args:
488 filename: string, File name of client secrets.
489 scope: string or list of strings, scope(s) of the credentials being
490 requested.
491 message: string, A friendly string to display to the user if the
492 clientsecrets file is missing or invalid. The message may contain HTML and
493 will be presented on the web interface for any method that uses the
494 decorator.
495 cache: An optional cache service client that implements get() and set()
496 methods. See clientsecrets.loadfile() for details.
497
498 Returns: An OAuth2Decorator
499
500 """
501 return OAuth2DecoratorFromClientSecrets(filename, scope,
502 message=message, cache=cache)
503
506 """Handler for the redirect_uri of the OAuth 2.0 dance."""
507
508 @login_required
531
532
533 application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
537 run_wsgi_app(application)
538