Home | History | Annotate | Download | only in contrib
      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 
     17 Utilities for making it easier to use OAuth 2.0 on Google App Engine.
     18 """
     19 
     20 import cgi
     21 import json
     22 import logging
     23 import os
     24 import pickle
     25 import threading
     26 
     27 from google.appengine.api import app_identity
     28 from google.appengine.api import memcache
     29 from google.appengine.api import users
     30 from google.appengine.ext import db
     31 from google.appengine.ext.webapp.util import login_required
     32 import httplib2
     33 import webapp2 as webapp
     34 
     35 import oauth2client
     36 from oauth2client import client
     37 from oauth2client import clientsecrets
     38 from oauth2client import util
     39 from oauth2client.contrib import xsrfutil
     40 
     41 # This is a temporary fix for a Google internal issue.
     42 try:
     43     from oauth2client.contrib import _appengine_ndb
     44 except ImportError:  # pragma: NO COVER
     45     _appengine_ndb = None
     46 
     47 
     48 __author__ = 'jcgregorio (at] google.com (Joe Gregorio)'
     49 
     50 logger = logging.getLogger(__name__)
     51 
     52 OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
     53 
     54 XSRF_MEMCACHE_ID = 'xsrf_secret_key'
     55 
     56 if _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
     63 else:
     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 
     72 def _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 
     84 class 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 
     93 def _generate_new_xsrf_secret_key():
     94     """Returns a random XSRF secret key."""
     95     return os.urandom(16).encode("hex")
     96 
     97 
     98 def 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 
    121 class 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 
    223 class 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 
    256 class 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 
    300 class 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 
    427 class 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 
    435 def _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 
    454 def _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 
    474 class 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 
    830 class 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)
    895 def 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