• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 Google Inc. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Utilities for Google App Engine
16
17Utilities for making it easier to use OAuth 2.0 on Google App Engine.
18"""
19
20import cgi
21import json
22import logging
23import os
24import pickle
25import threading
26
27from google.appengine.api import app_identity
28from google.appengine.api import memcache
29from google.appengine.api import users
30from google.appengine.ext import db
31from google.appengine.ext.webapp.util import login_required
32import httplib2
33import webapp2 as webapp
34
35import oauth2client
36from oauth2client import client
37from oauth2client import clientsecrets
38from oauth2client import util
39from oauth2client.contrib import xsrfutil
40
41# This is a temporary fix for a Google internal issue.
42try:
43    from oauth2client.contrib import _appengine_ndb
44except ImportError:  # pragma: NO COVER
45    _appengine_ndb = None
46
47
48__author__ = 'jcgregorio@google.com (Joe Gregorio)'
49
50logger = logging.getLogger(__name__)
51
52OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
53
54XSRF_MEMCACHE_ID = 'xsrf_secret_key'
55
56if _appengine_ndb is None:  # pragma: NO COVER
57    CredentialsNDBModel = None
58    CredentialsNDBProperty = None
59    FlowNDBProperty = None
60    _NDB_KEY = None
61    _NDB_MODEL = None
62    SiteXsrfSecretKeyNDB = None
63else:
64    CredentialsNDBModel = _appengine_ndb.CredentialsNDBModel
65    CredentialsNDBProperty = _appengine_ndb.CredentialsNDBProperty
66    FlowNDBProperty = _appengine_ndb.FlowNDBProperty
67    _NDB_KEY = _appengine_ndb.NDB_KEY
68    _NDB_MODEL = _appengine_ndb.NDB_MODEL
69    SiteXsrfSecretKeyNDB = _appengine_ndb.SiteXsrfSecretKeyNDB
70
71
72def _safe_html(s):
73    """Escape text to make it safe to display.
74
75    Args:
76        s: string, The text to escape.
77
78    Returns:
79        The escaped text as a string.
80    """
81    return cgi.escape(s, quote=1).replace("'", ''')
82
83
84class SiteXsrfSecretKey(db.Model):
85    """Storage for the sites XSRF secret key.
86
87    There will only be one instance stored of this model, the one used for the
88    site.
89    """
90    secret = db.StringProperty()
91
92
93def _generate_new_xsrf_secret_key():
94    """Returns a random XSRF secret key."""
95    return os.urandom(16).encode("hex")
96
97
98def xsrf_secret_key():
99    """Return the secret key for use for XSRF protection.
100
101    If the Site entity does not have a secret key, this method will also create
102    one and persist it.
103
104    Returns:
105        The secret key.
106    """
107    secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
108    if not secret:
109        # Load the one and only instance of SiteXsrfSecretKey.
110        model = SiteXsrfSecretKey.get_or_insert(key_name='site')
111        if not model.secret:
112            model.secret = _generate_new_xsrf_secret_key()
113            model.put()
114        secret = model.secret
115        memcache.add(XSRF_MEMCACHE_ID, secret,
116                     namespace=OAUTH2CLIENT_NAMESPACE)
117
118    return str(secret)
119
120
121class AppAssertionCredentials(client.AssertionCredentials):
122    """Credentials object for App Engine Assertion Grants
123
124    This object will allow an App Engine application to identify itself to
125    Google and other OAuth 2.0 servers that can verify assertions. It can be
126    used for the purpose of accessing data stored under an account assigned to
127    the App Engine application itself.
128
129    This credential does not require a flow to instantiate because it
130    represents a two legged flow, and therefore has all of the required
131    information to generate and refresh its own access tokens.
132    """
133
134    @util.positional(2)
135    def __init__(self, scope, **kwargs):
136        """Constructor for AppAssertionCredentials
137
138        Args:
139            scope: string or iterable of strings, scope(s) of the credentials
140                   being requested.
141            **kwargs: optional keyword args, including:
142            service_account_id: service account id of the application. If None
143                                or unspecified, the default service account for
144                                the app is used.
145        """
146        self.scope = util.scopes_to_string(scope)
147        self._kwargs = kwargs
148        self.service_account_id = kwargs.get('service_account_id', None)
149        self._service_account_email = None
150
151        # Assertion type is no longer used, but still in the
152        # parent class signature.
153        super(AppAssertionCredentials, self).__init__(None)
154
155    @classmethod
156    def from_json(cls, json_data):
157        data = json.loads(json_data)
158        return AppAssertionCredentials(data['scope'])
159
160    def _refresh(self, http_request):
161        """Refreshes the access_token.
162
163        Since the underlying App Engine app_identity implementation does its
164        own caching we can skip all the storage hoops and just to a refresh
165        using the API.
166
167        Args:
168            http_request: callable, a callable that matches the method
169                          signature of httplib2.Http.request, used to make the
170                          refresh request.
171
172        Raises:
173            AccessTokenRefreshError: When the refresh fails.
174        """
175        try:
176            scopes = self.scope.split()
177            (token, _) = app_identity.get_access_token(
178                scopes, service_account_id=self.service_account_id)
179        except app_identity.Error as e:
180            raise client.AccessTokenRefreshError(str(e))
181        self.access_token = token
182
183    @property
184    def serialization_data(self):
185        raise NotImplementedError('Cannot serialize credentials '
186                                  'for Google App Engine.')
187
188    def create_scoped_required(self):
189        return not self.scope
190
191    def create_scoped(self, scopes):
192        return AppAssertionCredentials(scopes, **self._kwargs)
193
194    def sign_blob(self, blob):
195        """Cryptographically sign a blob (of bytes).
196
197        Implements abstract method
198        :meth:`oauth2client.client.AssertionCredentials.sign_blob`.
199
200        Args:
201            blob: bytes, Message to be signed.
202
203        Returns:
204            tuple, A pair of the private key ID used to sign the blob and
205            the signed contents.
206        """
207        return app_identity.sign_blob(blob)
208
209    @property
210    def service_account_email(self):
211        """Get the email for the current service account.
212
213        Returns:
214            string, The email associated with the Google App Engine
215            service account.
216        """
217        if self._service_account_email is None:
218            self._service_account_email = (
219                app_identity.get_service_account_name())
220        return self._service_account_email
221
222
223class FlowProperty(db.Property):
224    """App Engine datastore Property for Flow.
225
226    Utility property that allows easy storage and retrieval of an
227    oauth2client.Flow
228    """
229
230    # Tell what the user type is.
231    data_type = client.Flow
232
233    # For writing to datastore.
234    def get_value_for_datastore(self, model_instance):
235        flow = super(FlowProperty, self).get_value_for_datastore(
236            model_instance)
237        return db.Blob(pickle.dumps(flow))
238
239    # For reading from datastore.
240    def make_value_from_datastore(self, value):
241        if value is None:
242            return None
243        return pickle.loads(value)
244
245    def validate(self, value):
246        if value is not None and not isinstance(value, client.Flow):
247            raise db.BadValueError(
248                'Property {0} must be convertible '
249                'to a FlowThreeLegged instance ({1})'.format(self.name, value))
250        return super(FlowProperty, self).validate(value)
251
252    def empty(self, value):
253        return not value
254
255
256class CredentialsProperty(db.Property):
257    """App Engine datastore Property for Credentials.
258
259    Utility property that allows easy storage and retrieval of
260    oauth2client.Credentials
261    """
262
263    # Tell what the user type is.
264    data_type = client.Credentials
265
266    # For writing to datastore.
267    def get_value_for_datastore(self, model_instance):
268        logger.info("get: Got type " + str(type(model_instance)))
269        cred = super(CredentialsProperty, self).get_value_for_datastore(
270            model_instance)
271        if cred is None:
272            cred = ''
273        else:
274            cred = cred.to_json()
275        return db.Blob(cred)
276
277    # For reading from datastore.
278    def make_value_from_datastore(self, value):
279        logger.info("make: Got type " + str(type(value)))
280        if value is None:
281            return None
282        if len(value) == 0:
283            return None
284        try:
285            credentials = client.Credentials.new_from_json(value)
286        except ValueError:
287            credentials = None
288        return credentials
289
290    def validate(self, value):
291        value = super(CredentialsProperty, self).validate(value)
292        logger.info("validate: Got type " + str(type(value)))
293        if value is not None and not isinstance(value, client.Credentials):
294            raise db.BadValueError(
295                'Property {0} must be convertible '
296                'to a Credentials instance ({1})'.format(self.name, value))
297        return value
298
299
300class StorageByKeyName(client.Storage):
301    """Store and retrieve a credential to and from the App Engine datastore.
302
303    This Storage helper presumes the Credentials have been stored as a
304    CredentialsProperty or CredentialsNDBProperty on a datastore model class,
305    and that entities are stored by key_name.
306    """
307
308    @util.positional(4)
309    def __init__(self, model, key_name, property_name, cache=None, user=None):
310        """Constructor for Storage.
311
312        Args:
313            model: db.Model or ndb.Model, model class
314            key_name: string, key name for the entity that has the credentials
315            property_name: string, name of the property that is a
316                           CredentialsProperty or CredentialsNDBProperty.
317            cache: memcache, a write-through cache to put in front of the
318                   datastore. If the model you are using is an NDB model, using
319                   a cache will be redundant since the model uses an instance
320                   cache and memcache for you.
321            user: users.User object, optional. Can be used to grab user ID as a
322                  key_name if no key name is specified.
323        """
324        super(StorageByKeyName, self).__init__()
325
326        if key_name is None:
327            if user is None:
328                raise ValueError('StorageByKeyName called with no '
329                                 'key name or user.')
330            key_name = user.user_id()
331
332        self._model = model
333        self._key_name = key_name
334        self._property_name = property_name
335        self._cache = cache
336
337    def _is_ndb(self):
338        """Determine whether the model of the instance is an NDB model.
339
340        Returns:
341            Boolean indicating whether or not the model is an NDB or DB model.
342        """
343        # issubclass will fail if one of the arguments is not a class, only
344        # need worry about new-style classes since ndb and db models are
345        # new-style
346        if isinstance(self._model, type):
347            if _NDB_MODEL is not None and issubclass(self._model, _NDB_MODEL):
348                return True
349            elif issubclass(self._model, db.Model):
350                return False
351
352        raise TypeError(
353            'Model class not an NDB or DB model: {0}.'.format(self._model))
354
355    def _get_entity(self):
356        """Retrieve entity from datastore.
357
358        Uses a different model method for db or ndb models.
359
360        Returns:
361            Instance of the model corresponding to the current storage object
362            and stored using the key name of the storage object.
363        """
364        if self._is_ndb():
365            return self._model.get_by_id(self._key_name)
366        else:
367            return self._model.get_by_key_name(self._key_name)
368
369    def _delete_entity(self):
370        """Delete entity from datastore.
371
372        Attempts to delete using the key_name stored on the object, whether or
373        not the given key is in the datastore.
374        """
375        if self._is_ndb():
376            _NDB_KEY(self._model, self._key_name).delete()
377        else:
378            entity_key = db.Key.from_path(self._model.kind(), self._key_name)
379            db.delete(entity_key)
380
381    @db.non_transactional(allow_existing=True)
382    def locked_get(self):
383        """Retrieve Credential from datastore.
384
385        Returns:
386            oauth2client.Credentials
387        """
388        credentials = None
389        if self._cache:
390            json = self._cache.get(self._key_name)
391            if json:
392                credentials = client.Credentials.new_from_json(json)
393        if credentials is None:
394            entity = self._get_entity()
395            if entity is not None:
396                credentials = getattr(entity, self._property_name)
397                if self._cache:
398                    self._cache.set(self._key_name, credentials.to_json())
399
400        if credentials and hasattr(credentials, 'set_store'):
401            credentials.set_store(self)
402        return credentials
403
404    @db.non_transactional(allow_existing=True)
405    def locked_put(self, credentials):
406        """Write a Credentials to the datastore.
407
408        Args:
409            credentials: Credentials, the credentials to store.
410        """
411        entity = self._model.get_or_insert(self._key_name)
412        setattr(entity, self._property_name, credentials)
413        entity.put()
414        if self._cache:
415            self._cache.set(self._key_name, credentials.to_json())
416
417    @db.non_transactional(allow_existing=True)
418    def locked_delete(self):
419        """Delete Credential from datastore."""
420
421        if self._cache:
422            self._cache.delete(self._key_name)
423
424        self._delete_entity()
425
426
427class CredentialsModel(db.Model):
428    """Storage for OAuth 2.0 Credentials
429
430    Storage of the model is keyed by the user.user_id().
431    """
432    credentials = CredentialsProperty()
433
434
435def _build_state_value(request_handler, user):
436    """Composes the value for the 'state' parameter.
437
438    Packs the current request URI and an XSRF token into an opaque string that
439    can be passed to the authentication server via the 'state' parameter.
440
441    Args:
442        request_handler: webapp.RequestHandler, The request.
443        user: google.appengine.api.users.User, The current user.
444
445    Returns:
446        The state value as a string.
447    """
448    uri = request_handler.request.url
449    token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
450                                    action_id=str(uri))
451    return uri + ':' + token
452
453
454def _parse_state_value(state, user):
455    """Parse the value of the 'state' parameter.
456
457    Parses the value and validates the XSRF token in the state parameter.
458
459    Args:
460        state: string, The value of the state parameter.
461        user: google.appengine.api.users.User, The current user.
462
463    Returns:
464        The redirect URI, or None if XSRF token is not valid.
465    """
466    uri, token = state.rsplit(':', 1)
467    if xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
468                               action_id=uri):
469        return uri
470    else:
471        return None
472
473
474class OAuth2Decorator(object):
475    """Utility for making OAuth 2.0 easier.
476
477    Instantiate and then use with oauth_required or oauth_aware
478    as decorators on webapp.RequestHandler methods.
479
480    ::
481
482        decorator = OAuth2Decorator(
483            client_id='837...ent.com',
484            client_secret='Qh...wwI',
485            scope='https://www.googleapis.com/auth/plus')
486
487        class MainHandler(webapp.RequestHandler):
488            @decorator.oauth_required
489            def get(self):
490                http = decorator.http()
491                # http is authorized with the user's Credentials and can be
492                # used in API calls
493
494    """
495
496    def set_credentials(self, credentials):
497        self._tls.credentials = credentials
498
499    def get_credentials(self):
500        """A thread local Credentials object.
501
502        Returns:
503            A client.Credentials object, or None if credentials hasn't been set
504            in this thread yet, which may happen when calling has_credentials
505            inside oauth_aware.
506        """
507        return getattr(self._tls, 'credentials', None)
508
509    credentials = property(get_credentials, set_credentials)
510
511    def set_flow(self, flow):
512        self._tls.flow = flow
513
514    def get_flow(self):
515        """A thread local Flow object.
516
517        Returns:
518            A credentials.Flow object, or None if the flow hasn't been set in
519            this thread yet, which happens in _create_flow() since Flows are
520            created lazily.
521        """
522        return getattr(self._tls, 'flow', None)
523
524    flow = property(get_flow, set_flow)
525
526    @util.positional(4)
527    def __init__(self, client_id, client_secret, scope,
528                 auth_uri=oauth2client.GOOGLE_AUTH_URI,
529                 token_uri=oauth2client.GOOGLE_TOKEN_URI,
530                 revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
531                 user_agent=None,
532                 message=None,
533                 callback_path='/oauth2callback',
534                 token_response_param=None,
535                 _storage_class=StorageByKeyName,
536                 _credentials_class=CredentialsModel,
537                 _credentials_property_name='credentials',
538                 **kwargs):
539        """Constructor for OAuth2Decorator
540
541        Args:
542            client_id: string, client identifier.
543            client_secret: string client secret.
544            scope: string or iterable of strings, scope(s) of the credentials
545                   being requested.
546            auth_uri: string, URI for authorization endpoint. For convenience
547                      defaults to Google's endpoints but any OAuth 2.0 provider
548                      can be used.
549            token_uri: string, URI for token endpoint. For convenience defaults
550                       to Google's endpoints but any OAuth 2.0 provider can be
551                       used.
552            revoke_uri: string, URI for revoke endpoint. For convenience
553                        defaults to Google's endpoints but any OAuth 2.0
554                        provider can be used.
555            user_agent: string, User agent of your application, default to
556                        None.
557            message: Message to display if there are problems with the
558                     OAuth 2.0 configuration. The message may contain HTML and
559                     will be presented on the web interface for any method that
560                     uses the decorator.
561            callback_path: string, The absolute path to use as the callback
562                           URI. Note that this must match up with the URI given
563                           when registering the application in the APIs
564                           Console.
565            token_response_param: string. If provided, the full JSON response
566                                  to the access token request will be encoded
567                                  and included in this query parameter in the
568                                  callback URI. This is useful with providers
569                                  (e.g. wordpress.com) that include extra
570                                  fields that the client may want.
571            _storage_class: "Protected" keyword argument not typically provided
572                            to this constructor. A storage class to aid in
573                            storing a Credentials object for a user in the
574                            datastore. Defaults to StorageByKeyName.
575            _credentials_class: "Protected" keyword argument not typically
576                                provided to this constructor. A db or ndb Model
577                                class to hold credentials. Defaults to
578                                CredentialsModel.
579            _credentials_property_name: "Protected" keyword argument not
580                                        typically provided to this constructor.
581                                        A string indicating the name of the
582                                        field on the _credentials_class where a
583                                        Credentials object will be stored.
584                                        Defaults to 'credentials'.
585            **kwargs: dict, Keyword arguments are passed along as kwargs to
586                      the OAuth2WebServerFlow constructor.
587        """
588        self._tls = threading.local()
589        self.flow = None
590        self.credentials = None
591        self._client_id = client_id
592        self._client_secret = client_secret
593        self._scope = util.scopes_to_string(scope)
594        self._auth_uri = auth_uri
595        self._token_uri = token_uri
596        self._revoke_uri = revoke_uri
597        self._user_agent = user_agent
598        self._kwargs = kwargs
599        self._message = message
600        self._in_error = False
601        self._callback_path = callback_path
602        self._token_response_param = token_response_param
603        self._storage_class = _storage_class
604        self._credentials_class = _credentials_class
605        self._credentials_property_name = _credentials_property_name
606
607    def _display_error_message(self, request_handler):
608        request_handler.response.out.write('<html><body>')
609        request_handler.response.out.write(_safe_html(self._message))
610        request_handler.response.out.write('</body></html>')
611
612    def oauth_required(self, method):
613        """Decorator that starts the OAuth 2.0 dance.
614
615        Starts the OAuth dance for the logged in user if they haven't already
616        granted access for this application.
617
618        Args:
619            method: callable, to be decorated method of a webapp.RequestHandler
620                    instance.
621        """
622
623        def check_oauth(request_handler, *args, **kwargs):
624            if self._in_error:
625                self._display_error_message(request_handler)
626                return
627
628            user = users.get_current_user()
629            # Don't use @login_decorator as this could be used in a
630            # POST request.
631            if not user:
632                request_handler.redirect(users.create_login_url(
633                    request_handler.request.uri))
634                return
635
636            self._create_flow(request_handler)
637
638            # Store the request URI in 'state' so we can use it later
639            self.flow.params['state'] = _build_state_value(
640                request_handler, user)
641            self.credentials = self._storage_class(
642                self._credentials_class, None,
643                self._credentials_property_name, user=user).get()
644
645            if not self.has_credentials():
646                return request_handler.redirect(self.authorize_url())
647            try:
648                resp = method(request_handler, *args, **kwargs)
649            except client.AccessTokenRefreshError:
650                return request_handler.redirect(self.authorize_url())
651            finally:
652                self.credentials = None
653            return resp
654
655        return check_oauth
656
657    def _create_flow(self, request_handler):
658        """Create the Flow object.
659
660        The Flow is calculated lazily since we don't know where this app is
661        running until it receives a request, at which point redirect_uri can be
662        calculated and then the Flow object can be constructed.
663
664        Args:
665            request_handler: webapp.RequestHandler, the request handler.
666        """
667        if self.flow is None:
668            redirect_uri = request_handler.request.relative_url(
669                self._callback_path)  # Usually /oauth2callback
670            self.flow = client.OAuth2WebServerFlow(
671                self._client_id, self._client_secret, self._scope,
672                redirect_uri=redirect_uri, user_agent=self._user_agent,
673                auth_uri=self._auth_uri, token_uri=self._token_uri,
674                revoke_uri=self._revoke_uri, **self._kwargs)
675
676    def oauth_aware(self, method):
677        """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
678
679        Does all the setup for the OAuth dance, but doesn't initiate it.
680        This decorator is useful if you want to create a page that knows
681        whether or not the user has granted access to this application.
682        From within a method decorated with @oauth_aware the has_credentials()
683        and authorize_url() methods can be called.
684
685        Args:
686            method: callable, to be decorated method of a webapp.RequestHandler
687                    instance.
688        """
689
690        def setup_oauth(request_handler, *args, **kwargs):
691            if self._in_error:
692                self._display_error_message(request_handler)
693                return
694
695            user = users.get_current_user()
696            # Don't use @login_decorator as this could be used in a
697            # POST request.
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            self.flow.params['state'] = _build_state_value(request_handler,
706                                                           user)
707            self.credentials = self._storage_class(
708                self._credentials_class, None,
709                self._credentials_property_name, user=user).get()
710            try:
711                resp = method(request_handler, *args, **kwargs)
712            finally:
713                self.credentials = None
714            return resp
715        return setup_oauth
716
717    def has_credentials(self):
718        """True if for the logged in user there are valid access Credentials.
719
720        Must only be called from with a webapp.RequestHandler subclassed method
721        that had been decorated with either @oauth_required or @oauth_aware.
722        """
723        return self.credentials is not None and not self.credentials.invalid
724
725    def authorize_url(self):
726        """Returns the URL to start the OAuth dance.
727
728        Must only be called from with a webapp.RequestHandler subclassed method
729        that had been decorated with either @oauth_required or @oauth_aware.
730        """
731        url = self.flow.step1_get_authorize_url()
732        return str(url)
733
734    def http(self, *args, **kwargs):
735        """Returns an authorized http instance.
736
737        Must only be called from within an @oauth_required decorated method, or
738        from within an @oauth_aware decorated method where has_credentials()
739        returns True.
740
741        Args:
742            *args: Positional arguments passed to httplib2.Http constructor.
743            **kwargs: Positional arguments passed to httplib2.Http constructor.
744        """
745        return self.credentials.authorize(httplib2.Http(*args, **kwargs))
746
747    @property
748    def callback_path(self):
749        """The absolute path where the callback will occur.
750
751        Note this is the absolute path, not the absolute URI, that will be
752        calculated by the decorator at runtime. See callback_handler() for how
753        this should be used.
754
755        Returns:
756            The callback path as a string.
757        """
758        return self._callback_path
759
760    def callback_handler(self):
761        """RequestHandler for the OAuth 2.0 redirect callback.
762
763        Usage::
764
765            app = webapp.WSGIApplication([
766                ('/index', MyIndexHandler),
767                ...,
768                (decorator.callback_path, decorator.callback_handler())
769            ])
770
771        Returns:
772            A webapp.RequestHandler that handles the redirect back from the
773            server during the OAuth 2.0 dance.
774        """
775        decorator = self
776
777        class OAuth2Handler(webapp.RequestHandler):
778            """Handler for the redirect_uri of the OAuth 2.0 dance."""
779
780            @login_required
781            def get(self):
782                error = self.request.get('error')
783                if error:
784                    errormsg = self.request.get('error_description', error)
785                    self.response.out.write(
786                        'The authorization request failed: {0}'.format(
787                            _safe_html(errormsg)))
788                else:
789                    user = users.get_current_user()
790                    decorator._create_flow(self)
791                    credentials = decorator.flow.step2_exchange(
792                        self.request.params)
793                    decorator._storage_class(
794                        decorator._credentials_class, None,
795                        decorator._credentials_property_name,
796                        user=user).put(credentials)
797                    redirect_uri = _parse_state_value(
798                        str(self.request.get('state')), user)
799                    if redirect_uri is None:
800                        self.response.out.write(
801                            'The authorization request failed')
802                        return
803
804                    if (decorator._token_response_param and
805                            credentials.token_response):
806                        resp_json = json.dumps(credentials.token_response)
807                        redirect_uri = util._add_query_parameter(
808                            redirect_uri, decorator._token_response_param,
809                            resp_json)
810
811                    self.redirect(redirect_uri)
812
813        return OAuth2Handler
814
815    def callback_application(self):
816        """WSGI application for handling the OAuth 2.0 redirect callback.
817
818        If you need finer grained control use `callback_handler` which returns
819        just the webapp.RequestHandler.
820
821        Returns:
822            A webapp.WSGIApplication that handles the redirect back from the
823            server during the OAuth 2.0 dance.
824        """
825        return webapp.WSGIApplication([
826            (self.callback_path, self.callback_handler())
827        ])
828
829
830class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
831    """An OAuth2Decorator that builds from a clientsecrets file.
832
833    Uses a clientsecrets file as the source for all the information when
834    constructing an OAuth2Decorator.
835
836    ::
837
838        decorator = OAuth2DecoratorFromClientSecrets(
839            os.path.join(os.path.dirname(__file__), 'client_secrets.json')
840            scope='https://www.googleapis.com/auth/plus')
841
842        class MainHandler(webapp.RequestHandler):
843            @decorator.oauth_required
844            def get(self):
845                http = decorator.http()
846                # http is authorized with the user's Credentials and can be
847                # used in API calls
848
849    """
850
851    @util.positional(3)
852    def __init__(self, filename, scope, message=None, cache=None, **kwargs):
853        """Constructor
854
855        Args:
856            filename: string, File name of client secrets.
857            scope: string or iterable of strings, scope(s) of the credentials
858                   being requested.
859            message: string, A friendly string to display to the user if the
860                     clientsecrets file is missing or invalid. The message may
861                     contain HTML and will be presented on the web interface
862                     for any method that uses the decorator.
863            cache: An optional cache service client that implements get() and
864                   set()
865            methods. See clientsecrets.loadfile() for details.
866            **kwargs: dict, Keyword arguments are passed along as kwargs to
867                      the OAuth2WebServerFlow constructor.
868        """
869        client_type, client_info = clientsecrets.loadfile(filename,
870                                                          cache=cache)
871        if client_type not in (clientsecrets.TYPE_WEB,
872                               clientsecrets.TYPE_INSTALLED):
873            raise clientsecrets.InvalidClientSecretsError(
874                "OAuth2Decorator doesn't support this OAuth 2.0 flow.")
875
876        constructor_kwargs = dict(kwargs)
877        constructor_kwargs.update({
878            'auth_uri': client_info['auth_uri'],
879            'token_uri': client_info['token_uri'],
880            'message': message,
881        })
882        revoke_uri = client_info.get('revoke_uri')
883        if revoke_uri is not None:
884            constructor_kwargs['revoke_uri'] = revoke_uri
885        super(OAuth2DecoratorFromClientSecrets, self).__init__(
886            client_info['client_id'], client_info['client_secret'],
887            scope, **constructor_kwargs)
888        if message is not None:
889            self._message = message
890        else:
891            self._message = 'Please configure your application for OAuth 2.0.'
892
893
894@util.positional(2)
895def oauth2decorator_from_clientsecrets(filename, scope,
896                                       message=None, cache=None):
897    """Creates an OAuth2Decorator populated from a clientsecrets file.
898
899    Args:
900        filename: string, File name of client secrets.
901        scope: string or list of strings, scope(s) of the credentials being
902               requested.
903        message: string, A friendly string to display to the user if the
904                 clientsecrets file is missing or invalid. The message may
905                 contain HTML and will be presented on the web interface for
906                 any method that uses the decorator.
907        cache: An optional cache service client that implements get() and set()
908               methods. See clientsecrets.loadfile() for details.
909
910    Returns: An OAuth2Decorator
911    """
912    return OAuth2DecoratorFromClientSecrets(filename, scope,
913                                            message=message, cache=cache)
914