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 cgi
24 import httplib2
25 import logging
26 import os
27 import pickle
28 import time
29
30 from google.appengine.api import app_identity
31 from google.appengine.api import memcache
32 from google.appengine.api import users
33 from google.appengine.ext import db
34 from google.appengine.ext import ndb
35 from google.appengine.ext import webapp
36 from google.appengine.ext.webapp.util import login_required
37 from google.appengine.ext.webapp.util import run_wsgi_app
38 from oauth2client import clientsecrets
39 from oauth2client import util
40 from oauth2client import xsrfutil
41 from oauth2client.anyjson import simplejson
42 from oauth2client.client import AccessTokenRefreshError
43 from oauth2client.client import AssertionCredentials
44 from oauth2client.client import Credentials
45 from oauth2client.client import Flow
46 from oauth2client.client import OAuth2WebServerFlow
47 from oauth2client.client import Storage
48
49 logger = logging.getLogger(__name__)
50
51 OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
52
53 XSRF_MEMCACHE_ID = 'xsrf_secret_key'
57 """Escape text to make it safe to display.
58
59 Args:
60 s: string, The text to escape.
61
62 Returns:
63 The escaped text as a string.
64 """
65 return cgi.escape(s, quote=1).replace("'", ''')
66
69 """The client_secrets.json file is malformed or missing required fields."""
70
73 """The XSRF token is invalid or expired."""
74
77 """Storage for the sites XSRF secret key.
78
79 There will only be one instance stored of this model, the one used for the
80 site.
81 """
82 secret = db.StringProperty()
83
86 """NDB Model for storage for the sites XSRF secret key.
87
88 Since this model uses the same kind as SiteXsrfSecretKey, it can be used
89 interchangeably. This simply provides an NDB model for interacting with the
90 same data the DB model interacts with.
91
92 There should only be one instance stored of this model, the one used for the
93 site.
94 """
95 secret = ndb.StringProperty()
96
97 @classmethod
99 """Return the kind name for this class."""
100 return 'SiteXsrfSecretKey'
101
104 """Returns a random XSRF secret key.
105 """
106 return os.urandom(16).encode("hex")
107
129
132 """Credentials object for App Engine Assertion Grants
133
134 This object will allow an App Engine application to identify itself to Google
135 and other OAuth 2.0 servers that can verify assertions. It can be used for the
136 purpose of accessing data stored under an account assigned to the App Engine
137 application itself.
138
139 This credential does not require a flow to instantiate because it represents
140 a two legged flow, and therefore has all of the required information to
141 generate and refresh its own access tokens.
142 """
143
144 @util.positional(2)
146 """Constructor for AppAssertionCredentials
147
148 Args:
149 scope: string or iterable of strings, scope(s) of the credentials being
150 requested.
151 """
152 self.scope = util.scopes_to_string(scope)
153
154 super(AppAssertionCredentials, self).__init__(
155 'ignored'
156 )
157
158 @classmethod
162
164 """Refreshes the access_token.
165
166 Since the underlying App Engine app_identity implementation does its own
167 caching we can skip all the storage hoops and just to a refresh using the
168 API.
169
170 Args:
171 http_request: callable, a callable that matches the method signature of
172 httplib2.Http.request, used to make the refresh request.
173
174 Raises:
175 AccessTokenRefreshError: When the refresh fails.
176 """
177 try:
178 scopes = self.scope.split()
179 (token, _) = app_identity.get_access_token(scopes)
180 except app_identity.Error, e:
181 raise AccessTokenRefreshError(str(e))
182 self.access_token = token
183
186 """App Engine datastore Property for Flow.
187
188 Utility property that allows easy storage and retrieval of an
189 oauth2client.Flow"""
190
191
192 data_type = Flow
193
194
199
200
202 if value is None:
203 return None
204 return pickle.loads(value)
205
207 if value is not None and not isinstance(value, Flow):
208 raise db.BadValueError('Property %s must be convertible '
209 'to a FlowThreeLegged instance (%s)' %
210 (self.name, value))
211 return super(FlowProperty, self).validate(value)
212
215
218 """App Engine NDB datastore Property for Flow.
219
220 Serves the same purpose as the DB FlowProperty, but for NDB models. Since
221 PickleProperty inherits from BlobProperty, the underlying representation of
222 the data in the datastore will be the same as in the DB case.
223
224 Utility property that allows easy storage and retrieval of an
225 oauth2client.Flow
226 """
227
229 """Validates a value as a proper Flow object.
230
231 Args:
232 value: A value to be set on the property.
233
234 Raises:
235 TypeError if the value is not an instance of Flow.
236 """
237 logger.info('validate: Got type %s', type(value))
238 if value is not None and not isinstance(value, Flow):
239 raise TypeError('Property %s must be convertible to a flow '
240 'instance; received: %s.' % (self._name, value))
241
244 """App Engine datastore Property for Credentials.
245
246 Utility property that allows easy storage and retrieval of
247 oath2client.Credentials
248 """
249
250
251 data_type = Credentials
252
253
263
264
276
278 value = super(CredentialsProperty, self).validate(value)
279 logger.info("validate: Got type " + str(type(value)))
280 if value is not None and not isinstance(value, Credentials):
281 raise db.BadValueError('Property %s must be convertible '
282 'to a Credentials instance (%s)' %
283 (self.name, value))
284
285
286 return value
287
293 """App Engine NDB datastore Property for Credentials.
294
295 Serves the same purpose as the DB CredentialsProperty, but for NDB models.
296 Since CredentialsProperty stores data as a blob and this inherits from
297 BlobProperty, the data in the datastore will be the same as in the DB case.
298
299 Utility property that allows easy storage and retrieval of Credentials and
300 subclasses.
301 """
303 """Validates a value as a proper credentials object.
304
305 Args:
306 value: A value to be set on the property.
307
308 Raises:
309 TypeError if the value is not an instance of Credentials.
310 """
311 logger.info('validate: Got type %s', type(value))
312 if value is not None and not isinstance(value, Credentials):
313 raise TypeError('Property %s must be convertible to a credentials '
314 'instance; received: %s.' % (self._name, value))
315
317 """Converts our validated value to a JSON serialized string.
318
319 Args:
320 value: A value to be set in the datastore.
321
322 Returns:
323 A JSON serialized version of the credential, else '' if value is None.
324 """
325 if value is None:
326 return ''
327 else:
328 return value.to_json()
329
331 """Converts our stored JSON string back to the desired type.
332
333 Args:
334 value: A value from the datastore to be converted to the desired type.
335
336 Returns:
337 A deserialized Credentials (or subclass) object, else None if the
338 value can't be parsed.
339 """
340 if not value:
341 return None
342 try:
343
344 credentials = Credentials.new_from_json(value)
345 except ValueError:
346 credentials = None
347 return credentials
348
351 """Store and retrieve a credential to and from the App Engine datastore.
352
353 This Storage helper presumes the Credentials have been stored as a
354 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
355 that entities are stored by key_name.
356 """
357
358 @util.positional(4)
359 - def __init__(self, model, key_name, property_name, cache=None):
360 """Constructor for Storage.
361
362 Args:
363 model: db.Model or ndb.Model, model class
364 key_name: string, key name for the entity that has the credentials
365 property_name: string, name of the property that is a CredentialsProperty
366 or CredentialsNDBProperty.
367 cache: memcache, a write-through cache to put in front of the datastore.
368 If the model you are using is an NDB model, using a cache will be
369 redundant since the model uses an instance cache and memcache for you.
370 """
371 self._model = model
372 self._key_name = key_name
373 self._property_name = property_name
374 self._cache = cache
375
377 """Determine whether the model of the instance is an NDB model.
378
379 Returns:
380 Boolean indicating whether or not the model is an NDB or DB model.
381 """
382
383
384 if isinstance(self._model, type):
385 if issubclass(self._model, ndb.Model):
386 return True
387 elif issubclass(self._model, db.Model):
388 return False
389
390 raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
391
393 """Retrieve entity from datastore.
394
395 Uses a different model method for db or ndb models.
396
397 Returns:
398 Instance of the model corresponding to the current storage object
399 and stored using the key name of the storage object.
400 """
401 if self._is_ndb():
402 return self._model.get_by_id(self._key_name)
403 else:
404 return self._model.get_by_key_name(self._key_name)
405
407 """Delete entity from datastore.
408
409 Attempts to delete using the key_name stored on the object, whether or not
410 the given key is in the datastore.
411 """
412 if self._is_ndb():
413 ndb.Key(self._model, self._key_name).delete()
414 else:
415 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
416 db.delete(entity_key)
417
439
441 """Write a Credentials to the datastore.
442
443 Args:
444 credentials: Credentials, the credentials to store.
445 """
446 entity = self._model.get_or_insert(self._key_name)
447 setattr(entity, self._property_name, credentials)
448 entity.put()
449 if self._cache:
450 self._cache.set(self._key_name, credentials.to_json())
451
453 """Delete Credential from datastore."""
454
455 if self._cache:
456 self._cache.delete(self._key_name)
457
458 self._delete_entity()
459
467
470 """NDB Model for storage of OAuth 2.0 Credentials
471
472 Since this model uses the same kind as CredentialsModel and has a property
473 which can serialize and deserialize Credentials correctly, it can be used
474 interchangeably with a CredentialsModel to access, insert and delete the same
475 entities. This simply provides an NDB model for interacting with the
476 same data the DB model interacts with.
477
478 Storage of the model is keyed by the user.user_id().
479 """
480 credentials = CredentialsNDBProperty()
481
482 @classmethod
484 """Return the kind name for this class."""
485 return 'CredentialsModel'
486
489 """Composes the value for the 'state' parameter.
490
491 Packs the current request URI and an XSRF token into an opaque string that
492 can be passed to the authentication server via the 'state' parameter.
493
494 Args:
495 request_handler: webapp.RequestHandler, The request.
496 user: google.appengine.api.users.User, The current user.
497
498 Returns:
499 The state value as a string.
500 """
501 uri = request_handler.request.url
502 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
503 action_id=str(uri))
504 return uri + ':' + token
505
508 """Parse the value of the 'state' parameter.
509
510 Parses the value and validates the XSRF token in the state parameter.
511
512 Args:
513 state: string, The value of the state parameter.
514 user: google.appengine.api.users.User, The current user.
515
516 Raises:
517 InvalidXsrfTokenError: if the XSRF token is invalid.
518
519 Returns:
520 The redirect URI.
521 """
522 uri, token = state.rsplit(':', 1)
523 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
524 action_id=uri):
525 raise InvalidXsrfTokenError()
526
527 return uri
528
531 """Utility for making OAuth 2.0 easier.
532
533 Instantiate and then use with oauth_required or oauth_aware
534 as decorators on webapp.RequestHandler methods.
535
536 Example:
537
538 decorator = OAuth2Decorator(
539 client_id='837...ent.com',
540 client_secret='Qh...wwI',
541 scope='https://www.googleapis.com/auth/plus')
542
543
544 class MainHandler(webapp.RequestHandler):
545
546 @decorator.oauth_required
547 def get(self):
548 http = decorator.http()
549 # http is authorized with the user's Credentials and can be used
550 # in API calls
551
552 """
553
554 @util.positional(4)
555 - def __init__(self, client_id, client_secret, scope,
556 auth_uri='https://accounts.google.com/o/oauth2/auth',
557 token_uri='https://accounts.google.com/o/oauth2/token',
558 user_agent=None,
559 message=None,
560 callback_path='/oauth2callback',
561 **kwargs):
562
563 """Constructor for OAuth2Decorator
564
565 Args:
566 client_id: string, client identifier.
567 client_secret: string client secret.
568 scope: string or iterable of strings, scope(s) of the credentials being
569 requested.
570 auth_uri: string, URI for authorization endpoint. For convenience
571 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
572 token_uri: string, URI for token endpoint. For convenience
573 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
574 user_agent: string, User agent of your application, default to None.
575 message: Message to display if there are problems with the OAuth 2.0
576 configuration. The message may contain HTML and will be presented on the
577 web interface for any method that uses the decorator.
578 callback_path: string, The absolute path to use as the callback URI. Note
579 that this must match up with the URI given when registering the
580 application in the APIs Console.
581 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
582 OAuth2WebServerFlow constructor.
583 """
584 self.flow = None
585 self.credentials = None
586 self._client_id = client_id
587 self._client_secret = client_secret
588 self._scope = util.scopes_to_string(scope)
589 self._auth_uri = auth_uri
590 self._token_uri = token_uri
591 self._user_agent = user_agent
592 self._kwargs = kwargs
593 self._message = message
594 self._in_error = False
595 self._callback_path = callback_path
596
601
603 """Decorator that starts the OAuth 2.0 dance.
604
605 Starts the OAuth dance for the logged in user if they haven't already
606 granted access for this application.
607
608 Args:
609 method: callable, to be decorated method of a webapp.RequestHandler
610 instance.
611 """
612
613 def check_oauth(request_handler, *args, **kwargs):
614 if self._in_error:
615 self._display_error_message(request_handler)
616 return
617
618 user = users.get_current_user()
619
620 if not user:
621 request_handler.redirect(users.create_login_url(
622 request_handler.request.uri))
623 return
624
625 self._create_flow(request_handler)
626
627
628 self.flow.params['state'] = _build_state_value(request_handler, user)
629 self.credentials = StorageByKeyName(
630 CredentialsModel, user.user_id(), 'credentials').get()
631
632 if not self.has_credentials():
633 return request_handler.redirect(self.authorize_url())
634 try:
635 return method(request_handler, *args, **kwargs)
636 except AccessTokenRefreshError:
637 return request_handler.redirect(self.authorize_url())
638
639 return check_oauth
640
642 """Create the Flow object.
643
644 The Flow is calculated lazily since we don't know where this app is
645 running until it receives a request, at which point redirect_uri can be
646 calculated and then the Flow object can be constructed.
647
648 Args:
649 request_handler: webapp.RequestHandler, the request handler.
650 """
651 if self.flow is None:
652 redirect_uri = request_handler.request.relative_url(
653 self._callback_path)
654 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
655 self._scope, redirect_uri=redirect_uri,
656 user_agent=self._user_agent,
657 auth_uri=self._auth_uri,
658 token_uri=self._token_uri, **self._kwargs)
659
660
662 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
663
664 Does all the setup for the OAuth dance, but doesn't initiate it.
665 This decorator is useful if you want to create a page that knows
666 whether or not the user has granted access to this application.
667 From within a method decorated with @oauth_aware the has_credentials()
668 and authorize_url() methods can be called.
669
670 Args:
671 method: callable, to be decorated method of a webapp.RequestHandler
672 instance.
673 """
674
675 def setup_oauth(request_handler, *args, **kwargs):
676 if self._in_error:
677 self._display_error_message(request_handler)
678 return
679
680 user = users.get_current_user()
681
682 if not user:
683 request_handler.redirect(users.create_login_url(
684 request_handler.request.uri))
685 return
686
687 self._create_flow(request_handler)
688
689 self.flow.params['state'] = _build_state_value(request_handler, user)
690 self.credentials = StorageByKeyName(
691 CredentialsModel, user.user_id(), 'credentials').get()
692 return method(request_handler, *args, **kwargs)
693 return setup_oauth
694
696 """True if for the logged in user there are valid access Credentials.
697
698 Must only be called from with a webapp.RequestHandler subclassed method
699 that had been decorated with either @oauth_required or @oauth_aware.
700 """
701 return self.credentials is not None and not self.credentials.invalid
702
704 """Returns the URL to start the OAuth dance.
705
706 Must only be called from with a webapp.RequestHandler subclassed method
707 that had been decorated with either @oauth_required or @oauth_aware.
708 """
709 url = self.flow.step1_get_authorize_url()
710 return str(url)
711
713 """Returns an authorized http instance.
714
715 Must only be called from within an @oauth_required decorated method, or
716 from within an @oauth_aware decorated method where has_credentials()
717 returns True.
718 """
719 return self.credentials.authorize(httplib2.Http())
720
721 @property
723 """The absolute path where the callback will occur.
724
725 Note this is the absolute path, not the absolute URI, that will be
726 calculated by the decorator at runtime. See callback_handler() for how this
727 should be used.
728
729 Returns:
730 The callback path as a string.
731 """
732 return self._callback_path
733
734
736 """RequestHandler for the OAuth 2.0 redirect callback.
737
738 Usage:
739 app = webapp.WSGIApplication([
740 ('/index', MyIndexHandler),
741 ...,
742 (decorator.callback_path, decorator.callback_handler())
743 ])
744
745 Returns:
746 A webapp.RequestHandler that handles the redirect back from the
747 server during the OAuth 2.0 dance.
748 """
749 decorator = self
750
751 class OAuth2Handler(webapp.RequestHandler):
752 """Handler for the redirect_uri of the OAuth 2.0 dance."""
753
754 @login_required
755 def get(self):
756 error = self.request.get('error')
757 if error:
758 errormsg = self.request.get('error_description', error)
759 self.response.out.write(
760 'The authorization request failed: %s' % _safe_html(errormsg))
761 else:
762 user = users.get_current_user()
763 decorator._create_flow(self)
764 credentials = decorator.flow.step2_exchange(self.request.params)
765 StorageByKeyName(
766 CredentialsModel, user.user_id(), 'credentials').put(credentials)
767 redirect_uri = _parse_state_value(str(self.request.get('state')),
768 user)
769 self.redirect(redirect_uri)
770
771 return OAuth2Handler
772
774 """WSGI application for handling the OAuth 2.0 redirect callback.
775
776 If you need finer grained control use `callback_handler` which returns just
777 the webapp.RequestHandler.
778
779 Returns:
780 A webapp.WSGIApplication that handles the redirect back from the
781 server during the OAuth 2.0 dance.
782 """
783 return webapp.WSGIApplication([
784 (self.callback_path, self.callback_handler())
785 ])
786
789 """An OAuth2Decorator that builds from a clientsecrets file.
790
791 Uses a clientsecrets file as the source for all the information when
792 constructing an OAuth2Decorator.
793
794 Example:
795
796 decorator = OAuth2DecoratorFromClientSecrets(
797 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
798 scope='https://www.googleapis.com/auth/plus')
799
800
801 class MainHandler(webapp.RequestHandler):
802
803 @decorator.oauth_required
804 def get(self):
805 http = decorator.http()
806 # http is authorized with the user's Credentials and can be used
807 # in API calls
808 """
809
810 @util.positional(3)
811 - def __init__(self, filename, scope, message=None, cache=None):
812 """Constructor
813
814 Args:
815 filename: string, File name of client secrets.
816 scope: string or iterable of strings, scope(s) of the credentials being
817 requested.
818 message: string, A friendly string to display to the user if the
819 clientsecrets file is missing or invalid. The message may contain HTML
820 and will be presented on the web interface for any method that uses the
821 decorator.
822 cache: An optional cache service client that implements get() and set()
823 methods. See clientsecrets.loadfile() for details.
824 """
825 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
826 if client_type not in [
827 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
828 raise InvalidClientSecretsError(
829 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
830 super(OAuth2DecoratorFromClientSecrets, self).__init__(
831 client_info['client_id'],
832 client_info['client_secret'],
833 scope,
834 auth_uri=client_info['auth_uri'],
835 token_uri=client_info['token_uri'],
836 message=message)
837 if message is not None:
838 self._message = message
839 else:
840 self._message = "Please configure your application for OAuth 2.0"
841
846 """Creates an OAuth2Decorator populated from a clientsecrets file.
847
848 Args:
849 filename: string, File name of client secrets.
850 scope: string or list of strings, scope(s) of the credentials being
851 requested.
852 message: string, A friendly string to display to the user if the
853 clientsecrets file is missing or invalid. The message may contain HTML and
854 will be presented on the web interface for any method that uses the
855 decorator.
856 cache: An optional cache service client that implements get() and set()
857 methods. See clientsecrets.loadfile() for details.
858
859 Returns: An OAuth2Decorator
860
861 """
862 return OAuth2DecoratorFromClientSecrets(filename, scope,
863 message=message, cache=cache)
864