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 threading
29 import time
30
31 from google.appengine.api import app_identity
32 from google.appengine.api import memcache
33 from google.appengine.api import users
34 from google.appengine.ext import db
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 GOOGLE_AUTH_URI
39 from oauth2client import GOOGLE_REVOKE_URI
40 from oauth2client import GOOGLE_TOKEN_URI
41 from oauth2client import clientsecrets
42 from oauth2client import util
43 from oauth2client import xsrfutil
44 from oauth2client.anyjson import simplejson
45 from oauth2client.client import AccessTokenRefreshError
46 from oauth2client.client import AssertionCredentials
47 from oauth2client.client import Credentials
48 from oauth2client.client import Flow
49 from oauth2client.client import OAuth2WebServerFlow
50 from oauth2client.client import Storage
51
52
53
54 try:
55 from google.appengine.ext import ndb
56 except ImportError:
57 ndb = None
58
59
60 logger = logging.getLogger(__name__)
61
62 OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
63
64 XSRF_MEMCACHE_ID = 'xsrf_secret_key'
68 """Escape text to make it safe to display.
69
70 Args:
71 s: string, The text to escape.
72
73 Returns:
74 The escaped text as a string.
75 """
76 return cgi.escape(s, quote=1).replace("'", ''')
77
80 """The client_secrets.json file is malformed or missing required fields."""
81
84 """The XSRF token is invalid or expired."""
85
88 """Storage for the sites XSRF secret key.
89
90 There will only be one instance stored of this model, the one used for the
91 site.
92 """
93 secret = db.StringProperty()
94
95 if ndb is not None:
97 """NDB Model for storage for the sites XSRF secret key.
98
99 Since this model uses the same kind as SiteXsrfSecretKey, it can be used
100 interchangeably. This simply provides an NDB model for interacting with the
101 same data the DB model interacts with.
102
103 There should only be one instance stored of this model, the one used for the
104 site.
105 """
106 secret = ndb.StringProperty()
107
108 @classmethod
110 """Return the kind name for this class."""
111 return 'SiteXsrfSecretKey'
112
115 """Returns a random XSRF secret key.
116 """
117 return os.urandom(16).encode("hex")
118
140
143 """Credentials object for App Engine Assertion Grants
144
145 This object will allow an App Engine application to identify itself to Google
146 and other OAuth 2.0 servers that can verify assertions. It can be used for the
147 purpose of accessing data stored under an account assigned to the App Engine
148 application itself.
149
150 This credential does not require a flow to instantiate because it represents
151 a two legged flow, and therefore has all of the required information to
152 generate and refresh its own access tokens.
153 """
154
155 @util.positional(2)
157 """Constructor for AppAssertionCredentials
158
159 Args:
160 scope: string or iterable of strings, scope(s) of the credentials being
161 requested.
162 """
163 self.scope = util.scopes_to_string(scope)
164
165
166 super(AppAssertionCredentials, self).__init__(None)
167
168 @classmethod
172
174 """Refreshes the access_token.
175
176 Since the underlying App Engine app_identity implementation does its own
177 caching we can skip all the storage hoops and just to a refresh using the
178 API.
179
180 Args:
181 http_request: callable, a callable that matches the method signature of
182 httplib2.Http.request, used to make the refresh request.
183
184 Raises:
185 AccessTokenRefreshError: When the refresh fails.
186 """
187 try:
188 scopes = self.scope.split()
189 (token, _) = app_identity.get_access_token(scopes)
190 except app_identity.Error, e:
191 raise AccessTokenRefreshError(str(e))
192 self.access_token = token
193
196 """App Engine datastore Property for Flow.
197
198 Utility property that allows easy storage and retrieval of an
199 oauth2client.Flow"""
200
201
202 data_type = Flow
203
204
209
210
212 if value is None:
213 return None
214 return pickle.loads(value)
215
217 if value is not None and not isinstance(value, Flow):
218 raise db.BadValueError('Property %s must be convertible '
219 'to a FlowThreeLegged instance (%s)' %
220 (self.name, value))
221 return super(FlowProperty, self).validate(value)
222
225
226
227 if ndb is not None:
229 """App Engine NDB datastore Property for Flow.
230
231 Serves the same purpose as the DB FlowProperty, but for NDB models. Since
232 PickleProperty inherits from BlobProperty, the underlying representation of
233 the data in the datastore will be the same as in the DB case.
234
235 Utility property that allows easy storage and retrieval of an
236 oauth2client.Flow
237 """
238
240 """Validates a value as a proper Flow object.
241
242 Args:
243 value: A value to be set on the property.
244
245 Raises:
246 TypeError if the value is not an instance of Flow.
247 """
248 logger.info('validate: Got type %s', type(value))
249 if value is not None and not isinstance(value, Flow):
250 raise TypeError('Property %s must be convertible to a flow '
251 'instance; received: %s.' % (self._name, value))
252
255 """App Engine datastore Property for Credentials.
256
257 Utility property that allows easy storage and retrieval of
258 oath2client.Credentials
259 """
260
261
262 data_type = Credentials
263
264
274
275
287
289 value = super(CredentialsProperty, self).validate(value)
290 logger.info("validate: Got type " + str(type(value)))
291 if value is not None and not isinstance(value, Credentials):
292 raise db.BadValueError('Property %s must be convertible '
293 'to a Credentials instance (%s)' %
294 (self.name, value))
295
296
297 return value
298
299
300 if ndb is not None:
305 """App Engine NDB datastore Property for Credentials.
306
307 Serves the same purpose as the DB CredentialsProperty, but for NDB models.
308 Since CredentialsProperty stores data as a blob and this inherits from
309 BlobProperty, the data in the datastore will be the same as in the DB case.
310
311 Utility property that allows easy storage and retrieval of Credentials and
312 subclasses.
313 """
315 """Validates a value as a proper credentials object.
316
317 Args:
318 value: A value to be set on the property.
319
320 Raises:
321 TypeError if the value is not an instance of Credentials.
322 """
323 logger.info('validate: Got type %s', type(value))
324 if value is not None and not isinstance(value, Credentials):
325 raise TypeError('Property %s must be convertible to a credentials '
326 'instance; received: %s.' % (self._name, value))
327
329 """Converts our validated value to a JSON serialized string.
330
331 Args:
332 value: A value to be set in the datastore.
333
334 Returns:
335 A JSON serialized version of the credential, else '' if value is None.
336 """
337 if value is None:
338 return ''
339 else:
340 return value.to_json()
341
343 """Converts our stored JSON string back to the desired type.
344
345 Args:
346 value: A value from the datastore to be converted to the desired type.
347
348 Returns:
349 A deserialized Credentials (or subclass) object, else None if the
350 value can't be parsed.
351 """
352 if not value:
353 return None
354 try:
355
356 credentials = Credentials.new_from_json(value)
357 except ValueError:
358 credentials = None
359 return credentials
360
363 """Store and retrieve a credential to and from the App Engine datastore.
364
365 This Storage helper presumes the Credentials have been stored as a
366 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
367 that entities are stored by key_name.
368 """
369
370 @util.positional(4)
371 - def __init__(self, model, key_name, property_name, cache=None, user=None):
372 """Constructor for Storage.
373
374 Args:
375 model: db.Model or ndb.Model, model class
376 key_name: string, key name for the entity that has the credentials
377 property_name: string, name of the property that is a CredentialsProperty
378 or CredentialsNDBProperty.
379 cache: memcache, a write-through cache to put in front of the datastore.
380 If the model you are using is an NDB model, using a cache will be
381 redundant since the model uses an instance cache and memcache for you.
382 user: users.User object, optional. Can be used to grab user ID as a
383 key_name if no key name is specified.
384 """
385 if key_name is None:
386 if user is None:
387 raise ValueError('StorageByKeyName called with no key name or user.')
388 key_name = user.user_id()
389
390 self._model = model
391 self._key_name = key_name
392 self._property_name = property_name
393 self._cache = cache
394
396 """Determine whether the model of the instance is an NDB model.
397
398 Returns:
399 Boolean indicating whether or not the model is an NDB or DB model.
400 """
401
402
403 if isinstance(self._model, type):
404 if ndb is not None and issubclass(self._model, ndb.Model):
405 return True
406 elif issubclass(self._model, db.Model):
407 return False
408
409 raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
410
412 """Retrieve entity from datastore.
413
414 Uses a different model method for db or ndb models.
415
416 Returns:
417 Instance of the model corresponding to the current storage object
418 and stored using the key name of the storage object.
419 """
420 if self._is_ndb():
421 return self._model.get_by_id(self._key_name)
422 else:
423 return self._model.get_by_key_name(self._key_name)
424
426 """Delete entity from datastore.
427
428 Attempts to delete using the key_name stored on the object, whether or not
429 the given key is in the datastore.
430 """
431 if self._is_ndb():
432 ndb.Key(self._model, self._key_name).delete()
433 else:
434 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
435 db.delete(entity_key)
436
458
460 """Write a Credentials to the datastore.
461
462 Args:
463 credentials: Credentials, the credentials to store.
464 """
465 entity = self._model.get_or_insert(self._key_name)
466 setattr(entity, self._property_name, credentials)
467 entity.put()
468 if self._cache:
469 self._cache.set(self._key_name, credentials.to_json())
470
472 """Delete Credential from datastore."""
473
474 if self._cache:
475 self._cache.delete(self._key_name)
476
477 self._delete_entity()
478
486
487
488 if ndb is not None:
490 """NDB Model for storage of OAuth 2.0 Credentials
491
492 Since this model uses the same kind as CredentialsModel and has a property
493 which can serialize and deserialize Credentials correctly, it can be used
494 interchangeably with a CredentialsModel to access, insert and delete the
495 same entities. This simply provides an NDB model for interacting with the
496 same data the DB model interacts with.
497
498 Storage of the model is keyed by the user.user_id().
499 """
500 credentials = CredentialsNDBProperty()
501
502 @classmethod
504 """Return the kind name for this class."""
505 return 'CredentialsModel'
506
509 """Composes the value for the 'state' parameter.
510
511 Packs the current request URI and an XSRF token into an opaque string that
512 can be passed to the authentication server via the 'state' parameter.
513
514 Args:
515 request_handler: webapp.RequestHandler, The request.
516 user: google.appengine.api.users.User, The current user.
517
518 Returns:
519 The state value as a string.
520 """
521 uri = request_handler.request.url
522 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
523 action_id=str(uri))
524 return uri + ':' + token
525
528 """Parse the value of the 'state' parameter.
529
530 Parses the value and validates the XSRF token in the state parameter.
531
532 Args:
533 state: string, The value of the state parameter.
534 user: google.appengine.api.users.User, The current user.
535
536 Raises:
537 InvalidXsrfTokenError: if the XSRF token is invalid.
538
539 Returns:
540 The redirect URI.
541 """
542 uri, token = state.rsplit(':', 1)
543 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
544 action_id=uri):
545 raise InvalidXsrfTokenError()
546
547 return uri
548
551 """Utility for making OAuth 2.0 easier.
552
553 Instantiate and then use with oauth_required or oauth_aware
554 as decorators on webapp.RequestHandler methods.
555
556 Example:
557
558 decorator = OAuth2Decorator(
559 client_id='837...ent.com',
560 client_secret='Qh...wwI',
561 scope='https://www.googleapis.com/auth/plus')
562
563
564 class MainHandler(webapp.RequestHandler):
565
566 @decorator.oauth_required
567 def get(self):
568 http = decorator.http()
569 # http is authorized with the user's Credentials and can be used
570 # in API calls
571
572 """
573
576
578 """A thread local Credentials object.
579
580 Returns:
581 A client.Credentials object, or None if credentials hasn't been set in
582 this thread yet, which may happen when calling has_credentials inside
583 oauth_aware.
584 """
585 return getattr(self._tls, 'credentials', None)
586
587 credentials = property(get_credentials, set_credentials)
588
591
593 """A thread local Flow object.
594
595 Returns:
596 A credentials.Flow object, or None if the flow hasn't been set in this
597 thread yet, which happens in _create_flow() since Flows are created
598 lazily.
599 """
600 return getattr(self._tls, 'flow', None)
601
602 flow = property(get_flow, set_flow)
603
604
605 @util.positional(4)
606 - def __init__(self, client_id, client_secret, scope,
607 auth_uri=GOOGLE_AUTH_URI,
608 token_uri=GOOGLE_TOKEN_URI,
609 revoke_uri=GOOGLE_REVOKE_URI,
610 user_agent=None,
611 message=None,
612 callback_path='/oauth2callback',
613 token_response_param=None,
614 _storage_class=StorageByKeyName,
615 _credentials_class=CredentialsModel,
616 _credentials_property_name='credentials',
617 **kwargs):
618
619 """Constructor for OAuth2Decorator
620
621 Args:
622 client_id: string, client identifier.
623 client_secret: string client secret.
624 scope: string or iterable of strings, scope(s) of the credentials being
625 requested.
626 auth_uri: string, URI for authorization endpoint. For convenience
627 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
628 token_uri: string, URI for token endpoint. For convenience
629 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
630 revoke_uri: string, URI for revoke endpoint. For convenience
631 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
632 user_agent: string, User agent of your application, default to None.
633 message: Message to display if there are problems with the OAuth 2.0
634 configuration. The message may contain HTML and will be presented on the
635 web interface for any method that uses the decorator.
636 callback_path: string, The absolute path to use as the callback URI. Note
637 that this must match up with the URI given when registering the
638 application in the APIs Console.
639 token_response_param: string. If provided, the full JSON response
640 to the access token request will be encoded and included in this query
641 parameter in the callback URI. This is useful with providers (e.g.
642 wordpress.com) that include extra fields that the client may want.
643 _storage_class: "Protected" keyword argument not typically provided to
644 this constructor. A storage class to aid in storing a Credentials object
645 for a user in the datastore. Defaults to StorageByKeyName.
646 _credentials_class: "Protected" keyword argument not typically provided to
647 this constructor. A db or ndb Model class to hold credentials. Defaults
648 to CredentialsModel.
649 _credentials_property_name: "Protected" keyword argument not typically
650 provided to this constructor. A string indicating the name of the field
651 on the _credentials_class where a Credentials object will be stored.
652 Defaults to 'credentials'.
653 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
654 OAuth2WebServerFlow constructor.
655 """
656 self._tls = threading.local()
657 self.flow = None
658 self.credentials = None
659 self._client_id = client_id
660 self._client_secret = client_secret
661 self._scope = util.scopes_to_string(scope)
662 self._auth_uri = auth_uri
663 self._token_uri = token_uri
664 self._revoke_uri = revoke_uri
665 self._user_agent = user_agent
666 self._kwargs = kwargs
667 self._message = message
668 self._in_error = False
669 self._callback_path = callback_path
670 self._token_response_param = token_response_param
671 self._storage_class = _storage_class
672 self._credentials_class = _credentials_class
673 self._credentials_property_name = _credentials_property_name
674
676 request_handler.response.out.write('<html><body>')
677 request_handler.response.out.write(_safe_html(self._message))
678 request_handler.response.out.write('</body></html>')
679
681 """Decorator that starts the OAuth 2.0 dance.
682
683 Starts the OAuth dance for the logged in user if they haven't already
684 granted access for this application.
685
686 Args:
687 method: callable, to be decorated method of a webapp.RequestHandler
688 instance.
689 """
690
691 def check_oauth(request_handler, *args, **kwargs):
692 if self._in_error:
693 self._display_error_message(request_handler)
694 return
695
696 user = users.get_current_user()
697
698 if not user:
699 request_handler.redirect(users.create_login_url(
700 request_handler.request.uri))
701 return
702
703 self._create_flow(request_handler)
704
705
706 self.flow.params['state'] = _build_state_value(request_handler, user)
707 self.credentials = self._storage_class(
708 self._credentials_class, None,
709 self._credentials_property_name, user=user).get()
710
711 if not self.has_credentials():
712 return request_handler.redirect(self.authorize_url())
713 try:
714 resp = method(request_handler, *args, **kwargs)
715 except AccessTokenRefreshError:
716 return request_handler.redirect(self.authorize_url())
717 finally:
718 self.credentials = None
719 return resp
720
721 return check_oauth
722
724 """Create the Flow object.
725
726 The Flow is calculated lazily since we don't know where this app is
727 running until it receives a request, at which point redirect_uri can be
728 calculated and then the Flow object can be constructed.
729
730 Args:
731 request_handler: webapp.RequestHandler, the request handler.
732 """
733 if self.flow is None:
734 redirect_uri = request_handler.request.relative_url(
735 self._callback_path)
736 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
737 self._scope, redirect_uri=redirect_uri,
738 user_agent=self._user_agent,
739 auth_uri=self._auth_uri,
740 token_uri=self._token_uri,
741 revoke_uri=self._revoke_uri,
742 **self._kwargs)
743
745 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
746
747 Does all the setup for the OAuth dance, but doesn't initiate it.
748 This decorator is useful if you want to create a page that knows
749 whether or not the user has granted access to this application.
750 From within a method decorated with @oauth_aware the has_credentials()
751 and authorize_url() methods can be called.
752
753 Args:
754 method: callable, to be decorated method of a webapp.RequestHandler
755 instance.
756 """
757
758 def setup_oauth(request_handler, *args, **kwargs):
759 if self._in_error:
760 self._display_error_message(request_handler)
761 return
762
763 user = users.get_current_user()
764
765 if not user:
766 request_handler.redirect(users.create_login_url(
767 request_handler.request.uri))
768 return
769
770 self._create_flow(request_handler)
771
772 self.flow.params['state'] = _build_state_value(request_handler, user)
773 self.credentials = self._storage_class(
774 self._credentials_class, None,
775 self._credentials_property_name, user=user).get()
776 try:
777 resp = method(request_handler, *args, **kwargs)
778 finally:
779 self.credentials = None
780 return resp
781 return setup_oauth
782
783
785 """True if for the logged in user there are valid access Credentials.
786
787 Must only be called from with a webapp.RequestHandler subclassed method
788 that had been decorated with either @oauth_required or @oauth_aware.
789 """
790 return self.credentials is not None and not self.credentials.invalid
791
793 """Returns the URL to start the OAuth dance.
794
795 Must only be called from with a webapp.RequestHandler subclassed method
796 that had been decorated with either @oauth_required or @oauth_aware.
797 """
798 url = self.flow.step1_get_authorize_url()
799 return str(url)
800
802 """Returns an authorized http instance.
803
804 Must only be called from within an @oauth_required decorated method, or
805 from within an @oauth_aware decorated method where has_credentials()
806 returns True.
807 """
808 return self.credentials.authorize(httplib2.Http())
809
810 @property
812 """The absolute path where the callback will occur.
813
814 Note this is the absolute path, not the absolute URI, that will be
815 calculated by the decorator at runtime. See callback_handler() for how this
816 should be used.
817
818 Returns:
819 The callback path as a string.
820 """
821 return self._callback_path
822
823
825 """RequestHandler for the OAuth 2.0 redirect callback.
826
827 Usage:
828 app = webapp.WSGIApplication([
829 ('/index', MyIndexHandler),
830 ...,
831 (decorator.callback_path, decorator.callback_handler())
832 ])
833
834 Returns:
835 A webapp.RequestHandler that handles the redirect back from the
836 server during the OAuth 2.0 dance.
837 """
838 decorator = self
839
840 class OAuth2Handler(webapp.RequestHandler):
841 """Handler for the redirect_uri of the OAuth 2.0 dance."""
842
843 @login_required
844 def get(self):
845 error = self.request.get('error')
846 if error:
847 errormsg = self.request.get('error_description', error)
848 self.response.out.write(
849 'The authorization request failed: %s' % _safe_html(errormsg))
850 else:
851 user = users.get_current_user()
852 decorator._create_flow(self)
853 credentials = decorator.flow.step2_exchange(self.request.params)
854 decorator._storage_class(
855 decorator._credentials_class, None,
856 decorator._credentials_property_name, user=user).put(credentials)
857 redirect_uri = _parse_state_value(str(self.request.get('state')),
858 user)
859
860 if decorator._token_response_param and credentials.token_response:
861 resp_json = simplejson.dumps(credentials.token_response)
862 redirect_uri = util._add_query_parameter(
863 redirect_uri, decorator._token_response_param, resp_json)
864
865 self.redirect(redirect_uri)
866
867 return OAuth2Handler
868
870 """WSGI application for handling the OAuth 2.0 redirect callback.
871
872 If you need finer grained control use `callback_handler` which returns just
873 the webapp.RequestHandler.
874
875 Returns:
876 A webapp.WSGIApplication that handles the redirect back from the
877 server during the OAuth 2.0 dance.
878 """
879 return webapp.WSGIApplication([
880 (self.callback_path, self.callback_handler())
881 ])
882
885 """An OAuth2Decorator that builds from a clientsecrets file.
886
887 Uses a clientsecrets file as the source for all the information when
888 constructing an OAuth2Decorator.
889
890 Example:
891
892 decorator = OAuth2DecoratorFromClientSecrets(
893 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
894 scope='https://www.googleapis.com/auth/plus')
895
896
897 class MainHandler(webapp.RequestHandler):
898
899 @decorator.oauth_required
900 def get(self):
901 http = decorator.http()
902 # http is authorized with the user's Credentials and can be used
903 # in API calls
904 """
905
906 @util.positional(3)
907 - def __init__(self, filename, scope, message=None, cache=None):
908 """Constructor
909
910 Args:
911 filename: string, File name of client secrets.
912 scope: string or iterable of strings, scope(s) of the credentials being
913 requested.
914 message: string, A friendly string to display to the user if the
915 clientsecrets file is missing or invalid. The message may contain HTML
916 and will be presented on the web interface for any method that uses the
917 decorator.
918 cache: An optional cache service client that implements get() and set()
919 methods. See clientsecrets.loadfile() for details.
920 """
921 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
922 if client_type not in [
923 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
924 raise InvalidClientSecretsError(
925 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
926 constructor_kwargs = {
927 'auth_uri': client_info['auth_uri'],
928 'token_uri': client_info['token_uri'],
929 'message': message,
930 }
931 revoke_uri = client_info.get('revoke_uri')
932 if revoke_uri is not None:
933 constructor_kwargs['revoke_uri'] = revoke_uri
934 super(OAuth2DecoratorFromClientSecrets, self).__init__(
935 client_info['client_id'], client_info['client_secret'],
936 scope, **constructor_kwargs)
937 if message is not None:
938 self._message = message
939 else:
940 self._message = 'Please configure your application for OAuth 2.0.'
941
946 """Creates an OAuth2Decorator populated from a clientsecrets file.
947
948 Args:
949 filename: string, File name of client secrets.
950 scope: string or list of strings, scope(s) of the credentials being
951 requested.
952 message: string, A friendly string to display to the user if the
953 clientsecrets file is missing or invalid. The message may contain HTML and
954 will be presented on the web interface for any method that uses the
955 decorator.
956 cache: An optional cache service client that implements get() and set()
957 methods. See clientsecrets.loadfile() for details.
958
959 Returns: An OAuth2Decorator
960
961 """
962 return OAuth2DecoratorFromClientSecrets(filename, scope,
963 message=message, cache=cache)
964