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