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
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
22import cgi
23import json
24import logging
25import os
26import pickle
27import threading
28
29import httplib2
30
31from google.appengine.api import app_identity
32from google.appengine.api import memcache
33from google.appengine.api import users
34from google.appengine.ext import db
35from google.appengine.ext import webapp
36from google.appengine.ext.webapp.util import login_required
37from google.appengine.ext.webapp.util import run_wsgi_app
38from oauth2client import GOOGLE_AUTH_URI
39from oauth2client import GOOGLE_REVOKE_URI
40from oauth2client import GOOGLE_TOKEN_URI
41from oauth2client import clientsecrets
42from oauth2client import util
43from oauth2client import xsrfutil
44from oauth2client.client import AccessTokenRefreshError
45from oauth2client.client import AssertionCredentials
46from oauth2client.client import Credentials
47from oauth2client.client import Flow
48from oauth2client.client import OAuth2WebServerFlow
49from oauth2client.client import Storage
50
51# TODO(dhermes): Resolve import issue.
52# This is a temporary fix for a Google internal issue.
53try:
54  from google.appengine.ext import ndb
55except ImportError:
56  ndb = None
57
58
59logger = logging.getLogger(__name__)
60
61OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
62
63XSRF_MEMCACHE_ID = 'xsrf_secret_key'
64
65
66def _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
78class InvalidClientSecretsError(Exception):
79  """The client_secrets.json file is malformed or missing required fields."""
80
81
82class InvalidXsrfTokenError(Exception):
83  """The XSRF token is invalid or expired."""
84
85
86class 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
94if 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
113def _generate_new_xsrf_secret_key():
114  """Returns a random XSRF secret key.
115  """
116  return os.urandom(16).encode("hex")
117
118
119def 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
141class 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
210class 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
242if 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
269class 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
315if 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
377class 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
498class 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
506if 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
526def _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
545def _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
568class 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
906class 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)
968def 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