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 os
26 import pickle
27 import time
28
29 from google.appengine.api import app_identity
30 from google.appengine.api import memcache
31 from google.appengine.api import users
32 from google.appengine.ext import db
33 from google.appengine.ext import webapp
34 from google.appengine.ext.webapp.util import login_required
35 from google.appengine.ext.webapp.util import run_wsgi_app
36 from oauth2client import clientsecrets
37 from oauth2client import util
38 from oauth2client import xsrfutil
39 from oauth2client.anyjson import simplejson
40 from oauth2client.client import AccessTokenRefreshError
41 from oauth2client.client import AssertionCredentials
42 from oauth2client.client import Credentials
43 from oauth2client.client import Flow
44 from oauth2client.client import OAuth2WebServerFlow
45 from oauth2client.client import Storage
46
47 logger = logging.getLogger(__name__)
48
49 OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
50
51 XSRF_MEMCACHE_ID = 'xsrf_secret_key'
55 """The client_secrets.json file is malformed or missing required fields."""
56
59 """The XSRF token is invalid or expired."""
60
63 """Storage for the sites XSRF secret key.
64
65 There will only be one instance stored of this model, the one used for the
66 site. """
67 secret = db.StringProperty()
68
71 """Returns a random XSRF secret key.
72 """
73 return os.urandom(16).encode("hex")
74
96
99 """Credentials object for App Engine Assertion Grants
100
101 This object will allow an App Engine application to identify itself to Google
102 and other OAuth 2.0 servers that can verify assertions. It can be used for the
103 purpose of accessing data stored under an account assigned to the App Engine
104 application itself.
105
106 This credential does not require a flow to instantiate because it represents
107 a two legged flow, and therefore has all of the required information to
108 generate and refresh its own access tokens.
109 """
110
111 @util.positional(2)
113 """Constructor for AppAssertionCredentials
114
115 Args:
116 scope: string or list of strings, scope(s) of the credentials being
117 requested.
118 """
119 if type(scope) is list:
120 scope = ' '.join(scope)
121 self.scope = scope
122
123 super(AppAssertionCredentials, self).__init__(
124 'ignored'
125 )
126
127 @classmethod
131
133 """Refreshes the access_token.
134
135 Since the underlying App Engine app_identity implementation does its own
136 caching we can skip all the storage hoops and just to a refresh using the
137 API.
138
139 Args:
140 http_request: callable, a callable that matches the method signature of
141 httplib2.Http.request, used to make the refresh request.
142
143 Raises:
144 AccessTokenRefreshError: When the refresh fails.
145 """
146 try:
147 (token, _) = app_identity.get_access_token(self.scope)
148 except app_identity.Error, e:
149 raise AccessTokenRefreshError(str(e))
150 self.access_token = token
151
154 """App Engine datastore Property for Flow.
155
156 Utility property that allows easy storage and retreival of an
157 oauth2client.Flow"""
158
159
160 data_type = Flow
161
162
167
168
170 if value is None:
171 return None
172 return pickle.loads(value)
173
175 if value is not None and not isinstance(value, Flow):
176 raise db.BadValueError('Property %s must be convertible '
177 'to a FlowThreeLegged instance (%s)' %
178 (self.name, value))
179 return super(FlowProperty, self).validate(value)
180
183
186 """App Engine datastore Property for Credentials.
187
188 Utility property that allows easy storage and retrieval of
189 oath2client.Credentials
190 """
191
192
193 data_type = Credentials
194
195
205
206
218
220 value = super(CredentialsProperty, self).validate(value)
221 logger.info("validate: Got type " + str(type(value)))
222 if value is not None and not isinstance(value, Credentials):
223 raise db.BadValueError('Property %s must be convertible '
224 'to a Credentials instance (%s)' %
225 (self.name, value))
226
227
228 return value
229
232 """Store and retrieve a single credential to and from
233 the App Engine datastore.
234
235 This Storage helper presumes the Credentials
236 have been stored as a CredenialsProperty
237 on a datastore model class, and that entities
238 are stored by key_name.
239 """
240
241 @util.positional(4)
242 - def __init__(self, model, key_name, property_name, cache=None):
243 """Constructor for Storage.
244
245 Args:
246 model: db.Model, model class
247 key_name: string, key name for the entity that has the credentials
248 property_name: string, name of the property that is a CredentialsProperty
249 cache: memcache, a write-through cache to put in front of the datastore
250 """
251 self._model = model
252 self._key_name = key_name
253 self._property_name = property_name
254 self._cache = cache
255
257 """Retrieve Credential from datastore.
258
259 Returns:
260 oauth2client.Credentials
261 """
262 if self._cache:
263 json = self._cache.get(self._key_name)
264 if json:
265 return Credentials.new_from_json(json)
266
267 credential = None
268 entity = self._model.get_by_key_name(self._key_name)
269 if entity is not None:
270 credential = getattr(entity, self._property_name)
271 if credential and hasattr(credential, 'set_store'):
272 credential.set_store(self)
273 if self._cache:
274 self._cache.set(self._key_name, credential.to_json())
275
276 return credential
277
279 """Write a Credentials to the datastore.
280
281 Args:
282 credentials: Credentials, the credentials to store.
283 """
284 entity = self._model.get_or_insert(self._key_name)
285 setattr(entity, self._property_name, credentials)
286 entity.put()
287 if self._cache:
288 self._cache.set(self._key_name, credentials.to_json())
289
291 """Delete Credential from datastore."""
292
293 if self._cache:
294 self._cache.delete(self._key_name)
295
296 entity = self._model.get_by_key_name(self._key_name)
297 if entity is not None:
298 entity.delete()
299
307
310 """Composes the value for the 'state' parameter.
311
312 Packs the current request URI and an XSRF token into an opaque string that
313 can be passed to the authentication server via the 'state' parameter.
314
315 Args:
316 request_handler: webapp.RequestHandler, The request.
317 user: google.appengine.api.users.User, The current user.
318
319 Returns:
320 The state value as a string.
321 """
322 uri = request_handler.request.url
323 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
324 action_id=str(uri))
325 return uri + ':' + token
326
329 """Parse the value of the 'state' parameter.
330
331 Parses the value and validates the XSRF token in the state parameter.
332
333 Args:
334 state: string, The value of the state parameter.
335 user: google.appengine.api.users.User, The current user.
336
337 Raises:
338 InvalidXsrfTokenError: if the XSRF token is invalid.
339
340 Returns:
341 The redirect URI.
342 """
343 uri, token = state.rsplit(':', 1)
344 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
345 action_id=uri):
346 raise InvalidXsrfTokenError()
347
348 return uri
349
352 """Utility for making OAuth 2.0 easier.
353
354 Instantiate and then use with oauth_required or oauth_aware
355 as decorators on webapp.RequestHandler methods.
356
357 Example:
358
359 decorator = OAuth2Decorator(
360 client_id='837...ent.com',
361 client_secret='Qh...wwI',
362 scope='https://www.googleapis.com/auth/plus')
363
364
365 class MainHandler(webapp.RequestHandler):
366
367 @decorator.oauth_required
368 def get(self):
369 http = decorator.http()
370 # http is authorized with the user's Credentials and can be used
371 # in API calls
372
373 """
374
375 @util.positional(4)
376 - def __init__(self, client_id, client_secret, scope,
377 auth_uri='https://accounts.google.com/o/oauth2/auth',
378 token_uri='https://accounts.google.com/o/oauth2/token',
379 user_agent=None,
380 message=None,
381 callback_path='/oauth2callback',
382 **kwargs):
383
384 """Constructor for OAuth2Decorator
385
386 Args:
387 client_id: string, client identifier.
388 client_secret: string client secret.
389 scope: string or list of strings, scope(s) of the credentials being
390 requested.
391 auth_uri: string, URI for authorization endpoint. For convenience
392 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
393 token_uri: string, URI for token endpoint. For convenience
394 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
395 user_agent: string, User agent of your application, default to None.
396 message: Message to display if there are problems with the OAuth 2.0
397 configuration. The message may contain HTML and will be presented on the
398 web interface for any method that uses the decorator.
399 callback_path: string, The absolute path to use as the callback URI. Note
400 that this must match up with the URI given when registering the
401 application in the APIs Console.
402 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
403 OAuth2WebServerFlow constructor.
404 """
405 self.flow = None
406 self.credentials = None
407 self._client_id = client_id
408 self._client_secret = client_secret
409 self._scope = scope
410 self._auth_uri = auth_uri
411 self._token_uri = token_uri
412 self._user_agent = user_agent
413 self._kwargs = kwargs
414 self._message = message
415 self._in_error = False
416 self._callback_path = callback_path
417
419 request_handler.response.out.write('<html><body>')
420 request_handler.response.out.write(self._message)
421 request_handler.response.out.write('</body></html>')
422
424 """Decorator that starts the OAuth 2.0 dance.
425
426 Starts the OAuth dance for the logged in user if they haven't already
427 granted access for this application.
428
429 Args:
430 method: callable, to be decorated method of a webapp.RequestHandler
431 instance.
432 """
433
434 def check_oauth(request_handler, *args, **kwargs):
435 if self._in_error:
436 self._display_error_message(request_handler)
437 return
438
439 user = users.get_current_user()
440
441 if not user:
442 request_handler.redirect(users.create_login_url(
443 request_handler.request.uri))
444 return
445
446 self._create_flow(request_handler)
447
448
449 self.flow.params['state'] = _build_state_value(request_handler, user)
450 self.credentials = StorageByKeyName(
451 CredentialsModel, user.user_id(), 'credentials').get()
452
453 if not self.has_credentials():
454 return request_handler.redirect(self.authorize_url())
455 try:
456 return method(request_handler, *args, **kwargs)
457 except AccessTokenRefreshError:
458 return request_handler.redirect(self.authorize_url())
459
460 return check_oauth
461
463 """Create the Flow object.
464
465 The Flow is calculated lazily since we don't know where this app is
466 running until it receives a request, at which point redirect_uri can be
467 calculated and then the Flow object can be constructed.
468
469 Args:
470 request_handler: webapp.RequestHandler, the request handler.
471 """
472 if self.flow is None:
473 redirect_uri = request_handler.request.relative_url(
474 self._callback_path)
475 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
476 self._scope, redirect_uri=redirect_uri,
477 user_agent=self._user_agent,
478 auth_uri=self._auth_uri,
479 token_uri=self._token_uri, **self._kwargs)
480
481
483 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
484
485 Does all the setup for the OAuth dance, but doesn't initiate it.
486 This decorator is useful if you want to create a page that knows
487 whether or not the user has granted access to this application.
488 From within a method decorated with @oauth_aware the has_credentials()
489 and authorize_url() methods can be called.
490
491 Args:
492 method: callable, to be decorated method of a webapp.RequestHandler
493 instance.
494 """
495
496 def setup_oauth(request_handler, *args, **kwargs):
497 if self._in_error:
498 self._display_error_message(request_handler)
499 return
500
501 user = users.get_current_user()
502
503 if not user:
504 request_handler.redirect(users.create_login_url(
505 request_handler.request.uri))
506 return
507
508 self._create_flow(request_handler)
509
510 self.flow.params['state'] = _build_state_value(request_handler, user)
511 self.credentials = StorageByKeyName(
512 CredentialsModel, user.user_id(), 'credentials').get()
513 return method(request_handler, *args, **kwargs)
514 return setup_oauth
515
517 """True if for the logged in user there are valid access Credentials.
518
519 Must only be called from with a webapp.RequestHandler subclassed method
520 that had been decorated with either @oauth_required or @oauth_aware.
521 """
522 return self.credentials is not None and not self.credentials.invalid
523
525 """Returns the URL to start the OAuth dance.
526
527 Must only be called from with a webapp.RequestHandler subclassed method
528 that had been decorated with either @oauth_required or @oauth_aware.
529 """
530 url = self.flow.step1_get_authorize_url()
531 return str(url)
532
534 """Returns an authorized http instance.
535
536 Must only be called from within an @oauth_required decorated method, or
537 from within an @oauth_aware decorated method where has_credentials()
538 returns True.
539 """
540 return self.credentials.authorize(httplib2.Http())
541
542 @property
544 """The absolute path where the callback will occur.
545
546 Note this is the absolute path, not the absolute URI, that will be
547 calculated by the decorator at runtime. See callback_handler() for how this
548 should be used.
549
550 Returns:
551 The callback path as a string.
552 """
553 return self._callback_path
554
555
557 """RequestHandler for the OAuth 2.0 redirect callback.
558
559 Usage:
560 app = webapp.WSGIApplication([
561 ('/index', MyIndexHandler),
562 ...,
563 (decorator.callback_path, decorator.callback_handler())
564 ])
565
566 Returns:
567 A webapp.RequestHandler that handles the redirect back from the
568 server during the OAuth 2.0 dance.
569 """
570 decorator = self
571
572 class OAuth2Handler(webapp.RequestHandler):
573 """Handler for the redirect_uri of the OAuth 2.0 dance."""
574
575 @login_required
576 def get(self):
577 error = self.request.get('error')
578 if error:
579 errormsg = self.request.get('error_description', error)
580 self.response.out.write(
581 'The authorization request failed: %s' % errormsg)
582 else:
583 user = users.get_current_user()
584 decorator._create_flow(self)
585 credentials = decorator.flow.step2_exchange(self.request.params)
586 StorageByKeyName(
587 CredentialsModel, user.user_id(), 'credentials').put(credentials)
588 redirect_uri = _parse_state_value(str(self.request.get('state')),
589 user)
590 self.redirect(redirect_uri)
591
592 return OAuth2Handler
593
595 """WSGI application for handling the OAuth 2.0 redirect callback.
596
597 If you need finer grained control use `callback_handler` which returns just
598 the webapp.RequestHandler.
599
600 Returns:
601 A webapp.WSGIApplication that handles the redirect back from the
602 server during the OAuth 2.0 dance.
603 """
604 return webapp.WSGIApplication([
605 (self.callback_path, self.callback_handler())
606 ])
607
610 """An OAuth2Decorator that builds from a clientsecrets file.
611
612 Uses a clientsecrets file as the source for all the information when
613 constructing an OAuth2Decorator.
614
615 Example:
616
617 decorator = OAuth2DecoratorFromClientSecrets(
618 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
619 scope='https://www.googleapis.com/auth/plus')
620
621
622 class MainHandler(webapp.RequestHandler):
623
624 @decorator.oauth_required
625 def get(self):
626 http = decorator.http()
627 # http is authorized with the user's Credentials and can be used
628 # in API calls
629 """
630
631 @util.positional(3)
632 - def __init__(self, filename, scope, message=None, cache=None):
633 """Constructor
634
635 Args:
636 filename: string, File name of client secrets.
637 scope: string or list of strings, scope(s) of the credentials being
638 requested.
639 message: string, A friendly string to display to the user if the
640 clientsecrets file is missing or invalid. The message may contain HTML
641 and will be presented on the web interface for any method that uses the
642 decorator.
643 cache: An optional cache service client that implements get() and set()
644 methods. See clientsecrets.loadfile() for details.
645 """
646 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
647 if client_type not in [
648 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
649 raise InvalidClientSecretsError(
650 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
651 super(OAuth2DecoratorFromClientSecrets, self).__init__(
652 client_info['client_id'],
653 client_info['client_secret'],
654 scope,
655 auth_uri=client_info['auth_uri'],
656 token_uri=client_info['token_uri'],
657 message=message)
658 if message is not None:
659 self._message = message
660 else:
661 self._message = "Please configure your application for OAuth 2.0"
662
667 """Creates an OAuth2Decorator populated from a clientsecrets file.
668
669 Args:
670 filename: string, File name of client secrets.
671 scope: string or list of strings, scope(s) of the credentials being
672 requested.
673 message: string, A friendly string to display to the user if the
674 clientsecrets file is missing or invalid. The message may contain HTML and
675 will be presented on the web interface for any method that uses the
676 decorator.
677 cache: An optional cache service client that implements get() and set()
678 methods. See clientsecrets.loadfile() for details.
679
680 Returns: An OAuth2Decorator
681
682 """
683 return OAuth2DecoratorFromClientSecrets(filename, scope,
684 message=message, cache=cache)
685