Home | History | Annotate | Download | only in oauth2client
      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 __author__ = 'jcgregorio (at] 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 # TODO(dhermes): Resolve import issue.
     52 # This is a temporary fix for a Google internal issue.
     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'
     64 
     65 
     66 def _safe_html(s):
     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 
     77 
     78 class InvalidClientSecretsError(Exception):
     79   """The client_secrets.json file is malformed or missing required fields."""
     80 
     81 
     82 class InvalidXsrfTokenError(Exception):
     83   """The XSRF token is invalid or expired."""
     84 
     85 
     86 class SiteXsrfSecretKey(db.Model):
     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:
     95   class SiteXsrfSecretKeyNDB(ndb.Model):
     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
    108     def _get_kind(cls):
    109       """Return the kind name for this class."""
    110       return 'SiteXsrfSecretKey'
    111 
    112 
    113 def _generate_new_xsrf_secret_key():
    114   """Returns a random XSRF secret key.
    115   """
    116   return os.urandom(16).encode("hex")
    117 
    118 
    119 def xsrf_secret_key():
    120   """Return the secret key for use for XSRF protection.
    121 
    122   If the Site entity does not have a secret key, this method will also create
    123   one and persist it.
    124 
    125   Returns:
    126     The secret key.
    127   """
    128   secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
    129   if not secret:
    130     # Load the one and only instance of SiteXsrfSecretKey.
    131     model = SiteXsrfSecretKey.get_or_insert(key_name='site')
    132     if not model.secret:
    133       model.secret = _generate_new_xsrf_secret_key()
    134       model.put()
    135     secret = model.secret
    136     memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
    137 
    138   return str(secret)
    139 
    140 
    141 class AppAssertionCredentials(AssertionCredentials):
    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)
    155   def __init__(self, scope, **kwargs):
    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     # Assertion type is no longer used, but still in the parent class signature.
    170     super(AppAssertionCredentials, self).__init__(None)
    171 
    172   @classmethod
    173   def from_json(cls, json_data):
    174     data = json.loads(json_data)
    175     return AppAssertionCredentials(data['scope'])
    176 
    177   def _refresh(self, http_request):
    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
    200   def serialization_data(self):
    201     raise NotImplementedError('Cannot serialize credentials for AppEngine.')
    202 
    203   def create_scoped_required(self):
    204     return not self.scope
    205 
    206   def create_scoped(self, scopes):
    207     return AppAssertionCredentials(scopes, **self._kwargs)
    208 
    209 
    210 class FlowProperty(db.Property):
    211   """App Engine datastore Property for Flow.
    212 
    213   Utility property that allows easy storage and retrieval of an
    214   oauth2client.Flow"""
    215 
    216   # Tell what the user type is.
    217   data_type = Flow
    218 
    219   # For writing to datastore.
    220   def get_value_for_datastore(self, model_instance):
    221     flow = super(FlowProperty,
    222                  self).get_value_for_datastore(model_instance)
    223     return db.Blob(pickle.dumps(flow))
    224 
    225   # For reading from datastore.
    226   def make_value_from_datastore(self, value):
    227     if value is None:
    228       return None
    229     return pickle.loads(value)
    230 
    231   def validate(self, value):
    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 
    238   def empty(self, value):
    239     return not value
    240 
    241 
    242 if ndb is not None:
    243   class FlowNDBProperty(ndb.PickleProperty):
    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 
    254     def _validate(self, value):
    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 
    268 
    269 class CredentialsProperty(db.Property):
    270   """App Engine datastore Property for Credentials.
    271 
    272   Utility property that allows easy storage and retrieval of
    273   oath2client.Credentials
    274   """
    275 
    276   # Tell what the user type is.
    277   data_type = Credentials
    278 
    279   # For writing to datastore.
    280   def get_value_for_datastore(self, model_instance):
    281     logger.info("get: Got type " + str(type(model_instance)))
    282     cred = super(CredentialsProperty,
    283                  self).get_value_for_datastore(model_instance)
    284     if cred is None:
    285       cred = ''
    286     else:
    287       cred = cred.to_json()
    288     return db.Blob(cred)
    289 
    290   # For reading from datastore.
    291   def make_value_from_datastore(self, value):
    292     logger.info("make: Got type " + str(type(value)))
    293     if value is None:
    294       return None
    295     if len(value) == 0:
    296       return None
    297     try:
    298       credentials = Credentials.new_from_json(value)
    299     except ValueError:
    300       credentials = None
    301     return credentials
    302 
    303   def validate(self, value):
    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     #if value is not None and not isinstance(value, Credentials):
    311     #  return None
    312     return value
    313 
    314 
    315 if ndb is not None:
    316   # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
    317   #                and subclass mechanics to use new_from_dict, to_dict,
    318   #                from_dict, etc.
    319   class CredentialsNDBProperty(ndb.BlobProperty):
    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     """
    329     def _validate(self, value):
    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 
    343     def _to_base_type(self, value):
    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 
    357     def _from_base_type(self, value):
    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         # Uses the from_json method of the implied class of value
    371         credentials = Credentials.new_from_json(value)
    372       except ValueError:
    373         credentials = None
    374       return credentials
    375 
    376 
    377 class StorageByKeyName(Storage):
    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 
    410   def _is_ndb(self):
    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     # issubclass will fail if one of the arguments is not a class, only need
    417     # worry about new-style classes since ndb and db models are new-style
    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 
    426   def _get_entity(self):
    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 
    440   def _delete_entity(self):
    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)
    453   def locked_get(self):
    454     """Retrieve Credential from datastore.
    455 
    456     Returns:
    457       oauth2client.Credentials
    458     """
    459     credentials = None
    460     if self._cache:
    461       json = self._cache.get(self._key_name)
    462       if json:
    463         credentials = Credentials.new_from_json(json)
    464     if credentials is None:
    465       entity = self._get_entity()
    466       if entity is not None:
    467         credentials = getattr(entity, self._property_name)
    468         if self._cache:
    469           self._cache.set(self._key_name, credentials.to_json())
    470 
    471     if credentials and hasattr(credentials, 'set_store'):
    472       credentials.set_store(self)
    473     return credentials
    474 
    475   @db.non_transactional(allow_existing=True)
    476   def locked_put(self, credentials):
    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)
    489   def locked_delete(self):
    490     """Delete Credential from datastore."""
    491 
    492     if self._cache:
    493       self._cache.delete(self._key_name)
    494 
    495     self._delete_entity()
    496 
    497 
    498 class CredentialsModel(db.Model):
    499   """Storage for OAuth 2.0 Credentials
    500 
    501   Storage of the model is keyed by the user.user_id().
    502   """
    503   credentials = CredentialsProperty()
    504 
    505 
    506 if ndb is not None:
    507   class CredentialsNDBModel(ndb.Model):
    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
    521     def _get_kind(cls):
    522       """Return the kind name for this class."""
    523       return 'CredentialsModel'
    524 
    525 
    526 def _build_state_value(request_handler, user):
    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 
    544 
    545 def _parse_state_value(state, user):
    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 
    567 
    568 class OAuth2Decorator(object):
    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   ::
    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     class MainHandler(webapp.RequestHandler):
    582       @decorator.oauth_required
    583       def get(self):
    584         http = decorator.http()
    585         # http is authorized with the user's Credentials and can be used
    586         # in API calls
    587 
    588   """
    589 
    590   def set_credentials(self, credentials):
    591     self._tls.credentials = credentials
    592 
    593   def get_credentials(self):
    594     """A thread local Credentials object.
    595 
    596     Returns:
    597       A client.Credentials object, or None if credentials hasn't been set in
    598       this thread yet, which may happen when calling has_credentials inside
    599       oauth_aware.
    600     """
    601     return getattr(self._tls, 'credentials', None)
    602 
    603   credentials = property(get_credentials, set_credentials)
    604 
    605   def set_flow(self, flow):
    606     self._tls.flow = flow
    607 
    608   def get_flow(self):
    609     """A thread local Flow object.
    610 
    611     Returns:
    612       A credentials.Flow object, or None if the flow hasn't been set in this
    613       thread yet, which happens in _create_flow() since Flows are created
    614       lazily.
    615     """
    616     return getattr(self._tls, 'flow', None)
    617 
    618   flow = property(get_flow, set_flow)
    619 
    620 
    621   @util.positional(4)
    622   def __init__(self, client_id, client_secret, scope,
    623                auth_uri=GOOGLE_AUTH_URI,
    624                token_uri=GOOGLE_TOKEN_URI,
    625                revoke_uri=GOOGLE_REVOKE_URI,
    626                user_agent=None,
    627                message=None,
    628                callback_path='/oauth2callback',
    629                token_response_param=None,
    630                _storage_class=StorageByKeyName,
    631                _credentials_class=CredentialsModel,
    632                _credentials_property_name='credentials',
    633                **kwargs):
    634 
    635     """Constructor for OAuth2Decorator
    636 
    637     Args:
    638       client_id: string, client identifier.
    639       client_secret: string client secret.
    640       scope: string or iterable of strings, scope(s) of the credentials being
    641         requested.
    642       auth_uri: string, URI for authorization endpoint. For convenience
    643         defaults to Google's endpoints but any OAuth 2.0 provider can be used.
    644       token_uri: string, URI for token endpoint. For convenience
    645         defaults to Google's endpoints but any OAuth 2.0 provider can be used.
    646       revoke_uri: string, URI for revoke endpoint. For convenience
    647         defaults to Google's endpoints but any OAuth 2.0 provider can be used.
    648       user_agent: string, User agent of your application, default to None.
    649       message: Message to display if there are problems with the OAuth 2.0
    650         configuration. The message may contain HTML and will be presented on the
    651         web interface for any method that uses the decorator.
    652       callback_path: string, The absolute path to use as the callback URI. Note
    653         that this must match up with the URI given when registering the
    654         application in the APIs Console.
    655       token_response_param: string. If provided, the full JSON response
    656         to the access token request will be encoded and included in this query
    657         parameter in the callback URI. This is useful with providers (e.g.
    658         wordpress.com) that include extra fields that the client may want.
    659       _storage_class: "Protected" keyword argument not typically provided to
    660         this constructor. A storage class to aid in storing a Credentials object
    661         for a user in the datastore. Defaults to StorageByKeyName.
    662       _credentials_class: "Protected" keyword argument not typically provided to
    663         this constructor. A db or ndb Model class to hold credentials. Defaults
    664         to CredentialsModel.
    665       _credentials_property_name: "Protected" keyword argument not typically
    666         provided to this constructor. A string indicating the name of the field
    667         on the _credentials_class where a Credentials object will be stored.
    668         Defaults to 'credentials'.
    669       **kwargs: dict, Keyword arguments are passed along as kwargs to
    670         the OAuth2WebServerFlow constructor.
    671 
    672     """
    673     self._tls = threading.local()
    674     self.flow = None
    675     self.credentials = None
    676     self._client_id = client_id
    677     self._client_secret = client_secret
    678     self._scope = util.scopes_to_string(scope)
    679     self._auth_uri = auth_uri
    680     self._token_uri = token_uri
    681     self._revoke_uri = revoke_uri
    682     self._user_agent = user_agent
    683     self._kwargs = kwargs
    684     self._message = message
    685     self._in_error = False
    686     self._callback_path = callback_path
    687     self._token_response_param = token_response_param
    688     self._storage_class = _storage_class
    689     self._credentials_class = _credentials_class
    690     self._credentials_property_name = _credentials_property_name
    691 
    692   def _display_error_message(self, request_handler):
    693     request_handler.response.out.write('<html><body>')
    694     request_handler.response.out.write(_safe_html(self._message))
    695     request_handler.response.out.write('</body></html>')
    696 
    697   def oauth_required(self, method):
    698     """Decorator that starts the OAuth 2.0 dance.
    699 
    700     Starts the OAuth dance for the logged in user if they haven't already
    701     granted access for this application.
    702 
    703     Args:
    704       method: callable, to be decorated method of a webapp.RequestHandler
    705         instance.
    706     """
    707 
    708     def check_oauth(request_handler, *args, **kwargs):
    709       if self._in_error:
    710         self._display_error_message(request_handler)
    711         return
    712 
    713       user = users.get_current_user()
    714       # Don't use @login_decorator as this could be used in a POST request.
    715       if not user:
    716         request_handler.redirect(users.create_login_url(
    717             request_handler.request.uri))
    718         return
    719 
    720       self._create_flow(request_handler)
    721 
    722       # Store the request URI in 'state' so we can use it later
    723       self.flow.params['state'] = _build_state_value(request_handler, user)
    724       self.credentials = self._storage_class(
    725           self._credentials_class, None,
    726           self._credentials_property_name, user=user).get()
    727 
    728       if not self.has_credentials():
    729         return request_handler.redirect(self.authorize_url())
    730       try:
    731         resp = method(request_handler, *args, **kwargs)
    732       except AccessTokenRefreshError:
    733         return request_handler.redirect(self.authorize_url())
    734       finally:
    735         self.credentials = None
    736       return resp
    737 
    738     return check_oauth
    739 
    740   def _create_flow(self, request_handler):
    741     """Create the Flow object.
    742 
    743     The Flow is calculated lazily since we don't know where this app is
    744     running until it receives a request, at which point redirect_uri can be
    745     calculated and then the Flow object can be constructed.
    746 
    747     Args:
    748       request_handler: webapp.RequestHandler, the request handler.
    749     """
    750     if self.flow is None:
    751       redirect_uri = request_handler.request.relative_url(
    752           self._callback_path) # Usually /oauth2callback
    753       self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
    754                                       self._scope, redirect_uri=redirect_uri,
    755                                       user_agent=self._user_agent,
    756                                       auth_uri=self._auth_uri,
    757                                       token_uri=self._token_uri,
    758                                       revoke_uri=self._revoke_uri,
    759                                       **self._kwargs)
    760 
    761   def oauth_aware(self, method):
    762     """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
    763 
    764     Does all the setup for the OAuth dance, but doesn't initiate it.
    765     This decorator is useful if you want to create a page that knows
    766     whether or not the user has granted access to this application.
    767     From within a method decorated with @oauth_aware the has_credentials()
    768     and authorize_url() methods can be called.
    769 
    770     Args:
    771       method: callable, to be decorated method of a webapp.RequestHandler
    772         instance.
    773     """
    774 
    775     def setup_oauth(request_handler, *args, **kwargs):
    776       if self._in_error:
    777         self._display_error_message(request_handler)
    778         return
    779 
    780       user = users.get_current_user()
    781       # Don't use @login_decorator as this could be used in a POST request.
    782       if not user:
    783         request_handler.redirect(users.create_login_url(
    784             request_handler.request.uri))
    785         return
    786 
    787       self._create_flow(request_handler)
    788 
    789       self.flow.params['state'] = _build_state_value(request_handler, user)
    790       self.credentials = self._storage_class(
    791           self._credentials_class, None,
    792           self._credentials_property_name, user=user).get()
    793       try:
    794         resp = method(request_handler, *args, **kwargs)
    795       finally:
    796         self.credentials = None
    797       return resp
    798     return setup_oauth
    799 
    800 
    801   def has_credentials(self):
    802     """True if for the logged in user there are valid access Credentials.
    803 
    804     Must only be called from with a webapp.RequestHandler subclassed method
    805     that had been decorated with either @oauth_required or @oauth_aware.
    806     """
    807     return self.credentials is not None and not self.credentials.invalid
    808 
    809   def authorize_url(self):
    810     """Returns the URL to start the OAuth dance.
    811 
    812     Must only be called from with a webapp.RequestHandler subclassed method
    813     that had been decorated with either @oauth_required or @oauth_aware.
    814     """
    815     url = self.flow.step1_get_authorize_url()
    816     return str(url)
    817 
    818   def http(self, *args, **kwargs):
    819     """Returns an authorized http instance.
    820 
    821     Must only be called from within an @oauth_required decorated method, or
    822     from within an @oauth_aware decorated method where has_credentials()
    823     returns True.
    824 
    825     Args:
    826         *args: Positional arguments passed to httplib2.Http constructor.
    827         **kwargs: Positional arguments passed to httplib2.Http constructor.
    828     """
    829     return self.credentials.authorize(httplib2.Http(*args, **kwargs))
    830 
    831   @property
    832   def callback_path(self):
    833     """The absolute path where the callback will occur.
    834 
    835     Note this is the absolute path, not the absolute URI, that will be
    836     calculated by the decorator at runtime. See callback_handler() for how this
    837     should be used.
    838 
    839     Returns:
    840       The callback path as a string.
    841     """
    842     return self._callback_path
    843 
    844 
    845   def callback_handler(self):
    846     """RequestHandler for the OAuth 2.0 redirect callback.
    847 
    848     Usage::
    849 
    850        app = webapp.WSGIApplication([
    851          ('/index', MyIndexHandler),
    852          ...,
    853          (decorator.callback_path, decorator.callback_handler())
    854        ])
    855 
    856     Returns:
    857       A webapp.RequestHandler that handles the redirect back from the
    858       server during the OAuth 2.0 dance.
    859     """
    860     decorator = self
    861 
    862     class OAuth2Handler(webapp.RequestHandler):
    863       """Handler for the redirect_uri of the OAuth 2.0 dance."""
    864 
    865       @login_required
    866       def get(self):
    867         error = self.request.get('error')
    868         if error:
    869           errormsg = self.request.get('error_description', error)
    870           self.response.out.write(
    871               'The authorization request failed: %s' % _safe_html(errormsg))
    872         else:
    873           user = users.get_current_user()
    874           decorator._create_flow(self)
    875           credentials = decorator.flow.step2_exchange(self.request.params)
    876           decorator._storage_class(
    877               decorator._credentials_class, None,
    878               decorator._credentials_property_name, user=user).put(credentials)
    879           redirect_uri = _parse_state_value(str(self.request.get('state')),
    880                                             user)
    881 
    882           if decorator._token_response_param and credentials.token_response:
    883             resp_json = json.dumps(credentials.token_response)
    884             redirect_uri = util._add_query_parameter(
    885                 redirect_uri, decorator._token_response_param, resp_json)
    886 
    887           self.redirect(redirect_uri)
    888 
    889     return OAuth2Handler
    890 
    891   def callback_application(self):
    892     """WSGI application for handling the OAuth 2.0 redirect callback.
    893 
    894     If you need finer grained control use `callback_handler` which returns just
    895     the webapp.RequestHandler.
    896 
    897     Returns:
    898       A webapp.WSGIApplication that handles the redirect back from the
    899       server during the OAuth 2.0 dance.
    900     """
    901     return webapp.WSGIApplication([
    902         (self.callback_path, self.callback_handler())
    903         ])
    904 
    905 
    906 class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
    907   """An OAuth2Decorator that builds from a clientsecrets file.
    908 
    909   Uses a clientsecrets file as the source for all the information when
    910   constructing an OAuth2Decorator.
    911 
    912   ::
    913 
    914     decorator = OAuth2DecoratorFromClientSecrets(
    915       os.path.join(os.path.dirname(__file__), 'client_secrets.json')
    916       scope='https://www.googleapis.com/auth/plus')
    917 
    918     class MainHandler(webapp.RequestHandler):
    919       @decorator.oauth_required
    920       def get(self):
    921         http = decorator.http()
    922         # http is authorized with the user's Credentials and can be used
    923         # in API calls
    924 
    925   """
    926 
    927   @util.positional(3)
    928   def __init__(self, filename, scope, message=None, cache=None, **kwargs):
    929     """Constructor
    930 
    931     Args:
    932       filename: string, File name of client secrets.
    933       scope: string or iterable of strings, scope(s) of the credentials being
    934         requested.
    935       message: string, A friendly string to display to the user if the
    936         clientsecrets file is missing or invalid. The message may contain HTML
    937         and will be presented on the web interface for any method that uses the
    938         decorator.
    939       cache: An optional cache service client that implements get() and set()
    940         methods. See clientsecrets.loadfile() for details.
    941       **kwargs: dict, Keyword arguments are passed along as kwargs to
    942         the OAuth2WebServerFlow constructor.
    943     """
    944     client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
    945     if client_type not in [
    946         clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
    947       raise InvalidClientSecretsError(
    948           "OAuth2Decorator doesn't support this OAuth 2.0 flow.")
    949     constructor_kwargs = dict(kwargs)
    950     constructor_kwargs.update({
    951         'auth_uri': client_info['auth_uri'],
    952         'token_uri': client_info['token_uri'],
    953         'message': message,
    954     })
    955     revoke_uri = client_info.get('revoke_uri')
    956     if revoke_uri is not None:
    957       constructor_kwargs['revoke_uri'] = revoke_uri
    958     super(OAuth2DecoratorFromClientSecrets, self).__init__(
    959         client_info['client_id'], client_info['client_secret'],
    960         scope, **constructor_kwargs)
    961     if message is not None:
    962       self._message = message
    963     else:
    964       self._message = 'Please configure your application for OAuth 2.0.'
    965 
    966 
    967 @util.positional(2)
    968 def oauth2decorator_from_clientsecrets(filename, scope,
    969                                        message=None, cache=None):
    970   """Creates an OAuth2Decorator populated from a clientsecrets file.
    971 
    972   Args:
    973     filename: string, File name of client secrets.
    974     scope: string or list of strings, scope(s) of the credentials being
    975       requested.
    976     message: string, A friendly string to display to the user if the
    977       clientsecrets file is missing or invalid. The message may contain HTML and
    978       will be presented on the web interface for any method that uses the
    979       decorator.
    980     cache: An optional cache service client that implements get() and set()
    981       methods. See clientsecrets.loadfile() for details.
    982 
    983   Returns: An OAuth2Decorator
    984 
    985   """
    986   return OAuth2DecoratorFromClientSecrets(filename, scope,
    987                                           message=message, cache=cache)
    988