From bb1354e256e82c084e3d6af0de6cf23f1c515ea6 Mon Sep 17 00:00:00 2001 From: Edward Lemur Date: Tue, 22 Oct 2019 22:19:20 +0000 Subject: [PATCH] depot_tools: Remove oauth2client. Bug: 1001756 Change-Id: I135e424bf96def8f964ccefc5161274456152acb Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1874452 Reviewed-by: Vadim Shtayura Commit-Queue: Edward Lesmes --- third_party/oauth2client/LICENSE | 202 --- third_party/oauth2client/MODIFICATIONS.diff | 66 - third_party/oauth2client/README.chromium | 15 - third_party/oauth2client/__init__.py | 5 - third_party/oauth2client/anyjson.py | 32 - third_party/oauth2client/appengine.py | 963 ------------- third_party/oauth2client/client.py | 1363 ------------------- third_party/oauth2client/clientsecrets.py | 153 --- third_party/oauth2client/crypt.py | 377 ----- third_party/oauth2client/django_orm.py | 134 -- third_party/oauth2client/file.py | 124 -- third_party/oauth2client/gce.py | 90 -- third_party/oauth2client/keyring_storage.py | 109 -- third_party/oauth2client/locked_file.py | 373 ----- third_party/oauth2client/multistore_file.py | 465 ------- third_party/oauth2client/old_run.py | 160 --- third_party/oauth2client/tools.py | 243 ---- third_party/oauth2client/util.py | 196 --- third_party/oauth2client/xsrfutil.py | 113 -- 19 files changed, 5183 deletions(-) delete mode 100644 third_party/oauth2client/LICENSE delete mode 100644 third_party/oauth2client/MODIFICATIONS.diff delete mode 100644 third_party/oauth2client/README.chromium delete mode 100644 third_party/oauth2client/__init__.py delete mode 100644 third_party/oauth2client/anyjson.py delete mode 100644 third_party/oauth2client/appengine.py delete mode 100644 third_party/oauth2client/client.py delete mode 100644 third_party/oauth2client/clientsecrets.py delete mode 100644 third_party/oauth2client/crypt.py delete mode 100644 third_party/oauth2client/django_orm.py delete mode 100644 third_party/oauth2client/file.py delete mode 100644 third_party/oauth2client/gce.py delete mode 100644 third_party/oauth2client/keyring_storage.py delete mode 100644 third_party/oauth2client/locked_file.py delete mode 100644 third_party/oauth2client/multistore_file.py delete mode 100644 third_party/oauth2client/old_run.py delete mode 100644 third_party/oauth2client/tools.py delete mode 100644 third_party/oauth2client/util.py delete mode 100644 third_party/oauth2client/xsrfutil.py diff --git a/third_party/oauth2client/LICENSE b/third_party/oauth2client/LICENSE deleted file mode 100644 index d645695673..0000000000 --- a/third_party/oauth2client/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/third_party/oauth2client/MODIFICATIONS.diff b/third_party/oauth2client/MODIFICATIONS.diff deleted file mode 100644 index 7490d914be..0000000000 --- a/third_party/oauth2client/MODIFICATIONS.diff +++ /dev/null @@ -1,66 +0,0 @@ -diff --git a/third_party/oauth2client/client.py b/third_party/oauth2client/client.py -index 4e8e616..6901f3f 100644 ---- a/third_party/oauth2client/client.py -+++ b/third_party/oauth2client/client.py -@@ -23,24 +23,23 @@ import base64 - import clientsecrets - import copy - import datetime --import httplib2 -+from .. import httplib2 - import logging --import os - import sys - import time - import urllib - import urlparse - --from oauth2client import GOOGLE_AUTH_URI --from oauth2client import GOOGLE_REVOKE_URI --from oauth2client import GOOGLE_TOKEN_URI --from oauth2client import util --from oauth2client.anyjson import simplejson -+from . import GOOGLE_AUTH_URI -+from . import GOOGLE_REVOKE_URI -+from . import GOOGLE_TOKEN_URI -+from . import util -+from .anyjson import simplejson - - HAS_OPENSSL = False - HAS_CRYPTO = False - try: -- from oauth2client import crypt -+ from . import crypt - HAS_CRYPTO = True - if crypt.OpenSSLVerifier is not None: - HAS_OPENSSL = True -diff --git a/third_party/oauth2client/locked_file.py b/third_party/oauth2client/locked_file.py -index 31514dc..858b702 100644 ---- a/third_party/oauth2client/locked_file.py -+++ b/third_party/oauth2client/locked_file.py -@@ -35,7 +35,7 @@ import logging - import os - import time - --from oauth2client import util -+from . import util - - logger = logging.getLogger(__name__) - -diff --git a/third_party/oauth2client/multistore_file.py b/third_party/oauth2client/multistore_file.py -index ce7a519..ea89027 100644 ---- a/third_party/oauth2client/multistore_file.py -+++ b/third_party/oauth2client/multistore_file.py -@@ -50,9 +50,9 @@ import os - import threading - - from anyjson import simplejson --from oauth2client.client import Storage as BaseStorage --from oauth2client.client import Credentials --from oauth2client import util -+from .client import Storage as BaseStorage -+from .client import Credentials -+from . import util - from locked_file import LockedFile - - logger = logging.getLogger(__name__) diff --git a/third_party/oauth2client/README.chromium b/third_party/oauth2client/README.chromium deleted file mode 100644 index ea15b96361..0000000000 --- a/third_party/oauth2client/README.chromium +++ /dev/null @@ -1,15 +0,0 @@ -Name: oauth2client -Short Name: oauth2client -URL: https://pypi.python.org/packages/source/o/oauth2client/oauth2client-1.2.tar.gz -Version: 1.2 -License: Apache License 2.0 - -Description: -OAuth2 authentication library in Python - -Local modifications: -See also MODIFICATIONS.diff - -Notes: -Requires the httplib2 library. - diff --git a/third_party/oauth2client/__init__.py b/third_party/oauth2client/__init__.py deleted file mode 100644 index ac847483d0..0000000000 --- a/third_party/oauth2client/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -__version__ = "1.2" - -GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth' -GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke' -GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token' diff --git a/third_party/oauth2client/anyjson.py b/third_party/oauth2client/anyjson.py deleted file mode 100644 index ae21c338ba..0000000000 --- a/third_party/oauth2client/anyjson.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (C) 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utility module to import a JSON module - -Hides all the messy details of exactly where -we get a simplejson module from. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - - -try: # pragma: no cover - # Should work for Python2.6 and higher. - import json as simplejson -except ImportError: # pragma: no cover - try: - import simplejson - except ImportError: - # Try to import from django, should work on App Engine - from django.utils import simplejson diff --git a/third_party/oauth2client/appengine.py b/third_party/oauth2client/appengine.py deleted file mode 100644 index 5cd3f4bafb..0000000000 --- a/third_party/oauth2client/appengine.py +++ /dev/null @@ -1,963 +0,0 @@ -# Copyright (C) 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utilities for Google App Engine - -Utilities for making it easier to use OAuth 2.0 on Google App Engine. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import base64 -import cgi -import httplib2 -import logging -import os -import pickle -import threading -import time - -from google.appengine.api import app_identity -from google.appengine.api import memcache -from google.appengine.api import users -from google.appengine.ext import db -from google.appengine.ext import webapp -from google.appengine.ext.webapp.util import login_required -from google.appengine.ext.webapp.util import run_wsgi_app -from oauth2client import GOOGLE_AUTH_URI -from oauth2client import GOOGLE_REVOKE_URI -from oauth2client import GOOGLE_TOKEN_URI -from oauth2client import clientsecrets -from oauth2client import util -from oauth2client import xsrfutil -from oauth2client.anyjson import simplejson -from oauth2client.client import AccessTokenRefreshError -from oauth2client.client import AssertionCredentials -from oauth2client.client import Credentials -from oauth2client.client import Flow -from oauth2client.client import OAuth2WebServerFlow -from oauth2client.client import Storage - -# TODO(dhermes): Resolve import issue. -# This is a temporary fix for a Google internal issue. -try: - from google.appengine.ext import ndb -except ImportError: - ndb = None - - -logger = logging.getLogger(__name__) - -OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' - -XSRF_MEMCACHE_ID = 'xsrf_secret_key' - - -def _safe_html(s): - """Escape text to make it safe to display. - - Args: - s: string, The text to escape. - - Returns: - The escaped text as a string. - """ - return cgi.escape(s, quote=1).replace("'", ''') - - -class InvalidClientSecretsError(Exception): - """The client_secrets.json file is malformed or missing required fields.""" - - -class InvalidXsrfTokenError(Exception): - """The XSRF token is invalid or expired.""" - - -class SiteXsrfSecretKey(db.Model): - """Storage for the sites XSRF secret key. - - There will only be one instance stored of this model, the one used for the - site. - """ - secret = db.StringProperty() - -if ndb is not None: - class SiteXsrfSecretKeyNDB(ndb.Model): - """NDB Model for storage for the sites XSRF secret key. - - Since this model uses the same kind as SiteXsrfSecretKey, it can be used - interchangeably. This simply provides an NDB model for interacting with the - same data the DB model interacts with. - - There should only be one instance stored of this model, the one used for the - site. - """ - secret = ndb.StringProperty() - - @classmethod - def _get_kind(cls): - """Return the kind name for this class.""" - return 'SiteXsrfSecretKey' - - -def _generate_new_xsrf_secret_key(): - """Returns a random XSRF secret key. - """ - return os.urandom(16).encode("hex") - - -def xsrf_secret_key(): - """Return the secret key for use for XSRF protection. - - If the Site entity does not have a secret key, this method will also create - one and persist it. - - Returns: - The secret key. - """ - secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE) - if not secret: - # Load the one and only instance of SiteXsrfSecretKey. - model = SiteXsrfSecretKey.get_or_insert(key_name='site') - if not model.secret: - model.secret = _generate_new_xsrf_secret_key() - model.put() - secret = model.secret - memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE) - - return str(secret) - - -class AppAssertionCredentials(AssertionCredentials): - """Credentials object for App Engine Assertion Grants - - This object will allow an App Engine application to identify itself to Google - and other OAuth 2.0 servers that can verify assertions. It can be used for the - purpose of accessing data stored under an account assigned to the App Engine - application itself. - - This credential does not require a flow to instantiate because it represents - a two legged flow, and therefore has all of the required information to - generate and refresh its own access tokens. - """ - - @util.positional(2) - def __init__(self, scope, **kwargs): - """Constructor for AppAssertionCredentials - - Args: - scope: string or iterable of strings, scope(s) of the credentials being - requested. - """ - self.scope = util.scopes_to_string(scope) - - # Assertion type is no longer used, but still in the parent class signature. - super(AppAssertionCredentials, self).__init__(None) - - @classmethod - def from_json(cls, json): - data = simplejson.loads(json) - return AppAssertionCredentials(data['scope']) - - def _refresh(self, http_request): - """Refreshes the access_token. - - Since the underlying App Engine app_identity implementation does its own - caching we can skip all the storage hoops and just to a refresh using the - API. - - Args: - http_request: callable, a callable that matches the method signature of - httplib2.Http.request, used to make the refresh request. - - Raises: - AccessTokenRefreshError: When the refresh fails. - """ - try: - scopes = self.scope.split() - (token, _) = app_identity.get_access_token(scopes) - except app_identity.Error, e: - raise AccessTokenRefreshError(str(e)) - self.access_token = token - - -class FlowProperty(db.Property): - """App Engine datastore Property for Flow. - - Utility property that allows easy storage and retrieval of an - oauth2client.Flow""" - - # Tell what the user type is. - data_type = Flow - - # For writing to datastore. - def get_value_for_datastore(self, model_instance): - flow = super(FlowProperty, - self).get_value_for_datastore(model_instance) - return db.Blob(pickle.dumps(flow)) - - # For reading from datastore. - def make_value_from_datastore(self, value): - if value is None: - return None - return pickle.loads(value) - - def validate(self, value): - if value is not None and not isinstance(value, Flow): - raise db.BadValueError('Property %s must be convertible ' - 'to a FlowThreeLegged instance (%s)' % - (self.name, value)) - return super(FlowProperty, self).validate(value) - - def empty(self, value): - return not value - - -if ndb is not None: - class FlowNDBProperty(ndb.PickleProperty): - """App Engine NDB datastore Property for Flow. - - Serves the same purpose as the DB FlowProperty, but for NDB models. Since - PickleProperty inherits from BlobProperty, the underlying representation of - the data in the datastore will be the same as in the DB case. - - Utility property that allows easy storage and retrieval of an - oauth2client.Flow - """ - - def _validate(self, value): - """Validates a value as a proper Flow object. - - Args: - value: A value to be set on the property. - - Raises: - TypeError if the value is not an instance of Flow. - """ - logger.info('validate: Got type %s', type(value)) - if value is not None and not isinstance(value, Flow): - raise TypeError('Property %s must be convertible to a flow ' - 'instance; received: %s.' % (self._name, value)) - - -class CredentialsProperty(db.Property): - """App Engine datastore Property for Credentials. - - Utility property that allows easy storage and retrieval of - oath2client.Credentials - """ - - # Tell what the user type is. - data_type = Credentials - - # For writing to datastore. - def get_value_for_datastore(self, model_instance): - logger.info("get: Got type " + str(type(model_instance))) - cred = super(CredentialsProperty, - self).get_value_for_datastore(model_instance) - if cred is None: - cred = '' - else: - cred = cred.to_json() - return db.Blob(cred) - - # For reading from datastore. - def make_value_from_datastore(self, value): - logger.info("make: Got type " + str(type(value))) - if value is None: - return None - if len(value) == 0: - return None - try: - credentials = Credentials.new_from_json(value) - except ValueError: - credentials = None - return credentials - - def validate(self, value): - value = super(CredentialsProperty, self).validate(value) - logger.info("validate: Got type " + str(type(value))) - if value is not None and not isinstance(value, Credentials): - raise db.BadValueError('Property %s must be convertible ' - 'to a Credentials instance (%s)' % - (self.name, value)) - #if value is not None and not isinstance(value, Credentials): - # return None - return value - - -if ndb is not None: - # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials - # and subclass mechanics to use new_from_dict, to_dict, - # from_dict, etc. - class CredentialsNDBProperty(ndb.BlobProperty): - """App Engine NDB datastore Property for Credentials. - - Serves the same purpose as the DB CredentialsProperty, but for NDB models. - Since CredentialsProperty stores data as a blob and this inherits from - BlobProperty, the data in the datastore will be the same as in the DB case. - - Utility property that allows easy storage and retrieval of Credentials and - subclasses. - """ - def _validate(self, value): - """Validates a value as a proper credentials object. - - Args: - value: A value to be set on the property. - - Raises: - TypeError if the value is not an instance of Credentials. - """ - logger.info('validate: Got type %s', type(value)) - if value is not None and not isinstance(value, Credentials): - raise TypeError('Property %s must be convertible to a credentials ' - 'instance; received: %s.' % (self._name, value)) - - def _to_base_type(self, value): - """Converts our validated value to a JSON serialized string. - - Args: - value: A value to be set in the datastore. - - Returns: - A JSON serialized version of the credential, else '' if value is None. - """ - if value is None: - return '' - else: - return value.to_json() - - def _from_base_type(self, value): - """Converts our stored JSON string back to the desired type. - - Args: - value: A value from the datastore to be converted to the desired type. - - Returns: - A deserialized Credentials (or subclass) object, else None if the - value can't be parsed. - """ - if not value: - return None - try: - # Uses the from_json method of the implied class of value - credentials = Credentials.new_from_json(value) - except ValueError: - credentials = None - return credentials - - -class StorageByKeyName(Storage): - """Store and retrieve a credential to and from the App Engine datastore. - - This Storage helper presumes the Credentials have been stored as a - CredentialsProperty or CredentialsNDBProperty on a datastore model class, and - that entities are stored by key_name. - """ - - @util.positional(4) - def __init__(self, model, key_name, property_name, cache=None, user=None): - """Constructor for Storage. - - Args: - model: db.Model or ndb.Model, model class - key_name: string, key name for the entity that has the credentials - property_name: string, name of the property that is a CredentialsProperty - or CredentialsNDBProperty. - cache: memcache, a write-through cache to put in front of the datastore. - If the model you are using is an NDB model, using a cache will be - redundant since the model uses an instance cache and memcache for you. - user: users.User object, optional. Can be used to grab user ID as a - key_name if no key name is specified. - """ - if key_name is None: - if user is None: - raise ValueError('StorageByKeyName called with no key name or user.') - key_name = user.user_id() - - self._model = model - self._key_name = key_name - self._property_name = property_name - self._cache = cache - - def _is_ndb(self): - """Determine whether the model of the instance is an NDB model. - - Returns: - Boolean indicating whether or not the model is an NDB or DB model. - """ - # issubclass will fail if one of the arguments is not a class, only need - # worry about new-style classes since ndb and db models are new-style - if isinstance(self._model, type): - if ndb is not None and issubclass(self._model, ndb.Model): - return True - elif issubclass(self._model, db.Model): - return False - - raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,)) - - def _get_entity(self): - """Retrieve entity from datastore. - - Uses a different model method for db or ndb models. - - Returns: - Instance of the model corresponding to the current storage object - and stored using the key name of the storage object. - """ - if self._is_ndb(): - return self._model.get_by_id(self._key_name) - else: - return self._model.get_by_key_name(self._key_name) - - def _delete_entity(self): - """Delete entity from datastore. - - Attempts to delete using the key_name stored on the object, whether or not - the given key is in the datastore. - """ - if self._is_ndb(): - ndb.Key(self._model, self._key_name).delete() - else: - entity_key = db.Key.from_path(self._model.kind(), self._key_name) - db.delete(entity_key) - - def locked_get(self): - """Retrieve Credential from datastore. - - Returns: - oauth2client.Credentials - """ - credentials = None - if self._cache: - json = self._cache.get(self._key_name) - if json: - credentials = Credentials.new_from_json(json) - if credentials is None: - entity = self._get_entity() - if entity is not None: - credentials = getattr(entity, self._property_name) - if self._cache: - self._cache.set(self._key_name, credentials.to_json()) - - if credentials and hasattr(credentials, 'set_store'): - credentials.set_store(self) - return credentials - - def locked_put(self, credentials): - """Write a Credentials to the datastore. - - Args: - credentials: Credentials, the credentials to store. - """ - entity = self._model.get_or_insert(self._key_name) - setattr(entity, self._property_name, credentials) - entity.put() - if self._cache: - self._cache.set(self._key_name, credentials.to_json()) - - def locked_delete(self): - """Delete Credential from datastore.""" - - if self._cache: - self._cache.delete(self._key_name) - - self._delete_entity() - - -class CredentialsModel(db.Model): - """Storage for OAuth 2.0 Credentials - - Storage of the model is keyed by the user.user_id(). - """ - credentials = CredentialsProperty() - - -if ndb is not None: - class CredentialsNDBModel(ndb.Model): - """NDB Model for storage of OAuth 2.0 Credentials - - Since this model uses the same kind as CredentialsModel and has a property - which can serialize and deserialize Credentials correctly, it can be used - interchangeably with a CredentialsModel to access, insert and delete the - same entities. This simply provides an NDB model for interacting with the - same data the DB model interacts with. - - Storage of the model is keyed by the user.user_id(). - """ - credentials = CredentialsNDBProperty() - - @classmethod - def _get_kind(cls): - """Return the kind name for this class.""" - return 'CredentialsModel' - - -def _build_state_value(request_handler, user): - """Composes the value for the 'state' parameter. - - Packs the current request URI and an XSRF token into an opaque string that - can be passed to the authentication server via the 'state' parameter. - - Args: - request_handler: webapp.RequestHandler, The request. - user: google.appengine.api.users.User, The current user. - - Returns: - The state value as a string. - """ - uri = request_handler.request.url - token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(), - action_id=str(uri)) - return uri + ':' + token - - -def _parse_state_value(state, user): - """Parse the value of the 'state' parameter. - - Parses the value and validates the XSRF token in the state parameter. - - Args: - state: string, The value of the state parameter. - user: google.appengine.api.users.User, The current user. - - Raises: - InvalidXsrfTokenError: if the XSRF token is invalid. - - Returns: - The redirect URI. - """ - uri, token = state.rsplit(':', 1) - if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(), - action_id=uri): - raise InvalidXsrfTokenError() - - return uri - - -class OAuth2Decorator(object): - """Utility for making OAuth 2.0 easier. - - Instantiate and then use with oauth_required or oauth_aware - as decorators on webapp.RequestHandler methods. - - Example: - - decorator = OAuth2Decorator( - client_id='837...ent.com', - client_secret='Qh...wwI', - scope='https://www.googleapis.com/auth/plus') - - - class MainHandler(webapp.RequestHandler): - - @decorator.oauth_required - def get(self): - http = decorator.http() - # http is authorized with the user's Credentials and can be used - # in API calls - - """ - - def set_credentials(self, credentials): - self._tls.credentials = credentials - - def get_credentials(self): - """A thread local Credentials object. - - Returns: - A client.Credentials object, or None if credentials hasn't been set in - this thread yet, which may happen when calling has_credentials inside - oauth_aware. - """ - return getattr(self._tls, 'credentials', None) - - credentials = property(get_credentials, set_credentials) - - def set_flow(self, flow): - self._tls.flow = flow - - def get_flow(self): - """A thread local Flow object. - - Returns: - A credentials.Flow object, or None if the flow hasn't been set in this - thread yet, which happens in _create_flow() since Flows are created - lazily. - """ - return getattr(self._tls, 'flow', None) - - flow = property(get_flow, set_flow) - - - @util.positional(4) - def __init__(self, client_id, client_secret, scope, - auth_uri=GOOGLE_AUTH_URI, - token_uri=GOOGLE_TOKEN_URI, - revoke_uri=GOOGLE_REVOKE_URI, - user_agent=None, - message=None, - callback_path='/oauth2callback', - token_response_param=None, - _storage_class=StorageByKeyName, - _credentials_class=CredentialsModel, - _credentials_property_name='credentials', - **kwargs): - - """Constructor for OAuth2Decorator - - Args: - client_id: string, client identifier. - client_secret: string client secret. - scope: string or iterable of strings, scope(s) of the credentials being - requested. - auth_uri: string, URI for authorization endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - token_uri: string, URI for token endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - revoke_uri: string, URI for revoke endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - user_agent: string, User agent of your application, default to None. - message: Message to display if there are problems with the OAuth 2.0 - configuration. The message may contain HTML and will be presented on the - web interface for any method that uses the decorator. - callback_path: string, The absolute path to use as the callback URI. Note - that this must match up with the URI given when registering the - application in the APIs Console. - token_response_param: string. If provided, the full JSON response - to the access token request will be encoded and included in this query - parameter in the callback URI. This is useful with providers (e.g. - wordpress.com) that include extra fields that the client may want. - _storage_class: "Protected" keyword argument not typically provided to - this constructor. A storage class to aid in storing a Credentials object - for a user in the datastore. Defaults to StorageByKeyName. - _credentials_class: "Protected" keyword argument not typically provided to - this constructor. A db or ndb Model class to hold credentials. Defaults - to CredentialsModel. - _credentials_property_name: "Protected" keyword argument not typically - provided to this constructor. A string indicating the name of the field - on the _credentials_class where a Credentials object will be stored. - Defaults to 'credentials'. - **kwargs: dict, Keyword arguments are be passed along as kwargs to the - OAuth2WebServerFlow constructor. - """ - self._tls = threading.local() - self.flow = None - self.credentials = None - self._client_id = client_id - self._client_secret = client_secret - self._scope = util.scopes_to_string(scope) - self._auth_uri = auth_uri - self._token_uri = token_uri - self._revoke_uri = revoke_uri - self._user_agent = user_agent - self._kwargs = kwargs - self._message = message - self._in_error = False - self._callback_path = callback_path - self._token_response_param = token_response_param - self._storage_class = _storage_class - self._credentials_class = _credentials_class - self._credentials_property_name = _credentials_property_name - - def _display_error_message(self, request_handler): - request_handler.response.out.write('') - request_handler.response.out.write(_safe_html(self._message)) - request_handler.response.out.write('') - - def oauth_required(self, method): - """Decorator that starts the OAuth 2.0 dance. - - Starts the OAuth dance for the logged in user if they haven't already - granted access for this application. - - Args: - method: callable, to be decorated method of a webapp.RequestHandler - instance. - """ - - def check_oauth(request_handler, *args, **kwargs): - if self._in_error: - self._display_error_message(request_handler) - return - - user = users.get_current_user() - # Don't use @login_decorator as this could be used in a POST request. - if not user: - request_handler.redirect(users.create_login_url( - request_handler.request.uri)) - return - - self._create_flow(request_handler) - - # Store the request URI in 'state' so we can use it later - self.flow.params['state'] = _build_state_value(request_handler, user) - self.credentials = self._storage_class( - self._credentials_class, None, - self._credentials_property_name, user=user).get() - - if not self.has_credentials(): - return request_handler.redirect(self.authorize_url()) - try: - resp = method(request_handler, *args, **kwargs) - except AccessTokenRefreshError: - return request_handler.redirect(self.authorize_url()) - finally: - self.credentials = None - return resp - - return check_oauth - - def _create_flow(self, request_handler): - """Create the Flow object. - - The Flow is calculated lazily since we don't know where this app is - running until it receives a request, at which point redirect_uri can be - calculated and then the Flow object can be constructed. - - Args: - request_handler: webapp.RequestHandler, the request handler. - """ - if self.flow is None: - redirect_uri = request_handler.request.relative_url( - self._callback_path) # Usually /oauth2callback - self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret, - self._scope, redirect_uri=redirect_uri, - user_agent=self._user_agent, - auth_uri=self._auth_uri, - token_uri=self._token_uri, - revoke_uri=self._revoke_uri, - **self._kwargs) - - def oauth_aware(self, method): - """Decorator that sets up for OAuth 2.0 dance, but doesn't do it. - - Does all the setup for the OAuth dance, but doesn't initiate it. - This decorator is useful if you want to create a page that knows - whether or not the user has granted access to this application. - From within a method decorated with @oauth_aware the has_credentials() - and authorize_url() methods can be called. - - Args: - method: callable, to be decorated method of a webapp.RequestHandler - instance. - """ - - def setup_oauth(request_handler, *args, **kwargs): - if self._in_error: - self._display_error_message(request_handler) - return - - user = users.get_current_user() - # Don't use @login_decorator as this could be used in a POST request. - if not user: - request_handler.redirect(users.create_login_url( - request_handler.request.uri)) - return - - self._create_flow(request_handler) - - self.flow.params['state'] = _build_state_value(request_handler, user) - self.credentials = self._storage_class( - self._credentials_class, None, - self._credentials_property_name, user=user).get() - try: - resp = method(request_handler, *args, **kwargs) - finally: - self.credentials = None - return resp - return setup_oauth - - - def has_credentials(self): - """True if for the logged in user there are valid access Credentials. - - Must only be called from with a webapp.RequestHandler subclassed method - that had been decorated with either @oauth_required or @oauth_aware. - """ - return self.credentials is not None and not self.credentials.invalid - - def authorize_url(self): - """Returns the URL to start the OAuth dance. - - Must only be called from with a webapp.RequestHandler subclassed method - that had been decorated with either @oauth_required or @oauth_aware. - """ - url = self.flow.step1_get_authorize_url() - return str(url) - - def http(self): - """Returns an authorized http instance. - - Must only be called from within an @oauth_required decorated method, or - from within an @oauth_aware decorated method where has_credentials() - returns True. - """ - return self.credentials.authorize(httplib2.Http()) - - @property - def callback_path(self): - """The absolute path where the callback will occur. - - Note this is the absolute path, not the absolute URI, that will be - calculated by the decorator at runtime. See callback_handler() for how this - should be used. - - Returns: - The callback path as a string. - """ - return self._callback_path - - - def callback_handler(self): - """RequestHandler for the OAuth 2.0 redirect callback. - - Usage: - app = webapp.WSGIApplication([ - ('/index', MyIndexHandler), - ..., - (decorator.callback_path, decorator.callback_handler()) - ]) - - Returns: - A webapp.RequestHandler that handles the redirect back from the - server during the OAuth 2.0 dance. - """ - decorator = self - - class OAuth2Handler(webapp.RequestHandler): - """Handler for the redirect_uri of the OAuth 2.0 dance.""" - - @login_required - def get(self): - error = self.request.get('error') - if error: - errormsg = self.request.get('error_description', error) - self.response.out.write( - 'The authorization request failed: %s' % _safe_html(errormsg)) - else: - user = users.get_current_user() - decorator._create_flow(self) - credentials = decorator.flow.step2_exchange(self.request.params) - decorator._storage_class( - decorator._credentials_class, None, - decorator._credentials_property_name, user=user).put(credentials) - redirect_uri = _parse_state_value(str(self.request.get('state')), - user) - - if decorator._token_response_param and credentials.token_response: - resp_json = simplejson.dumps(credentials.token_response) - redirect_uri = util._add_query_parameter( - redirect_uri, decorator._token_response_param, resp_json) - - self.redirect(redirect_uri) - - return OAuth2Handler - - def callback_application(self): - """WSGI application for handling the OAuth 2.0 redirect callback. - - If you need finer grained control use `callback_handler` which returns just - the webapp.RequestHandler. - - Returns: - A webapp.WSGIApplication that handles the redirect back from the - server during the OAuth 2.0 dance. - """ - return webapp.WSGIApplication([ - (self.callback_path, self.callback_handler()) - ]) - - -class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): - """An OAuth2Decorator that builds from a clientsecrets file. - - Uses a clientsecrets file as the source for all the information when - constructing an OAuth2Decorator. - - Example: - - decorator = OAuth2DecoratorFromClientSecrets( - os.path.join(os.path.dirname(__file__), 'client_secrets.json') - scope='https://www.googleapis.com/auth/plus') - - - class MainHandler(webapp.RequestHandler): - - @decorator.oauth_required - def get(self): - http = decorator.http() - # http is authorized with the user's Credentials and can be used - # in API calls - """ - - @util.positional(3) - def __init__(self, filename, scope, message=None, cache=None): - """Constructor - - Args: - filename: string, File name of client secrets. - scope: string or iterable of strings, scope(s) of the credentials being - requested. - message: string, A friendly string to display to the user if the - clientsecrets file is missing or invalid. The message may contain HTML - and will be presented on the web interface for any method that uses the - decorator. - cache: An optional cache service client that implements get() and set() - methods. See clientsecrets.loadfile() for details. - """ - client_type, client_info = clientsecrets.loadfile(filename, cache=cache) - if client_type not in [ - clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: - raise InvalidClientSecretsError( - 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.') - constructor_kwargs = { - 'auth_uri': client_info['auth_uri'], - 'token_uri': client_info['token_uri'], - 'message': message, - } - revoke_uri = client_info.get('revoke_uri') - if revoke_uri is not None: - constructor_kwargs['revoke_uri'] = revoke_uri - super(OAuth2DecoratorFromClientSecrets, self).__init__( - client_info['client_id'], client_info['client_secret'], - scope, **constructor_kwargs) - if message is not None: - self._message = message - else: - self._message = 'Please configure your application for OAuth 2.0.' - - -@util.positional(2) -def oauth2decorator_from_clientsecrets(filename, scope, - message=None, cache=None): - """Creates an OAuth2Decorator populated from a clientsecrets file. - - Args: - filename: string, File name of client secrets. - scope: string or list of strings, scope(s) of the credentials being - requested. - message: string, A friendly string to display to the user if the - clientsecrets file is missing or invalid. The message may contain HTML and - will be presented on the web interface for any method that uses the - decorator. - cache: An optional cache service client that implements get() and set() - methods. See clientsecrets.loadfile() for details. - - Returns: An OAuth2Decorator - - """ - return OAuth2DecoratorFromClientSecrets(filename, scope, - message=message, cache=cache) diff --git a/third_party/oauth2client/client.py b/third_party/oauth2client/client.py deleted file mode 100644 index 6901f3f381..0000000000 --- a/third_party/oauth2client/client.py +++ /dev/null @@ -1,1363 +0,0 @@ -# Copyright (C) 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""An OAuth 2.0 client. - -Tools for interacting with OAuth 2.0 protected resources. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import base64 -import clientsecrets -import copy -import datetime -from .. import httplib2 -import logging -import sys -import time -import urllib -import urlparse - -from . import GOOGLE_AUTH_URI -from . import GOOGLE_REVOKE_URI -from . import GOOGLE_TOKEN_URI -from . import util -from .anyjson import simplejson - -HAS_OPENSSL = False -HAS_CRYPTO = False -try: - from . import crypt - HAS_CRYPTO = True - if crypt.OpenSSLVerifier is not None: - HAS_OPENSSL = True -except ImportError: - pass - -try: - from urlparse import parse_qsl -except ImportError: - from cgi import parse_qsl - -logger = logging.getLogger(__name__) - -# Expiry is stored in RFC3339 UTC format -EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' - -# Which certs to use to validate id_tokens received. -ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' - -# Constant to use for the out of band OAuth 2.0 flow. -OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' - -# Google Data client libraries may need to set this to [401, 403]. -REFRESH_STATUS_CODES = [401] - - -class Error(Exception): - """Base error for this module.""" - - -class FlowExchangeError(Error): - """Error trying to exchange an authorization grant for an access token.""" - - -class AccessTokenRefreshError(Error): - """Error trying to refresh an expired access token.""" - - -class TokenRevokeError(Error): - """Error trying to revoke a token.""" - - -class UnknownClientSecretsFlowError(Error): - """The client secrets file called for an unknown type of OAuth 2.0 flow. """ - - -class AccessTokenCredentialsError(Error): - """Having only the access_token means no refresh is possible.""" - - -class VerifyJwtTokenError(Error): - """Could on retrieve certificates for validation.""" - - -class NonAsciiHeaderError(Error): - """Header names and values must be ASCII strings.""" - - -def _abstract(): - raise NotImplementedError('You need to override this function') - - -class MemoryCache(object): - """httplib2 Cache implementation which only caches locally.""" - - def __init__(self): - self.cache = {} - - def get(self, key): - return self.cache.get(key) - - def set(self, key, value): - self.cache[key] = value - - def delete(self, key): - self.cache.pop(key, None) - - -class Credentials(object): - """Base class for all Credentials objects. - - Subclasses must define an authorize() method that applies the credentials to - an HTTP transport. - - Subclasses must also specify a classmethod named 'from_json' that takes a JSON - string as input and returns an instaniated Credentials object. - """ - - NON_SERIALIZED_MEMBERS = ['store'] - - def authorize(self, http): - """Take an httplib2.Http instance (or equivalent) and authorizes it. - - Authorizes it for the set of credentials, usually by replacing - http.request() with a method that adds in the appropriate headers and then - delegates to the original Http.request() method. - - Args: - http: httplib2.Http, an http object to be used to make the refresh - request. - """ - _abstract() - - def refresh(self, http): - """Forces a refresh of the access_token. - - Args: - http: httplib2.Http, an http object to be used to make the refresh - request. - """ - _abstract() - - def revoke(self, http): - """Revokes a refresh_token and makes the credentials void. - - Args: - http: httplib2.Http, an http object to be used to make the revoke - request. - """ - _abstract() - - def apply(self, headers): - """Add the authorization to the headers. - - Args: - headers: dict, the headers to add the Authorization header to. - """ - _abstract() - - def _to_json(self, strip): - """Utility function that creates JSON repr. of a Credentials object. - - Args: - strip: array, An array of names of members to not include in the JSON. - - Returns: - string, a JSON representation of this instance, suitable to pass to - from_json(). - """ - t = type(self) - d = copy.copy(self.__dict__) - for member in strip: - if member in d: - del d[member] - if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime): - d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) - # Add in information we will need later to reconsistitue this instance. - d['_class'] = t.__name__ - d['_module'] = t.__module__ - return simplejson.dumps(d) - - def to_json(self): - """Creating a JSON representation of an instance of Credentials. - - Returns: - string, a JSON representation of this instance, suitable to pass to - from_json(). - """ - return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) - - @classmethod - def new_from_json(cls, s): - """Utility class method to instantiate a Credentials subclass from a JSON - representation produced by to_json(). - - Args: - s: string, JSON from to_json(). - - Returns: - An instance of the subclass of Credentials that was serialized with - to_json(). - """ - data = simplejson.loads(s) - # Find and call the right classmethod from_json() to restore the object. - module = data['_module'] - try: - m = __import__(module) - except ImportError: - # In case there's an object from the old package structure, update it - module = module.replace('.apiclient', '') - m = __import__(module) - - m = __import__(module, fromlist=module.split('.')[:-1]) - kls = getattr(m, data['_class']) - from_json = getattr(kls, 'from_json') - return from_json(s) - - @classmethod - def from_json(cls, s): - """Instantiate a Credentials object from a JSON description of it. - - The JSON should have been produced by calling .to_json() on the object. - - Args: - data: dict, A deserialized JSON object. - - Returns: - An instance of a Credentials subclass. - """ - return Credentials() - - -class Flow(object): - """Base class for all Flow objects.""" - pass - - -class Storage(object): - """Base class for all Storage objects. - - Store and retrieve a single credential. This class supports locking - such that multiple processes and threads can operate on a single - store. - """ - - def acquire_lock(self): - """Acquires any lock necessary to access this Storage. - - This lock is not reentrant. - """ - pass - - def release_lock(self): - """Release the Storage lock. - - Trying to release a lock that isn't held will result in a - RuntimeError. - """ - pass - - def locked_get(self): - """Retrieve credential. - - The Storage lock must be held when this is called. - - Returns: - oauth2client.client.Credentials - """ - _abstract() - - def locked_put(self, credentials): - """Write a credential. - - The Storage lock must be held when this is called. - - Args: - credentials: Credentials, the credentials to store. - """ - _abstract() - - def locked_delete(self): - """Delete a credential. - - The Storage lock must be held when this is called. - """ - _abstract() - - def get(self): - """Retrieve credential. - - The Storage lock must *not* be held when this is called. - - Returns: - oauth2client.client.Credentials - """ - self.acquire_lock() - try: - return self.locked_get() - finally: - self.release_lock() - - def put(self, credentials): - """Write a credential. - - The Storage lock must be held when this is called. - - Args: - credentials: Credentials, the credentials to store. - """ - self.acquire_lock() - try: - self.locked_put(credentials) - finally: - self.release_lock() - - def delete(self): - """Delete credential. - - Frees any resources associated with storing the credential. - The Storage lock must *not* be held when this is called. - - Returns: - None - """ - self.acquire_lock() - try: - return self.locked_delete() - finally: - self.release_lock() - - -def clean_headers(headers): - """Forces header keys and values to be strings, i.e not unicode. - - The httplib module just concats the header keys and values in a way that may - make the message header a unicode string, which, if it then tries to - contatenate to a binary request body may result in a unicode decode error. - - Args: - headers: dict, A dictionary of headers. - - Returns: - The same dictionary but with all the keys converted to strings. - """ - clean = {} - try: - for k, v in headers.iteritems(): - clean[str(k)] = str(v) - except UnicodeEncodeError: - raise NonAsciiHeaderError(k + ': ' + v) - return clean - - -def _update_query_params(uri, params): - """Updates a URI with new query parameters. - - Args: - uri: string, A valid URI, with potential existing query parameters. - params: dict, A dictionary of query parameters. - - Returns: - The same URI but with the new query parameters added. - """ - parts = list(urlparse.urlparse(uri)) - query_params = dict(parse_qsl(parts[4])) # 4 is the index of the query part - query_params.update(params) - parts[4] = urllib.urlencode(query_params) - return urlparse.urlunparse(parts) - - -class OAuth2Credentials(Credentials): - """Credentials object for OAuth 2.0. - - Credentials can be applied to an httplib2.Http object using the authorize() - method, which then adds the OAuth 2.0 access token to each request. - - OAuth2Credentials objects may be safely pickled and unpickled. - """ - - @util.positional(8) - def __init__(self, access_token, client_id, client_secret, refresh_token, - token_expiry, token_uri, user_agent, revoke_uri=None, - id_token=None, token_response=None): - """Create an instance of OAuth2Credentials. - - This constructor is not usually called by the user, instead - OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow. - - Args: - access_token: string, access token. - client_id: string, client identifier. - client_secret: string, client secret. - refresh_token: string, refresh token. - token_expiry: datetime, when the access_token expires. - token_uri: string, URI of token endpoint. - user_agent: string, The HTTP User-Agent to provide for this application. - revoke_uri: string, URI for revoke endpoint. Defaults to None; a token - can't be revoked if this is None. - id_token: object, The identity of the resource owner. - token_response: dict, the decoded response to the token request. None - if a token hasn't been requested yet. Stored because some providers - (e.g. wordpress.com) include extra fields that clients may want. - - Notes: - store: callable, A callable that when passed a Credential - will store the credential back to where it came from. - This is needed to store the latest access_token if it - has expired and been refreshed. - """ - self.access_token = access_token - self.client_id = client_id - self.client_secret = client_secret - self.refresh_token = refresh_token - self.store = None - self.token_expiry = token_expiry - self.token_uri = token_uri - self.user_agent = user_agent - self.revoke_uri = revoke_uri - self.id_token = id_token - self.token_response = token_response - - # True if the credentials have been revoked or expired and can't be - # refreshed. - self.invalid = False - - def authorize(self, http): - """Authorize an httplib2.Http instance with these credentials. - - The modified http.request method will add authentication headers to each - request and will refresh access_tokens when a 401 is received on a - request. In addition the http.request method has a credentials property, - http.request.credentials, which is the Credentials object that authorized - it. - - Args: - http: An instance of httplib2.Http - or something that acts like it. - - Returns: - A modified instance of http that was passed in. - - Example: - - h = httplib2.Http() - h = credentials.authorize(h) - - You can't create a new OAuth subclass of httplib2.Authenication - because it never gets passed the absolute URI, which is needed for - signing. So instead we have to overload 'request' with a closure - that adds in the Authorization header and then calls the original - version of 'request()'. - """ - request_orig = http.request - - # The closure that will replace 'httplib2.Http.request'. - @util.positional(1) - def new_request(uri, method='GET', body=None, headers=None, - redirections=httplib2.DEFAULT_MAX_REDIRECTS, - connection_type=None): - if not self.access_token: - logger.info('Attempting refresh to obtain initial access_token') - self._refresh(request_orig) - - # Modify the request headers to add the appropriate - # Authorization header. - if headers is None: - headers = {} - self.apply(headers) - - if self.user_agent is not None: - if 'user-agent' in headers: - headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] - else: - headers['user-agent'] = self.user_agent - - resp, content = request_orig(uri, method, body, clean_headers(headers), - redirections, connection_type) - - if resp.status in REFRESH_STATUS_CODES: - logger.info('Refreshing due to a %s' % str(resp.status)) - self._refresh(request_orig) - self.apply(headers) - return request_orig(uri, method, body, clean_headers(headers), - redirections, connection_type) - else: - return (resp, content) - - # Replace the request method with our own closure. - http.request = new_request - - # Set credentials as a property of the request method. - setattr(http.request, 'credentials', self) - - return http - - def refresh(self, http): - """Forces a refresh of the access_token. - - Args: - http: httplib2.Http, an http object to be used to make the refresh - request. - """ - self._refresh(http.request) - - def revoke(self, http): - """Revokes a refresh_token and makes the credentials void. - - Args: - http: httplib2.Http, an http object to be used to make the revoke - request. - """ - self._revoke(http.request) - - def apply(self, headers): - """Add the authorization to the headers. - - Args: - headers: dict, the headers to add the Authorization header to. - """ - headers['Authorization'] = 'Bearer ' + self.access_token - - def to_json(self): - return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) - - @classmethod - def from_json(cls, s): - """Instantiate a Credentials object from a JSON description of it. The JSON - should have been produced by calling .to_json() on the object. - - Args: - data: dict, A deserialized JSON object. - - Returns: - An instance of a Credentials subclass. - """ - data = simplejson.loads(s) - if 'token_expiry' in data and not isinstance(data['token_expiry'], - datetime.datetime): - try: - data['token_expiry'] = datetime.datetime.strptime( - data['token_expiry'], EXPIRY_FORMAT) - except: - data['token_expiry'] = None - retval = cls( - data['access_token'], - data['client_id'], - data['client_secret'], - data['refresh_token'], - data['token_expiry'], - data['token_uri'], - data['user_agent'], - revoke_uri=data.get('revoke_uri', None), - id_token=data.get('id_token', None), - token_response=data.get('token_response', None)) - retval.invalid = data['invalid'] - return retval - - @property - def access_token_expired(self): - """True if the credential is expired or invalid. - - If the token_expiry isn't set, we assume the token doesn't expire. - """ - if self.invalid: - return True - - if not self.token_expiry: - return False - - now = datetime.datetime.utcnow() - if now >= self.token_expiry: - logger.info('access_token is expired. Now: %s, token_expiry: %s', - now, self.token_expiry) - return True - return False - - def set_store(self, store): - """Set the Storage for the credential. - - Args: - store: Storage, an implementation of Stroage object. - This is needed to store the latest access_token if it - has expired and been refreshed. This implementation uses - locking to check for updates before updating the - access_token. - """ - self.store = store - - def _updateFromCredential(self, other): - """Update this Credential from another instance.""" - self.__dict__.update(other.__getstate__()) - - def __getstate__(self): - """Trim the state down to something that can be pickled.""" - d = copy.copy(self.__dict__) - del d['store'] - return d - - def __setstate__(self, state): - """Reconstitute the state of the object from being pickled.""" - self.__dict__.update(state) - self.store = None - - def _generate_refresh_request_body(self): - """Generate the body that will be used in the refresh request.""" - body = urllib.urlencode({ - 'grant_type': 'refresh_token', - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'refresh_token': self.refresh_token, - }) - return body - - def _generate_refresh_request_headers(self): - """Generate the headers that will be used in the refresh request.""" - headers = { - 'content-type': 'application/x-www-form-urlencoded', - } - - if self.user_agent is not None: - headers['user-agent'] = self.user_agent - - return headers - - def _refresh(self, http_request): - """Refreshes the access_token. - - This method first checks by reading the Storage object if available. - If a refresh is still needed, it holds the Storage lock until the - refresh is completed. - - Args: - http_request: callable, a callable that matches the method signature of - httplib2.Http.request, used to make the refresh request. - - Raises: - AccessTokenRefreshError: When the refresh fails. - """ - if not self.store: - self._do_refresh_request(http_request) - else: - self.store.acquire_lock() - try: - new_cred = self.store.locked_get() - if (new_cred and not new_cred.invalid and - new_cred.access_token != self.access_token): - logger.info('Updated access_token read from Storage') - self._updateFromCredential(new_cred) - else: - self._do_refresh_request(http_request) - finally: - self.store.release_lock() - - def _do_refresh_request(self, http_request): - """Refresh the access_token using the refresh_token. - - Args: - http_request: callable, a callable that matches the method signature of - httplib2.Http.request, used to make the refresh request. - - Raises: - AccessTokenRefreshError: When the refresh fails. - """ - body = self._generate_refresh_request_body() - headers = self._generate_refresh_request_headers() - - logger.info('Refreshing access_token') - resp, content = http_request( - self.token_uri, method='POST', body=body, headers=headers) - if resp.status == 200: - # TODO(jcgregorio) Raise an error if loads fails? - d = simplejson.loads(content) - self.token_response = d - self.access_token = d['access_token'] - self.refresh_token = d.get('refresh_token', self.refresh_token) - if 'expires_in' in d: - self.token_expiry = datetime.timedelta( - seconds=int(d['expires_in'])) + datetime.datetime.utcnow() - else: - self.token_expiry = None - if self.store: - self.store.locked_put(self) - else: - # An {'error':...} response body means the token is expired or revoked, - # so we flag the credentials as such. - logger.info('Failed to retrieve access token: %s' % content) - error_msg = 'Invalid response %s.' % resp['status'] - try: - d = simplejson.loads(content) - if 'error' in d: - error_msg = d['error'] - self.invalid = True - if self.store: - self.store.locked_put(self) - except StandardError: - pass - raise AccessTokenRefreshError(error_msg) - - def _revoke(self, http_request): - """Revokes the refresh_token and deletes the store if available. - - Args: - http_request: callable, a callable that matches the method signature of - httplib2.Http.request, used to make the revoke request. - """ - self._do_revoke(http_request, self.refresh_token) - - def _do_revoke(self, http_request, token): - """Revokes the credentials and deletes the store if available. - - Args: - http_request: callable, a callable that matches the method signature of - httplib2.Http.request, used to make the refresh request. - token: A string used as the token to be revoked. Can be either an - access_token or refresh_token. - - Raises: - TokenRevokeError: If the revoke request does not return with a 200 OK. - """ - logger.info('Revoking token') - query_params = {'token': token} - token_revoke_uri = _update_query_params(self.revoke_uri, query_params) - resp, content = http_request(token_revoke_uri) - if resp.status == 200: - self.invalid = True - else: - error_msg = 'Invalid response %s.' % resp.status - try: - d = simplejson.loads(content) - if 'error' in d: - error_msg = d['error'] - except StandardError: - pass - raise TokenRevokeError(error_msg) - - if self.store: - self.store.delete() - - -class AccessTokenCredentials(OAuth2Credentials): - """Credentials object for OAuth 2.0. - - Credentials can be applied to an httplib2.Http object using the - authorize() method, which then signs each request from that object - with the OAuth 2.0 access token. This set of credentials is for the - use case where you have acquired an OAuth 2.0 access_token from - another place such as a JavaScript client or another web - application, and wish to use it from Python. Because only the - access_token is present it can not be refreshed and will in time - expire. - - AccessTokenCredentials objects may be safely pickled and unpickled. - - Usage: - credentials = AccessTokenCredentials('', - 'my-user-agent/1.0') - http = httplib2.Http() - http = credentials.authorize(http) - - Exceptions: - AccessTokenCredentialsExpired: raised when the access_token expires or is - revoked. - """ - - def __init__(self, access_token, user_agent, revoke_uri=None): - """Create an instance of OAuth2Credentials - - This is one of the few types if Credentials that you should contrust, - Credentials objects are usually instantiated by a Flow. - - Args: - access_token: string, access token. - user_agent: string, The HTTP User-Agent to provide for this application. - revoke_uri: string, URI for revoke endpoint. Defaults to None; a token - can't be revoked if this is None. - """ - super(AccessTokenCredentials, self).__init__( - access_token, - None, - None, - None, - None, - None, - user_agent, - revoke_uri=revoke_uri) - - - @classmethod - def from_json(cls, s): - data = simplejson.loads(s) - retval = AccessTokenCredentials( - data['access_token'], - data['user_agent']) - return retval - - def _refresh(self, http_request): - raise AccessTokenCredentialsError( - 'The access_token is expired or invalid and can\'t be refreshed.') - - def _revoke(self, http_request): - """Revokes the access_token and deletes the store if available. - - Args: - http_request: callable, a callable that matches the method signature of - httplib2.Http.request, used to make the revoke request. - """ - self._do_revoke(http_request, self.access_token) - - -class AssertionCredentials(OAuth2Credentials): - """Abstract Credentials object used for OAuth 2.0 assertion grants. - - This credential does not require a flow to instantiate because it - represents a two legged flow, and therefore has all of the required - information to generate and refresh its own access tokens. It must - be subclassed to generate the appropriate assertion string. - - AssertionCredentials objects may be safely pickled and unpickled. - """ - - @util.positional(2) - def __init__(self, assertion_type, user_agent=None, - token_uri=GOOGLE_TOKEN_URI, - revoke_uri=GOOGLE_REVOKE_URI, - **unused_kwargs): - """Constructor for AssertionFlowCredentials. - - Args: - assertion_type: string, assertion type that will be declared to the auth - server - user_agent: string, The HTTP User-Agent to provide for this application. - token_uri: string, URI for token endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - revoke_uri: string, URI for revoke endpoint. - """ - super(AssertionCredentials, self).__init__( - None, - None, - None, - None, - None, - token_uri, - user_agent, - revoke_uri=revoke_uri) - self.assertion_type = assertion_type - - def _generate_refresh_request_body(self): - assertion = self._generate_assertion() - - body = urllib.urlencode({ - 'assertion': assertion, - 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', - }) - - return body - - def _generate_assertion(self): - """Generate the assertion string that will be used in the access token - request. - """ - _abstract() - - def _revoke(self, http_request): - """Revokes the access_token and deletes the store if available. - - Args: - http_request: callable, a callable that matches the method signature of - httplib2.Http.request, used to make the revoke request. - """ - self._do_revoke(http_request, self.access_token) - - -if HAS_CRYPTO: - # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is - # missing then don't create the SignedJwtAssertionCredentials or the - # verify_id_token() method. - - class SignedJwtAssertionCredentials(AssertionCredentials): - """Credentials object used for OAuth 2.0 Signed JWT assertion grants. - - This credential does not require a flow to instantiate because it represents - a two legged flow, and therefore has all of the required information to - generate and refresh its own access tokens. - - SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or - later. For App Engine you may also consider using AppAssertionCredentials. - """ - - MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds - - @util.positional(4) - def __init__(self, - service_account_name, - private_key, - scope, - private_key_password='notasecret', - user_agent=None, - token_uri=GOOGLE_TOKEN_URI, - revoke_uri=GOOGLE_REVOKE_URI, - **kwargs): - """Constructor for SignedJwtAssertionCredentials. - - Args: - service_account_name: string, id for account, usually an email address. - private_key: string, private key in PKCS12 or PEM format. - scope: string or iterable of strings, scope(s) of the credentials being - requested. - private_key_password: string, password for private_key, unused if - private_key is in PEM format. - user_agent: string, HTTP User-Agent to provide for this application. - token_uri: string, URI for token endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - revoke_uri: string, URI for revoke endpoint. - kwargs: kwargs, Additional parameters to add to the JWT token, for - example sub=joe@xample.org.""" - - super(SignedJwtAssertionCredentials, self).__init__( - None, - user_agent=user_agent, - token_uri=token_uri, - revoke_uri=revoke_uri, - ) - - self.scope = util.scopes_to_string(scope) - - # Keep base64 encoded so it can be stored in JSON. - self.private_key = base64.b64encode(private_key) - - self.private_key_password = private_key_password - self.service_account_name = service_account_name - self.kwargs = kwargs - - @classmethod - def from_json(cls, s): - data = simplejson.loads(s) - retval = SignedJwtAssertionCredentials( - data['service_account_name'], - base64.b64decode(data['private_key']), - data['scope'], - private_key_password=data['private_key_password'], - user_agent=data['user_agent'], - token_uri=data['token_uri'], - **data['kwargs'] - ) - retval.invalid = data['invalid'] - retval.access_token = data['access_token'] - return retval - - def _generate_assertion(self): - """Generate the assertion that will be used in the request.""" - now = long(time.time()) - payload = { - 'aud': self.token_uri, - 'scope': self.scope, - 'iat': now, - 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, - 'iss': self.service_account_name - } - payload.update(self.kwargs) - logger.debug(str(payload)) - - private_key = base64.b64decode(self.private_key) - return crypt.make_signed_jwt(crypt.Signer.from_string( - private_key, self.private_key_password), payload) - - # Only used in verify_id_token(), which is always calling to the same URI - # for the certs. - _cached_http = httplib2.Http(MemoryCache()) - - @util.positional(2) - def verify_id_token(id_token, audience, http=None, - cert_uri=ID_TOKEN_VERIFICATON_CERTS): - """Verifies a signed JWT id_token. - - This function requires PyOpenSSL and because of that it does not work on - App Engine. - - Args: - id_token: string, A Signed JWT. - audience: string, The audience 'aud' that the token should be for. - http: httplib2.Http, instance to use to make the HTTP request. Callers - should supply an instance that has caching enabled. - cert_uri: string, URI of the certificates in JSON format to - verify the JWT against. - - Returns: - The deserialized JSON in the JWT. - - Raises: - oauth2client.crypt.AppIdentityError if the JWT fails to verify. - """ - if http is None: - http = _cached_http - - resp, content = http.request(cert_uri) - - if resp.status == 200: - certs = simplejson.loads(content) - return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) - else: - raise VerifyJwtTokenError('Status code: %d' % resp.status) - - -def _urlsafe_b64decode(b64string): - # Guard against unicode strings, which base64 can't handle. - b64string = b64string.encode('ascii') - padded = b64string + '=' * (4 - len(b64string) % 4) - return base64.urlsafe_b64decode(padded) - - -def _extract_id_token(id_token): - """Extract the JSON payload from a JWT. - - Does the extraction w/o checking the signature. - - Args: - id_token: string, OAuth 2.0 id_token. - - Returns: - object, The deserialized JSON payload. - """ - segments = id_token.split('.') - - if (len(segments) != 3): - raise VerifyJwtTokenError( - 'Wrong number of segments in token: %s' % id_token) - - return simplejson.loads(_urlsafe_b64decode(segments[1])) - - -def _parse_exchange_token_response(content): - """Parses response of an exchange token request. - - Most providers return JSON but some (e.g. Facebook) return a - url-encoded string. - - Args: - content: The body of a response - - Returns: - Content as a dictionary object. Note that the dict could be empty, - i.e. {}. That basically indicates a failure. - """ - resp = {} - try: - resp = simplejson.loads(content) - except StandardError: - # different JSON libs raise different exceptions, - # so we just do a catch-all here - resp = dict(parse_qsl(content)) - - # some providers respond with 'expires', others with 'expires_in' - if resp and 'expires' in resp: - resp['expires_in'] = resp.pop('expires') - - return resp - - -@util.positional(4) -def credentials_from_code(client_id, client_secret, scope, code, - redirect_uri='postmessage', http=None, - user_agent=None, token_uri=GOOGLE_TOKEN_URI, - auth_uri=GOOGLE_AUTH_URI, - revoke_uri=GOOGLE_REVOKE_URI): - """Exchanges an authorization code for an OAuth2Credentials object. - - Args: - client_id: string, client identifier. - client_secret: string, client secret. - scope: string or iterable of strings, scope(s) to request. - code: string, An authroization code, most likely passed down from - the client - redirect_uri: string, this is generally set to 'postmessage' to match the - redirect_uri that the client specified - http: httplib2.Http, optional http instance to use to do the fetch - token_uri: string, URI for token endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - auth_uri: string, URI for authorization endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - revoke_uri: string, URI for revoke endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - - Returns: - An OAuth2Credentials object. - - Raises: - FlowExchangeError if the authorization code cannot be exchanged for an - access token - """ - flow = OAuth2WebServerFlow(client_id, client_secret, scope, - redirect_uri=redirect_uri, user_agent=user_agent, - auth_uri=auth_uri, token_uri=token_uri, - revoke_uri=revoke_uri) - - credentials = flow.step2_exchange(code, http=http) - return credentials - - -@util.positional(3) -def credentials_from_clientsecrets_and_code(filename, scope, code, - message = None, - redirect_uri='postmessage', - http=None, - cache=None): - """Returns OAuth2Credentials from a clientsecrets file and an auth code. - - Will create the right kind of Flow based on the contents of the clientsecrets - file or will raise InvalidClientSecretsError for unknown types of Flows. - - Args: - filename: string, File name of clientsecrets. - scope: string or iterable of strings, scope(s) to request. - code: string, An authorization code, most likely passed down from - the client - message: string, A friendly string to display to the user if the - clientsecrets file is missing or invalid. If message is provided then - sys.exit will be called in the case of an error. If message in not - provided then clientsecrets.InvalidClientSecretsError will be raised. - redirect_uri: string, this is generally set to 'postmessage' to match the - redirect_uri that the client specified - http: httplib2.Http, optional http instance to use to do the fetch - cache: An optional cache service client that implements get() and set() - methods. See clientsecrets.loadfile() for details. - - Returns: - An OAuth2Credentials object. - - Raises: - FlowExchangeError if the authorization code cannot be exchanged for an - access token - UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. - clientsecrets.InvalidClientSecretsError if the clientsecrets file is - invalid. - """ - flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, - redirect_uri=redirect_uri) - credentials = flow.step2_exchange(code, http=http) - return credentials - - -class OAuth2WebServerFlow(Flow): - """Does the Web Server Flow for OAuth 2.0. - - OAuth2WebServerFlow objects may be safely pickled and unpickled. - """ - - @util.positional(4) - def __init__(self, client_id, client_secret, scope, - redirect_uri=None, - user_agent=None, - auth_uri=GOOGLE_AUTH_URI, - token_uri=GOOGLE_TOKEN_URI, - revoke_uri=GOOGLE_REVOKE_URI, - **kwargs): - """Constructor for OAuth2WebServerFlow. - - The kwargs argument is used to set extra query parameters on the - auth_uri. For example, the access_type and approval_prompt - query parameters can be set via kwargs. - - Args: - client_id: string, client identifier. - client_secret: string client secret. - scope: string or iterable of strings, scope(s) of the credentials being - requested. - redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for - a non-web-based application, or a URI that handles the callback from - the authorization server. - user_agent: string, HTTP User-Agent to provide for this application. - auth_uri: string, URI for authorization endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - token_uri: string, URI for token endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - revoke_uri: string, URI for revoke endpoint. For convenience - defaults to Google's endpoints but any OAuth 2.0 provider can be used. - **kwargs: dict, The keyword arguments are all optional and required - parameters for the OAuth calls. - """ - self.client_id = client_id - self.client_secret = client_secret - self.scope = util.scopes_to_string(scope) - self.redirect_uri = redirect_uri - self.user_agent = user_agent - self.auth_uri = auth_uri - self.token_uri = token_uri - self.revoke_uri = revoke_uri - self.params = { - 'access_type': 'offline', - 'response_type': 'code', - } - self.params.update(kwargs) - - @util.positional(1) - def step1_get_authorize_url(self, redirect_uri=None): - """Returns a URI to redirect to the provider. - - Args: - redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for - a non-web-based application, or a URI that handles the callback from - the authorization server. This parameter is deprecated, please move to - passing the redirect_uri in via the constructor. - - Returns: - A URI as a string to redirect the user to begin the authorization flow. - """ - if redirect_uri is not None: - logger.warning(('The redirect_uri parameter for' - 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please' - 'move to passing the redirect_uri in via the constructor.')) - self.redirect_uri = redirect_uri - - if self.redirect_uri is None: - raise ValueError('The value of redirect_uri must not be None.') - - query_params = { - 'client_id': self.client_id, - 'redirect_uri': self.redirect_uri, - 'scope': self.scope, - } - query_params.update(self.params) - return _update_query_params(self.auth_uri, query_params) - - @util.positional(2) - def step2_exchange(self, code, http=None): - """Exhanges a code for OAuth2Credentials. - - Args: - code: string or dict, either the code as a string, or a dictionary - of the query parameters to the redirect_uri, which contains - the code. - http: httplib2.Http, optional http instance to use to do the fetch - - Returns: - An OAuth2Credentials object that can be used to authorize requests. - - Raises: - FlowExchangeError if a problem occured exchanging the code for a - refresh_token. - """ - - if not (isinstance(code, str) or isinstance(code, unicode)): - if 'code' not in code: - if 'error' in code: - error_msg = code['error'] - else: - error_msg = 'No code was supplied in the query parameters.' - raise FlowExchangeError(error_msg) - else: - code = code['code'] - - body = urllib.urlencode({ - 'grant_type': 'authorization_code', - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'code': code, - 'redirect_uri': self.redirect_uri, - 'scope': self.scope, - }) - headers = { - 'content-type': 'application/x-www-form-urlencoded', - } - - if self.user_agent is not None: - headers['user-agent'] = self.user_agent - - if http is None: - http = httplib2.Http() - - resp, content = http.request(self.token_uri, method='POST', body=body, - headers=headers) - d = _parse_exchange_token_response(content) - if resp.status == 200 and 'access_token' in d: - access_token = d['access_token'] - refresh_token = d.get('refresh_token', None) - token_expiry = None - if 'expires_in' in d: - token_expiry = datetime.datetime.utcnow() + datetime.timedelta( - seconds=int(d['expires_in'])) - - if 'id_token' in d: - d['id_token'] = _extract_id_token(d['id_token']) - - logger.info('Successfully retrieved access token') - return OAuth2Credentials(access_token, self.client_id, - self.client_secret, refresh_token, token_expiry, - self.token_uri, self.user_agent, - revoke_uri=self.revoke_uri, - id_token=d.get('id_token', None), - token_response=d) - else: - logger.info('Failed to retrieve access token: %s' % content) - if 'error' in d: - # you never know what those providers got to say - error_msg = unicode(d['error']) - else: - error_msg = 'Invalid response: %s.' % str(resp.status) - raise FlowExchangeError(error_msg) - - -@util.positional(2) -def flow_from_clientsecrets(filename, scope, redirect_uri=None, - message=None, cache=None): - """Create a Flow from a clientsecrets file. - - Will create the right kind of Flow based on the contents of the clientsecrets - file or will raise InvalidClientSecretsError for unknown types of Flows. - - Args: - filename: string, File name of client secrets. - scope: string or iterable of strings, scope(s) to request. - redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for - a non-web-based application, or a URI that handles the callback from - the authorization server. - message: string, A friendly string to display to the user if the - clientsecrets file is missing or invalid. If message is provided then - sys.exit will be called in the case of an error. If message in not - provided then clientsecrets.InvalidClientSecretsError will be raised. - cache: An optional cache service client that implements get() and set() - methods. See clientsecrets.loadfile() for details. - - Returns: - A Flow object. - - Raises: - UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. - clientsecrets.InvalidClientSecretsError if the clientsecrets file is - invalid. - """ - try: - client_type, client_info = clientsecrets.loadfile(filename, cache=cache) - if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED): - constructor_kwargs = { - 'redirect_uri': redirect_uri, - 'auth_uri': client_info['auth_uri'], - 'token_uri': client_info['token_uri'], - } - revoke_uri = client_info.get('revoke_uri') - if revoke_uri is not None: - constructor_kwargs['revoke_uri'] = revoke_uri - return OAuth2WebServerFlow( - client_info['client_id'], client_info['client_secret'], - scope, **constructor_kwargs) - - except clientsecrets.InvalidClientSecretsError: - if message: - sys.exit(message) - else: - raise - else: - raise UnknownClientSecretsFlowError( - 'This OAuth 2.0 flow is unsupported: %r' % client_type) diff --git a/third_party/oauth2client/clientsecrets.py b/third_party/oauth2client/clientsecrets.py deleted file mode 100644 index ac99aae969..0000000000 --- a/third_party/oauth2client/clientsecrets.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (C) 2011 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utilities for reading OAuth 2.0 client secret files. - -A client_secrets.json file contains all the information needed to interact with -an OAuth 2.0 protected service. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - - -from anyjson import simplejson - -# Properties that make a client_secrets.json file valid. -TYPE_WEB = 'web' -TYPE_INSTALLED = 'installed' - -VALID_CLIENT = { - TYPE_WEB: { - 'required': [ - 'client_id', - 'client_secret', - 'redirect_uris', - 'auth_uri', - 'token_uri', - ], - 'string': [ - 'client_id', - 'client_secret', - ], - }, - TYPE_INSTALLED: { - 'required': [ - 'client_id', - 'client_secret', - 'redirect_uris', - 'auth_uri', - 'token_uri', - ], - 'string': [ - 'client_id', - 'client_secret', - ], - }, -} - - -class Error(Exception): - """Base error for this module.""" - pass - - -class InvalidClientSecretsError(Error): - """Format of ClientSecrets file is invalid.""" - pass - - -def _validate_clientsecrets(obj): - if obj is None or len(obj) != 1: - raise InvalidClientSecretsError('Invalid file format.') - client_type = obj.keys()[0] - if client_type not in VALID_CLIENT.keys(): - raise InvalidClientSecretsError('Unknown client type: %s.' % client_type) - client_info = obj[client_type] - for prop_name in VALID_CLIENT[client_type]['required']: - if prop_name not in client_info: - raise InvalidClientSecretsError( - 'Missing property "%s" in a client type of "%s".' % (prop_name, - client_type)) - for prop_name in VALID_CLIENT[client_type]['string']: - if client_info[prop_name].startswith('[['): - raise InvalidClientSecretsError( - 'Property "%s" is not configured.' % prop_name) - return client_type, client_info - - -def load(fp): - obj = simplejson.load(fp) - return _validate_clientsecrets(obj) - - -def loads(s): - obj = simplejson.loads(s) - return _validate_clientsecrets(obj) - - -def _loadfile(filename): - try: - fp = file(filename, 'r') - try: - obj = simplejson.load(fp) - finally: - fp.close() - except IOError: - raise InvalidClientSecretsError('File not found: "%s"' % filename) - return _validate_clientsecrets(obj) - - -def loadfile(filename, cache=None): - """Loading of client_secrets JSON file, optionally backed by a cache. - - Typical cache storage would be App Engine memcache service, - but you can pass in any other cache client that implements - these methods: - - get(key, namespace=ns) - - set(key, value, namespace=ns) - - Usage: - # without caching - client_type, client_info = loadfile('secrets.json') - # using App Engine memcache service - from google.appengine.api import memcache - client_type, client_info = loadfile('secrets.json', cache=memcache) - - Args: - filename: string, Path to a client_secrets.json file on a filesystem. - cache: An optional cache service client that implements get() and set() - methods. If not specified, the file is always being loaded from - a filesystem. - - Raises: - InvalidClientSecretsError: In case of a validation error or some - I/O failure. Can happen only on cache miss. - - Returns: - (client_type, client_info) tuple, as _loadfile() normally would. - JSON contents is validated only during first load. Cache hits are not - validated. - """ - _SECRET_NAMESPACE = 'oauth2client:secrets#ns' - - if not cache: - return _loadfile(filename) - - obj = cache.get(filename, namespace=_SECRET_NAMESPACE) - if obj is None: - client_type, client_info = _loadfile(filename) - obj = {client_type: client_info} - cache.set(filename, obj, namespace=_SECRET_NAMESPACE) - - return obj.iteritems().next() diff --git a/third_party/oauth2client/crypt.py b/third_party/oauth2client/crypt.py deleted file mode 100644 index 2d31815dd6..0000000000 --- a/third_party/oauth2client/crypt.py +++ /dev/null @@ -1,377 +0,0 @@ -#!/usr/bin/python2.4 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2011 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 -import hashlib -import logging -import time - -from anyjson import simplejson - - -CLOCK_SKEW_SECS = 300 # 5 minutes in seconds -AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds -MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds - - -logger = logging.getLogger(__name__) - - -class AppIdentityError(Exception): - pass - - -try: - from OpenSSL import crypto - - - class OpenSSLVerifier(object): - """Verifies the signature on a message.""" - - def __init__(self, pubkey): - """Constructor. - - Args: - pubkey, OpenSSL.crypto.PKey, The public key to verify with. - """ - self._pubkey = pubkey - - def verify(self, message, signature): - """Verifies a message against a signature. - - Args: - message: string, The message to verify. - signature: string, The signature on the message. - - Returns: - True if message was signed by the private key associated with the public - key that this object was constructed with. - """ - try: - crypto.verify(self._pubkey, signature, message, 'sha256') - return True - except: - return False - - @staticmethod - def from_string(key_pem, is_x509_cert): - """Construct a Verified instance from a string. - - Args: - key_pem: string, public key in PEM format. - is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is - expected to be an RSA key in PEM format. - - Returns: - Verifier instance. - - Raises: - OpenSSL.crypto.Error if the key_pem can't be parsed. - """ - if is_x509_cert: - pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem) - else: - pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem) - return OpenSSLVerifier(pubkey) - - - class OpenSSLSigner(object): - """Signs messages with a private key.""" - - def __init__(self, pkey): - """Constructor. - - Args: - pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with. - """ - self._key = pkey - - def sign(self, message): - """Signs a message. - - Args: - message: string, Message to be signed. - - Returns: - string, The signature of the message for the given key. - """ - return crypto.sign(self._key, message, 'sha256') - - @staticmethod - def from_string(key, password='notasecret'): - """Construct a Signer instance from a string. - - Args: - key: string, private key in PKCS12 or PEM format. - password: string, password for the private key file. - - Returns: - Signer instance. - - Raises: - OpenSSL.crypto.Error if the key can't be parsed. - """ - if key.startswith('-----BEGIN '): - pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key) - else: - pkey = crypto.load_pkcs12(key, password).get_privatekey() - return OpenSSLSigner(pkey) - -except ImportError: - OpenSSLVerifier = None - OpenSSLSigner = None - - -try: - from Crypto.PublicKey import RSA - from Crypto.Hash import SHA256 - from Crypto.Signature import PKCS1_v1_5 - - - class PyCryptoVerifier(object): - """Verifies the signature on a message.""" - - def __init__(self, pubkey): - """Constructor. - - Args: - pubkey, OpenSSL.crypto.PKey (or equiv), The public key to verify with. - """ - self._pubkey = pubkey - - def verify(self, message, signature): - """Verifies a message against a signature. - - Args: - message: string, The message to verify. - signature: string, The signature on the message. - - Returns: - True if message was signed by the private key associated with the public - key that this object was constructed with. - """ - try: - return PKCS1_v1_5.new(self._pubkey).verify( - SHA256.new(message), signature) - except: - return False - - @staticmethod - def from_string(key_pem, is_x509_cert): - """Construct a Verified instance from a string. - - Args: - key_pem: string, public key in PEM format. - is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is - expected to be an RSA key in PEM format. - - Returns: - Verifier instance. - - Raises: - NotImplementedError if is_x509_cert is true. - """ - if is_x509_cert: - raise NotImplementedError( - 'X509 certs are not supported by the PyCrypto library. ' - 'Try using PyOpenSSL if native code is an option.') - else: - pubkey = RSA.importKey(key_pem) - return PyCryptoVerifier(pubkey) - - - class PyCryptoSigner(object): - """Signs messages with a private key.""" - - def __init__(self, pkey): - """Constructor. - - Args: - pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with. - """ - self._key = pkey - - def sign(self, message): - """Signs a message. - - Args: - message: string, Message to be signed. - - Returns: - string, The signature of the message for the given key. - """ - return PKCS1_v1_5.new(self._key).sign(SHA256.new(message)) - - @staticmethod - def from_string(key, password='notasecret'): - """Construct a Signer instance from a string. - - Args: - key: string, private key in PEM format. - password: string, password for private key file. Unused for PEM files. - - Returns: - Signer instance. - - Raises: - NotImplementedError if they key isn't in PEM format. - """ - if key.startswith('-----BEGIN '): - pkey = RSA.importKey(key) - else: - raise NotImplementedError( - 'PKCS12 format is not supported by the PyCrpto library. ' - 'Try converting to a "PEM" ' - '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) ' - 'or using PyOpenSSL if native code is an option.') - return PyCryptoSigner(pkey) - -except ImportError: - PyCryptoVerifier = None - PyCryptoSigner = None - - -if OpenSSLSigner: - Signer = OpenSSLSigner - Verifier = OpenSSLVerifier -elif PyCryptoSigner: - Signer = PyCryptoSigner - Verifier = PyCryptoVerifier -else: - raise ImportError('No encryption library found. Please install either ' - 'PyOpenSSL, or PyCrypto 2.6 or later') - - -def _urlsafe_b64encode(raw_bytes): - return base64.urlsafe_b64encode(raw_bytes).rstrip('=') - - -def _urlsafe_b64decode(b64string): - # Guard against unicode strings, which base64 can't handle. - b64string = b64string.encode('ascii') - padded = b64string + '=' * (4 - len(b64string) % 4) - return base64.urlsafe_b64decode(padded) - - -def _json_encode(data): - return simplejson.dumps(data, separators = (',', ':')) - - -def make_signed_jwt(signer, payload): - """Make a signed JWT. - - See http://self-issued.info/docs/draft-jones-json-web-token.html. - - Args: - signer: crypt.Signer, Cryptographic signer. - payload: dict, Dictionary of data to convert to JSON and then sign. - - Returns: - string, The JWT for the payload. - """ - header = {'typ': 'JWT', 'alg': 'RS256'} - - segments = [ - _urlsafe_b64encode(_json_encode(header)), - _urlsafe_b64encode(_json_encode(payload)), - ] - signing_input = '.'.join(segments) - - signature = signer.sign(signing_input) - segments.append(_urlsafe_b64encode(signature)) - - logger.debug(str(segments)) - - return '.'.join(segments) - - -def verify_signed_jwt_with_certs(jwt, certs, audience): - """Verify a JWT against public certs. - - See http://self-issued.info/docs/draft-jones-json-web-token.html. - - Args: - jwt: string, A JWT. - certs: dict, Dictionary where values of public keys in PEM format. - audience: string, The audience, 'aud', that this JWT should contain. If - None then the JWT's 'aud' parameter is not verified. - - Returns: - dict, The deserialized JSON payload in the JWT. - - Raises: - AppIdentityError if any checks are failed. - """ - segments = jwt.split('.') - - if (len(segments) != 3): - raise AppIdentityError( - 'Wrong number of segments in token: %s' % jwt) - signed = '%s.%s' % (segments[0], segments[1]) - - signature = _urlsafe_b64decode(segments[2]) - - # Parse token. - json_body = _urlsafe_b64decode(segments[1]) - try: - parsed = simplejson.loads(json_body) - except: - raise AppIdentityError('Can\'t parse token: %s' % json_body) - - # Check signature. - verified = False - for (keyname, pem) in certs.items(): - verifier = Verifier.from_string(pem, True) - if (verifier.verify(signed, signature)): - verified = True - break - if not verified: - raise AppIdentityError('Invalid token signature: %s' % jwt) - - # Check creation timestamp. - iat = parsed.get('iat') - if iat is None: - raise AppIdentityError('No iat field in token: %s' % json_body) - earliest = iat - CLOCK_SKEW_SECS - - # Check expiration timestamp. - now = long(time.time()) - exp = parsed.get('exp') - if exp is None: - raise AppIdentityError('No exp field in token: %s' % json_body) - if exp >= now + MAX_TOKEN_LIFETIME_SECS: - raise AppIdentityError( - 'exp field too far in future: %s' % json_body) - latest = exp + CLOCK_SKEW_SECS - - if now < earliest: - raise AppIdentityError('Token used too early, %d < %d: %s' % - (now, earliest, json_body)) - if now > latest: - raise AppIdentityError('Token used too late, %d > %d: %s' % - (now, latest, json_body)) - - # Check audience. - if audience is not None: - aud = parsed.get('aud') - if aud is None: - raise AppIdentityError('No aud field in token: %s' % json_body) - if aud != audience: - raise AppIdentityError('Wrong recipient, %s != %s: %s' % - (aud, audience, json_body)) - - return parsed diff --git a/third_party/oauth2client/django_orm.py b/third_party/oauth2client/django_orm.py deleted file mode 100644 index d54d20c285..0000000000 --- a/third_party/oauth2client/django_orm.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright (C) 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""OAuth 2.0 utilities for Django. - -Utilities for using OAuth 2.0 in conjunction with -the Django datastore. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import oauth2client -import base64 -import pickle - -from django.db import models -from oauth2client.client import Storage as BaseStorage - -class CredentialsField(models.Field): - - __metaclass__ = models.SubfieldBase - - def __init__(self, *args, **kwargs): - if 'null' not in kwargs: - kwargs['null'] = True - super(CredentialsField, self).__init__(*args, **kwargs) - - def get_internal_type(self): - return "TextField" - - def to_python(self, value): - if value is None: - return None - if isinstance(value, oauth2client.client.Credentials): - return value - return pickle.loads(base64.b64decode(value)) - - def get_db_prep_value(self, value, connection, prepared=False): - if value is None: - return None - return base64.b64encode(pickle.dumps(value)) - - -class FlowField(models.Field): - - __metaclass__ = models.SubfieldBase - - def __init__(self, *args, **kwargs): - if 'null' not in kwargs: - kwargs['null'] = True - super(FlowField, self).__init__(*args, **kwargs) - - def get_internal_type(self): - return "TextField" - - def to_python(self, value): - if value is None: - return None - if isinstance(value, oauth2client.client.Flow): - return value - return pickle.loads(base64.b64decode(value)) - - def get_db_prep_value(self, value, connection, prepared=False): - if value is None: - return None - return base64.b64encode(pickle.dumps(value)) - - -class Storage(BaseStorage): - """Store and retrieve a single credential to and from - the datastore. - - This Storage helper presumes the Credentials - have been stored as a CredenialsField - on a db model class. - """ - - def __init__(self, model_class, key_name, key_value, property_name): - """Constructor for Storage. - - Args: - model: db.Model, model class - key_name: string, key name for the entity that has the credentials - key_value: string, key value for the entity that has the credentials - property_name: string, name of the property that is an CredentialsProperty - """ - self.model_class = model_class - self.key_name = key_name - self.key_value = key_value - self.property_name = property_name - - def locked_get(self): - """Retrieve Credential from datastore. - - Returns: - oauth2client.Credentials - """ - credential = None - - query = {self.key_name: self.key_value} - entities = self.model_class.objects.filter(**query) - if len(entities) > 0: - credential = getattr(entities[0], self.property_name) - if credential and hasattr(credential, 'set_store'): - credential.set_store(self) - return credential - - def locked_put(self, credentials): - """Write a Credentials to the datastore. - - Args: - credentials: Credentials, the credentials to store. - """ - args = {self.key_name: self.key_value} - entity = self.model_class(**args) - setattr(entity, self.property_name, credentials) - entity.save() - - def locked_delete(self): - """Delete Credentials from the datastore.""" - - query = {self.key_name: self.key_value} - entities = self.model_class.objects.filter(**query).delete() diff --git a/third_party/oauth2client/file.py b/third_party/oauth2client/file.py deleted file mode 100644 index 1895f94eac..0000000000 --- a/third_party/oauth2client/file.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright (C) 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utilities for OAuth. - -Utilities for making it easier to work with OAuth 2.0 -credentials. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import os -import stat -import threading - -from anyjson import simplejson -from client import Storage as BaseStorage -from client import Credentials - - -class CredentialsFileSymbolicLinkError(Exception): - """Credentials files must not be symbolic links.""" - - -class Storage(BaseStorage): - """Store and retrieve a single credential to and from a file.""" - - def __init__(self, filename): - self._filename = filename - self._lock = threading.Lock() - - def _validate_file(self): - if os.path.islink(self._filename): - raise CredentialsFileSymbolicLinkError( - 'File: %s is a symbolic link.' % self._filename) - - def acquire_lock(self): - """Acquires any lock necessary to access this Storage. - - This lock is not reentrant.""" - self._lock.acquire() - - def release_lock(self): - """Release the Storage lock. - - Trying to release a lock that isn't held will result in a - RuntimeError. - """ - self._lock.release() - - def locked_get(self): - """Retrieve Credential from file. - - Returns: - oauth2client.client.Credentials - - Raises: - CredentialsFileSymbolicLinkError if the file is a symbolic link. - """ - credentials = None - self._validate_file() - try: - f = open(self._filename, 'rb') - content = f.read() - f.close() - except IOError: - return credentials - - try: - credentials = Credentials.new_from_json(content) - credentials.set_store(self) - except ValueError: - pass - - return credentials - - def _create_file_if_needed(self): - """Create an empty file if necessary. - - This method will not initialize the file. Instead it implements a - simple version of "touch" to ensure the file has been created. - """ - if not os.path.exists(self._filename): - old_umask = os.umask(0177) - try: - open(self._filename, 'a+b').close() - finally: - os.umask(old_umask) - - def locked_put(self, credentials): - """Write Credentials to file. - - Args: - credentials: Credentials, the credentials to store. - - Raises: - CredentialsFileSymbolicLinkError if the file is a symbolic link. - """ - - self._create_file_if_needed() - self._validate_file() - f = open(self._filename, 'wb') - f.write(credentials.to_json()) - f.close() - - def locked_delete(self): - """Delete Credentials file. - - Args: - credentials: Credentials, the credentials to store. - """ - - os.unlink(self._filename) diff --git a/third_party/oauth2client/gce.py b/third_party/oauth2client/gce.py deleted file mode 100644 index c7fd7c18a9..0000000000 --- a/third_party/oauth2client/gce.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (C) 2012 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utilities for Google Compute Engine - -Utilities for making it easier to use OAuth 2.0 on Google Compute Engine. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import httplib2 -import logging -import uritemplate - -from oauth2client import util -from oauth2client.anyjson import simplejson -from oauth2client.client import AccessTokenRefreshError -from oauth2client.client import AssertionCredentials - -logger = logging.getLogger(__name__) - -# URI Template for the endpoint that returns access_tokens. -META = ('http://metadata.google.internal/0.1/meta-data/service-accounts/' - 'default/acquire{?scope}') - - -class AppAssertionCredentials(AssertionCredentials): - """Credentials object for Compute Engine Assertion Grants - - This object will allow a Compute Engine instance to identify itself to - Google and other OAuth 2.0 servers that can verify assertions. It can be used - for the purpose of accessing data stored under an account assigned to the - Compute Engine instance itself. - - This credential does not require a flow to instantiate because it represents - a two legged flow, and therefore has all of the required information to - generate and refresh its own access tokens. - """ - - @util.positional(2) - def __init__(self, scope, **kwargs): - """Constructor for AppAssertionCredentials - - Args: - scope: string or iterable of strings, scope(s) of the credentials being - requested. - """ - self.scope = util.scopes_to_string(scope) - - # Assertion type is no longer used, but still in the parent class signature. - super(AppAssertionCredentials, self).__init__(None) - - @classmethod - def from_json(cls, json): - data = simplejson.loads(json) - return AppAssertionCredentials(data['scope']) - - def _refresh(self, http_request): - """Refreshes the access_token. - - Skip all the storage hoops and just refresh using the API. - - Args: - http_request: callable, a callable that matches the method signature of - httplib2.Http.request, used to make the refresh request. - - Raises: - AccessTokenRefreshError: When the refresh fails. - """ - uri = uritemplate.expand(META, {'scope': self.scope}) - response, content = http_request(uri) - if response.status == 200: - try: - d = simplejson.loads(content) - except StandardError, e: - raise AccessTokenRefreshError(str(e)) - self.access_token = d['accessToken'] - else: - raise AccessTokenRefreshError(content) diff --git a/third_party/oauth2client/keyring_storage.py b/third_party/oauth2client/keyring_storage.py deleted file mode 100644 index efe2949cd5..0000000000 --- a/third_party/oauth2client/keyring_storage.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (C) 2012 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""A keyring based Storage. - -A Storage for Credentials that uses the keyring module. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' - -import keyring -import threading - -from client import Storage as BaseStorage -from client import Credentials - - -class Storage(BaseStorage): - """Store and retrieve a single credential to and from the keyring. - - To use this module you must have the keyring module installed. See - . This is an optional module and is not - installed with oauth2client by default because it does not work on all the - platforms that oauth2client supports, such as Google App Engine. - - The keyring module is a cross-platform - library for access the keyring capabilities of the local system. The user will - be prompted for their keyring password when this module is used, and the - manner in which the user is prompted will vary per platform. - - Usage: - from oauth2client.keyring_storage import Storage - - s = Storage('name_of_application', 'user1') - credentials = s.get() - - """ - - def __init__(self, service_name, user_name): - """Constructor. - - Args: - service_name: string, The name of the service under which the credentials - are stored. - user_name: string, The name of the user to store credentials for. - """ - self._service_name = service_name - self._user_name = user_name - self._lock = threading.Lock() - - def acquire_lock(self): - """Acquires any lock necessary to access this Storage. - - This lock is not reentrant.""" - self._lock.acquire() - - def release_lock(self): - """Release the Storage lock. - - Trying to release a lock that isn't held will result in a - RuntimeError. - """ - self._lock.release() - - def locked_get(self): - """Retrieve Credential from file. - - Returns: - oauth2client.client.Credentials - """ - credentials = None - content = keyring.get_password(self._service_name, self._user_name) - - if content is not None: - try: - credentials = Credentials.new_from_json(content) - credentials.set_store(self) - except ValueError: - pass - - return credentials - - def locked_put(self, credentials): - """Write Credentials to file. - - Args: - credentials: Credentials, the credentials to store. - """ - keyring.set_password(self._service_name, self._user_name, - credentials.to_json()) - - def locked_delete(self): - """Delete Credentials file. - - Args: - credentials: Credentials, the credentials to store. - """ - keyring.set_password(self._service_name, self._user_name, '') diff --git a/third_party/oauth2client/locked_file.py b/third_party/oauth2client/locked_file.py deleted file mode 100644 index 858b702819..0000000000 --- a/third_party/oauth2client/locked_file.py +++ /dev/null @@ -1,373 +0,0 @@ -# Copyright 2011 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Locked file interface that should work on Unix and Windows pythons. - -This module first tries to use fcntl locking to ensure serialized access -to a file, then falls back on a lock file if that is unavialable. - -Usage: - f = LockedFile('filename', 'r+b', 'rb') - f.open_and_lock() - if f.is_locked(): - print 'Acquired filename with r+b mode' - f.file_handle().write('locked data') - else: - print 'Aquired filename with rb mode' - f.unlock_and_close() -""" - -__author__ = 'cache@google.com (David T McWherter)' - -import errno -import logging -import os -import time - -from . import util - -logger = logging.getLogger(__name__) - - -class CredentialsFileSymbolicLinkError(Exception): - """Credentials files must not be symbolic links.""" - - -class AlreadyLockedException(Exception): - """Trying to lock a file that has already been locked by the LockedFile.""" - pass - - -def validate_file(filename): - if os.path.islink(filename): - raise CredentialsFileSymbolicLinkError( - 'File: %s is a symbolic link.' % filename) - -class _Opener(object): - """Base class for different locking primitives.""" - - def __init__(self, filename, mode, fallback_mode): - """Create an Opener. - - Args: - filename: string, The pathname of the file. - mode: string, The preferred mode to access the file with. - fallback_mode: string, The mode to use if locking fails. - """ - self._locked = False - self._filename = filename - self._mode = mode - self._fallback_mode = fallback_mode - self._fh = None - - def is_locked(self): - """Was the file locked.""" - return self._locked - - def file_handle(self): - """The file handle to the file. Valid only after opened.""" - return self._fh - - def filename(self): - """The filename that is being locked.""" - return self._filename - - def open_and_lock(self, timeout, delay): - """Open the file and lock it. - - Args: - timeout: float, How long to try to lock for. - delay: float, How long to wait between retries. - """ - pass - - def unlock_and_close(self): - """Unlock and close the file.""" - pass - - -class _PosixOpener(_Opener): - """Lock files using Posix advisory lock files.""" - - def open_and_lock(self, timeout, delay): - """Open the file and lock it. - - Tries to create a .lock file next to the file we're trying to open. - - Args: - timeout: float, How long to try to lock for. - delay: float, How long to wait between retries. - - Raises: - AlreadyLockedException: if the lock is already acquired. - IOError: if the open fails. - CredentialsFileSymbolicLinkError if the file is a symbolic link. - """ - if self._locked: - raise AlreadyLockedException('File %s is already locked' % - self._filename) - self._locked = False - - validate_file(self._filename) - try: - self._fh = open(self._filename, self._mode) - except IOError, e: - # If we can't access with _mode, try _fallback_mode and don't lock. - if e.errno == errno.EACCES: - self._fh = open(self._filename, self._fallback_mode) - return - - lock_filename = self._posix_lockfile(self._filename) - start_time = time.time() - while True: - try: - self._lock_fd = os.open(lock_filename, - os.O_CREAT|os.O_EXCL|os.O_RDWR) - self._locked = True - break - - except OSError, e: - if e.errno != errno.EEXIST: - raise - if (time.time() - start_time) >= timeout: - logger.warn('Could not acquire lock %s in %s seconds' % ( - lock_filename, timeout)) - # Close the file and open in fallback_mode. - if self._fh: - self._fh.close() - self._fh = open(self._filename, self._fallback_mode) - return - time.sleep(delay) - - def unlock_and_close(self): - """Unlock a file by removing the .lock file, and close the handle.""" - if self._locked: - lock_filename = self._posix_lockfile(self._filename) - os.close(self._lock_fd) - os.unlink(lock_filename) - self._locked = False - self._lock_fd = None - if self._fh: - self._fh.close() - - def _posix_lockfile(self, filename): - """The name of the lock file to use for posix locking.""" - return '%s.lock' % filename - - -try: - import fcntl - - class _FcntlOpener(_Opener): - """Open, lock, and unlock a file using fcntl.lockf.""" - - def open_and_lock(self, timeout, delay): - """Open the file and lock it. - - Args: - timeout: float, How long to try to lock for. - delay: float, How long to wait between retries - - Raises: - AlreadyLockedException: if the lock is already acquired. - IOError: if the open fails. - CredentialsFileSymbolicLinkError if the file is a symbolic link. - """ - if self._locked: - raise AlreadyLockedException('File %s is already locked' % - self._filename) - start_time = time.time() - - validate_file(self._filename) - try: - self._fh = open(self._filename, self._mode) - except IOError, e: - # If we can't access with _mode, try _fallback_mode and don't lock. - if e.errno == errno.EACCES: - self._fh = open(self._filename, self._fallback_mode) - return - - # We opened in _mode, try to lock the file. - while True: - try: - fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX) - self._locked = True - return - except IOError, e: - # If not retrying, then just pass on the error. - if timeout == 0: - raise e - if e.errno != errno.EACCES: - raise e - # We could not acquire the lock. Try again. - if (time.time() - start_time) >= timeout: - logger.warn('Could not lock %s in %s seconds' % ( - self._filename, timeout)) - if self._fh: - self._fh.close() - self._fh = open(self._filename, self._fallback_mode) - return - time.sleep(delay) - - def unlock_and_close(self): - """Close and unlock the file using the fcntl.lockf primitive.""" - if self._locked: - fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN) - self._locked = False - if self._fh: - self._fh.close() -except ImportError: - _FcntlOpener = None - - -try: - import pywintypes - import win32con - import win32file - - class _Win32Opener(_Opener): - """Open, lock, and unlock a file using windows primitives.""" - - # Error #33: - # 'The process cannot access the file because another process' - FILE_IN_USE_ERROR = 33 - - # Error #158: - # 'The segment is already unlocked.' - FILE_ALREADY_UNLOCKED_ERROR = 158 - - def open_and_lock(self, timeout, delay): - """Open the file and lock it. - - Args: - timeout: float, How long to try to lock for. - delay: float, How long to wait between retries - - Raises: - AlreadyLockedException: if the lock is already acquired. - IOError: if the open fails. - CredentialsFileSymbolicLinkError if the file is a symbolic link. - """ - if self._locked: - raise AlreadyLockedException('File %s is already locked' % - self._filename) - start_time = time.time() - - validate_file(self._filename) - try: - self._fh = open(self._filename, self._mode) - except IOError, e: - # If we can't access with _mode, try _fallback_mode and don't lock. - if e.errno == errno.EACCES: - self._fh = open(self._filename, self._fallback_mode) - return - - # We opened in _mode, try to lock the file. - while True: - try: - hfile = win32file._get_osfhandle(self._fh.fileno()) - win32file.LockFileEx( - hfile, - (win32con.LOCKFILE_FAIL_IMMEDIATELY| - win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000, - pywintypes.OVERLAPPED()) - self._locked = True - return - except pywintypes.error, e: - if timeout == 0: - raise e - - # If the error is not that the file is already in use, raise. - if e[0] != _Win32Opener.FILE_IN_USE_ERROR: - raise - - # We could not acquire the lock. Try again. - if (time.time() - start_time) >= timeout: - logger.warn('Could not lock %s in %s seconds' % ( - self._filename, timeout)) - if self._fh: - self._fh.close() - self._fh = open(self._filename, self._fallback_mode) - return - time.sleep(delay) - - def unlock_and_close(self): - """Close and unlock the file using the win32 primitive.""" - if self._locked: - try: - hfile = win32file._get_osfhandle(self._fh.fileno()) - win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED()) - except pywintypes.error, e: - if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR: - raise - self._locked = False - if self._fh: - self._fh.close() -except ImportError: - _Win32Opener = None - - -class LockedFile(object): - """Represent a file that has exclusive access.""" - - @util.positional(4) - def __init__(self, filename, mode, fallback_mode, use_native_locking=True): - """Construct a LockedFile. - - Args: - filename: string, The path of the file to open. - mode: string, The mode to try to open the file with. - fallback_mode: string, The mode to use if locking fails. - use_native_locking: bool, Whether or not fcntl/win32 locking is used. - """ - opener = None - if not opener and use_native_locking: - if _Win32Opener: - opener = _Win32Opener(filename, mode, fallback_mode) - if _FcntlOpener: - opener = _FcntlOpener(filename, mode, fallback_mode) - - if not opener: - opener = _PosixOpener(filename, mode, fallback_mode) - - self._opener = opener - - def filename(self): - """Return the filename we were constructed with.""" - return self._opener._filename - - def file_handle(self): - """Return the file_handle to the opened file.""" - return self._opener.file_handle() - - def is_locked(self): - """Return whether we successfully locked the file.""" - return self._opener.is_locked() - - def open_and_lock(self, timeout=0, delay=0.05): - """Open the file, trying to lock it. - - Args: - timeout: float, The number of seconds to try to acquire the lock. - delay: float, The number of seconds to wait between retry attempts. - - Raises: - AlreadyLockedException: if the lock is already acquired. - IOError: if the open fails. - """ - self._opener.open_and_lock(timeout, delay) - - def unlock_and_close(self): - """Unlock and close a file.""" - self._opener.unlock_and_close() diff --git a/third_party/oauth2client/multistore_file.py b/third_party/oauth2client/multistore_file.py deleted file mode 100644 index ea89027f23..0000000000 --- a/third_party/oauth2client/multistore_file.py +++ /dev/null @@ -1,465 +0,0 @@ -# Copyright 2011 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Multi-credential file store with lock support. - -This module implements a JSON credential store where multiple -credentials can be stored in one file. That file supports locking -both in a single process and across processes. - -The credential themselves are keyed off of: -* client_id -* user_agent -* scope - -The format of the stored data is like so: -{ - 'file_version': 1, - 'data': [ - { - 'key': { - 'clientId': '', - 'userAgent': '', - 'scope': '' - }, - 'credential': { - # JSON serialized Credentials. - } - } - ] -} -""" - -__author__ = 'jbeda@google.com (Joe Beda)' - -import base64 -import errno -import logging -import os -import threading - -from anyjson import simplejson -from .client import Storage as BaseStorage -from .client import Credentials -from . import util -from locked_file import LockedFile - -logger = logging.getLogger(__name__) - -# A dict from 'filename'->_MultiStore instances -_multistores = {} -_multistores_lock = threading.Lock() - - -class Error(Exception): - """Base error for this module.""" - pass - - -class NewerCredentialStoreError(Error): - """The credential store is a newer version that supported.""" - pass - - -@util.positional(4) -def get_credential_storage(filename, client_id, user_agent, scope, - warn_on_readonly=True): - """Get a Storage instance for a credential. - - Args: - filename: The JSON file storing a set of credentials - client_id: The client_id for the credential - user_agent: The user agent for the credential - scope: string or iterable of strings, Scope(s) being requested - warn_on_readonly: if True, log a warning if the store is readonly - - Returns: - An object derived from client.Storage for getting/setting the - credential. - """ - # Recreate the legacy key with these specific parameters - key = {'clientId': client_id, 'userAgent': user_agent, - 'scope': util.scopes_to_string(scope)} - return get_credential_storage_custom_key( - filename, key, warn_on_readonly=warn_on_readonly) - - -@util.positional(2) -def get_credential_storage_custom_string_key( - filename, key_string, warn_on_readonly=True): - """Get a Storage instance for a credential using a single string as a key. - - Allows you to provide a string as a custom key that will be used for - credential storage and retrieval. - - Args: - filename: The JSON file storing a set of credentials - key_string: A string to use as the key for storing this credential. - warn_on_readonly: if True, log a warning if the store is readonly - - Returns: - An object derived from client.Storage for getting/setting the - credential. - """ - # Create a key dictionary that can be used - key_dict = {'key': key_string} - return get_credential_storage_custom_key( - filename, key_dict, warn_on_readonly=warn_on_readonly) - - -@util.positional(2) -def get_credential_storage_custom_key( - filename, key_dict, warn_on_readonly=True): - """Get a Storage instance for a credential using a dictionary as a key. - - Allows you to provide a dictionary as a custom key that will be used for - credential storage and retrieval. - - Args: - filename: The JSON file storing a set of credentials - key_dict: A dictionary to use as the key for storing this credential. There - is no ordering of the keys in the dictionary. Logically equivalent - dictionaries will produce equivalent storage keys. - warn_on_readonly: if True, log a warning if the store is readonly - - Returns: - An object derived from client.Storage for getting/setting the - credential. - """ - multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly) - key = util.dict_to_tuple_key(key_dict) - return multistore._get_storage(key) - - -@util.positional(1) -def get_all_credential_keys(filename, warn_on_readonly=True): - """Gets all the registered credential keys in the given Multistore. - - Args: - filename: The JSON file storing a set of credentials - warn_on_readonly: if True, log a warning if the store is readonly - - Returns: - A list of the credential keys present in the file. They are returned as - dictionaries that can be passed into get_credential_storage_custom_key to - get the actual credentials. - """ - multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly) - multistore._lock() - try: - return multistore._get_all_credential_keys() - finally: - multistore._unlock() - - -@util.positional(1) -def _get_multistore(filename, warn_on_readonly=True): - """A helper method to initialize the multistore with proper locking. - - Args: - filename: The JSON file storing a set of credentials - warn_on_readonly: if True, log a warning if the store is readonly - - Returns: - A multistore object - """ - filename = os.path.expanduser(filename) - _multistores_lock.acquire() - try: - multistore = _multistores.setdefault( - filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly)) - finally: - _multistores_lock.release() - return multistore - - -class _MultiStore(object): - """A file backed store for multiple credentials.""" - - @util.positional(2) - def __init__(self, filename, warn_on_readonly=True): - """Initialize the class. - - This will create the file if necessary. - """ - self._file = LockedFile(filename, 'r+b', 'rb') - self._thread_lock = threading.Lock() - self._read_only = False - self._warn_on_readonly = warn_on_readonly - - self._create_file_if_needed() - - # Cache of deserialized store. This is only valid after the - # _MultiStore is locked or _refresh_data_cache is called. This is - # of the form of: - # - # ((key, value), (key, value)...) -> OAuth2Credential - # - # If this is None, then the store hasn't been read yet. - self._data = None - - class _Storage(BaseStorage): - """A Storage object that knows how to read/write a single credential.""" - - def __init__(self, multistore, key): - self._multistore = multistore - self._key = key - - def acquire_lock(self): - """Acquires any lock necessary to access this Storage. - - This lock is not reentrant. - """ - self._multistore._lock() - - def release_lock(self): - """Release the Storage lock. - - Trying to release a lock that isn't held will result in a - RuntimeError. - """ - self._multistore._unlock() - - def locked_get(self): - """Retrieve credential. - - The Storage lock must be held when this is called. - - Returns: - oauth2client.client.Credentials - """ - credential = self._multistore._get_credential(self._key) - if credential: - credential.set_store(self) - return credential - - def locked_put(self, credentials): - """Write a credential. - - The Storage lock must be held when this is called. - - Args: - credentials: Credentials, the credentials to store. - """ - self._multistore._update_credential(self._key, credentials) - - def locked_delete(self): - """Delete a credential. - - The Storage lock must be held when this is called. - - Args: - credentials: Credentials, the credentials to store. - """ - self._multistore._delete_credential(self._key) - - def _create_file_if_needed(self): - """Create an empty file if necessary. - - This method will not initialize the file. Instead it implements a - simple version of "touch" to ensure the file has been created. - """ - if not os.path.exists(self._file.filename()): - old_umask = os.umask(0177) - try: - open(self._file.filename(), 'a+b').close() - finally: - os.umask(old_umask) - - def _lock(self): - """Lock the entire multistore.""" - self._thread_lock.acquire() - self._file.open_and_lock() - if not self._file.is_locked(): - self._read_only = True - if self._warn_on_readonly: - logger.warn('The credentials file (%s) is not writable. Opening in ' - 'read-only mode. Any refreshed credentials will only be ' - 'valid for this run.' % self._file.filename()) - if os.path.getsize(self._file.filename()) == 0: - logger.debug('Initializing empty multistore file') - # The multistore is empty so write out an empty file. - self._data = {} - self._write() - elif not self._read_only or self._data is None: - # Only refresh the data if we are read/write or we haven't - # cached the data yet. If we are readonly, we assume is isn't - # changing out from under us and that we only have to read it - # once. This prevents us from whacking any new access keys that - # we have cached in memory but were unable to write out. - self._refresh_data_cache() - - def _unlock(self): - """Release the lock on the multistore.""" - self._file.unlock_and_close() - self._thread_lock.release() - - def _locked_json_read(self): - """Get the raw content of the multistore file. - - The multistore must be locked when this is called. - - Returns: - The contents of the multistore decoded as JSON. - """ - assert self._thread_lock.locked() - self._file.file_handle().seek(0) - return simplejson.load(self._file.file_handle()) - - def _locked_json_write(self, data): - """Write a JSON serializable data structure to the multistore. - - The multistore must be locked when this is called. - - Args: - data: The data to be serialized and written. - """ - assert self._thread_lock.locked() - if self._read_only: - return - self._file.file_handle().seek(0) - simplejson.dump(data, self._file.file_handle(), sort_keys=True, indent=2) - self._file.file_handle().truncate() - - def _refresh_data_cache(self): - """Refresh the contents of the multistore. - - The multistore must be locked when this is called. - - Raises: - NewerCredentialStoreError: Raised when a newer client has written the - store. - """ - self._data = {} - try: - raw_data = self._locked_json_read() - except Exception: - logger.warn('Credential data store could not be loaded. ' - 'Will ignore and overwrite.') - return - - version = 0 - try: - version = raw_data['file_version'] - except Exception: - logger.warn('Missing version for credential data store. It may be ' - 'corrupt or an old version. Overwriting.') - if version > 1: - raise NewerCredentialStoreError( - 'Credential file has file_version of %d. ' - 'Only file_version of 1 is supported.' % version) - - credentials = [] - try: - credentials = raw_data['data'] - except (TypeError, KeyError): - pass - - for cred_entry in credentials: - try: - (key, credential) = self._decode_credential_from_json(cred_entry) - self._data[key] = credential - except: - # If something goes wrong loading a credential, just ignore it - logger.info('Error decoding credential, skipping', exc_info=True) - - def _decode_credential_from_json(self, cred_entry): - """Load a credential from our JSON serialization. - - Args: - cred_entry: A dict entry from the data member of our format - - Returns: - (key, cred) where the key is the key tuple and the cred is the - OAuth2Credential object. - """ - raw_key = cred_entry['key'] - key = util.dict_to_tuple_key(raw_key) - credential = None - credential = Credentials.new_from_json(simplejson.dumps(cred_entry['credential'])) - return (key, credential) - - def _write(self): - """Write the cached data back out. - - The multistore must be locked. - """ - raw_data = {'file_version': 1} - raw_creds = [] - raw_data['data'] = raw_creds - for (cred_key, cred) in self._data.items(): - raw_key = dict(cred_key) - raw_cred = simplejson.loads(cred.to_json()) - raw_creds.append({'key': raw_key, 'credential': raw_cred}) - self._locked_json_write(raw_data) - - def _get_all_credential_keys(self): - """Gets all the registered credential keys in the multistore. - - Returns: - A list of dictionaries corresponding to all the keys currently registered - """ - return [dict(key) for key in self._data.keys()] - - def _get_credential(self, key): - """Get a credential from the multistore. - - The multistore must be locked. - - Args: - key: The key used to retrieve the credential - - Returns: - The credential specified or None if not present - """ - return self._data.get(key, None) - - def _update_credential(self, key, cred): - """Update a credential and write the multistore. - - This must be called when the multistore is locked. - - Args: - key: The key used to retrieve the credential - cred: The OAuth2Credential to update/set - """ - self._data[key] = cred - self._write() - - def _delete_credential(self, key): - """Delete a credential and write the multistore. - - This must be called when the multistore is locked. - - Args: - key: The key used to retrieve the credential - """ - try: - del self._data[key] - except KeyError: - pass - self._write() - - def _get_storage(self, key): - """Get a Storage object to get/set a credential. - - This Storage is a 'view' into the multistore. - - Args: - key: The key used to retrieve the credential - - Returns: - A Storage object that can be used to get/set this cred - """ - return self._Storage(self, key) diff --git a/third_party/oauth2client/old_run.py b/third_party/oauth2client/old_run.py deleted file mode 100644 index da23358486..0000000000 --- a/third_party/oauth2client/old_run.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright (C) 2013 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module holds the old run() function which is deprecated, the -tools.run_flow() function should be used in its place.""" - - -import logging -import socket -import sys -import webbrowser - -import gflags - -from oauth2client import client -from oauth2client import util -from tools import ClientRedirectHandler -from tools import ClientRedirectServer - - -FLAGS = gflags.FLAGS - -gflags.DEFINE_boolean('auth_local_webserver', True, - ('Run a local web server to handle redirects during ' - 'OAuth authorization.')) - -gflags.DEFINE_string('auth_host_name', 'localhost', - ('Host name to use when running a local web server to ' - 'handle redirects during OAuth authorization.')) - -gflags.DEFINE_multi_int('auth_host_port', [8080, 8090], - ('Port to use when running a local web server to ' - 'handle redirects during OAuth authorization.')) - - -@util.positional(2) -def run(flow, storage, http=None): - """Core code for a command-line application. - - The run() function is called from your application and runs through all - the steps to obtain credentials. It takes a Flow argument and attempts to - open an authorization server page in the user's default web browser. The - server asks the user to grant your application access to the user's data. - If the user grants access, the run() function returns new credentials. The - new credentials are also stored in the Storage argument, which updates the - file associated with the Storage object. - - It presumes it is run from a command-line application and supports the - following flags: - - --auth_host_name: Host name to use when running a local web server - to handle redirects during OAuth authorization. - (default: 'localhost') - - --auth_host_port: Port to use when running a local web server to handle - redirects during OAuth authorization.; - repeat this option to specify a list of values - (default: '[8080, 8090]') - (an integer) - - --[no]auth_local_webserver: Run a local web server to handle redirects - during OAuth authorization. - (default: 'true') - - Since it uses flags make sure to initialize the gflags module before - calling run(). - - Args: - flow: Flow, an OAuth 2.0 Flow to step through. - storage: Storage, a Storage to store the credential in. - http: An instance of httplib2.Http.request - or something that acts like it. - - Returns: - Credentials, the obtained credential. - """ - logging.warning('This function, oauth2client.tools.run(), and the use of ' - 'the gflags library are deprecated and will be removed in a future ' - 'version of the library.') - if FLAGS.auth_local_webserver: - success = False - port_number = 0 - for port in FLAGS.auth_host_port: - port_number = port - try: - httpd = ClientRedirectServer((FLAGS.auth_host_name, port), - ClientRedirectHandler) - except socket.error, e: - pass - else: - success = True - break - FLAGS.auth_local_webserver = success - if not success: - print 'Failed to start a local webserver listening on either port 8080' - print 'or port 9090. Please check your firewall settings and locally' - print 'running programs that may be blocking or using those ports.' - print - print 'Falling back to --noauth_local_webserver and continuing with', - print 'authorization.' - print - - if FLAGS.auth_local_webserver: - oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number) - else: - oauth_callback = client.OOB_CALLBACK_URN - flow.redirect_uri = oauth_callback - authorize_url = flow.step1_get_authorize_url() - - if FLAGS.auth_local_webserver: - webbrowser.open(authorize_url, new=1, autoraise=True) - print 'Your browser has been opened to visit:' - print - print ' ' + authorize_url - print - print 'If your browser is on a different machine then exit and re-run' - print 'this application with the command-line parameter ' - print - print ' --noauth_local_webserver' - print - else: - print 'Go to the following link in your browser:' - print - print ' ' + authorize_url - print - - code = None - if FLAGS.auth_local_webserver: - httpd.handle_request() - if 'error' in httpd.query_params: - sys.exit('Authentication request was rejected.') - if 'code' in httpd.query_params: - code = httpd.query_params['code'] - else: - print 'Failed to find "code" in the query parameters of the redirect.' - sys.exit('Try running with --noauth_local_webserver.') - else: - code = raw_input('Enter verification code: ').strip() - - try: - credential = flow.step2_exchange(code, http=http) - except client.FlowExchangeError, e: - sys.exit('Authentication has failed: %s' % e) - - storage.put(credential) - credential.set_store(storage) - print 'Authentication successful.' - - return credential diff --git a/third_party/oauth2client/tools.py b/third_party/oauth2client/tools.py deleted file mode 100644 index 12c68bf0f0..0000000000 --- a/third_party/oauth2client/tools.py +++ /dev/null @@ -1,243 +0,0 @@ -# Copyright (C) 2013 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Command-line tools for authenticating via OAuth 2.0 - -Do the OAuth 2.0 Web Server dance for a command line application. Stores the -generated credentials in a common file that is used by other example apps in -the same directory. -""" - -__author__ = 'jcgregorio@google.com (Joe Gregorio)' -__all__ = ['argparser', 'run_flow', 'run', 'message_if_missing'] - - -import BaseHTTPServer -import argparse -import httplib2 -import logging -import os -import socket -import sys -import webbrowser - -from oauth2client import client -from oauth2client import file -from oauth2client import util - -try: - from urlparse import parse_qsl -except ImportError: - from cgi import parse_qsl - -_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0 - -To make this sample run you will need to populate the client_secrets.json file -found at: - - %s - -with information from the APIs Console . - -""" - -# run_parser is an ArgumentParser that contains command-line options expected -# by tools.run(). Pass it in as part of the 'parents' argument to your own -# ArgumentParser. -argparser = argparse.ArgumentParser(add_help=False) -argparser.add_argument('--auth_host_name', default='localhost', - help='Hostname when running a local web server.') -argparser.add_argument('--noauth_local_webserver', action='store_true', - default=False, help='Do not run a local web server.') -argparser.add_argument('--auth_host_port', default=[8080, 8090], type=int, - nargs='*', help='Port web server should listen on.') -argparser.add_argument('--logging_level', default='ERROR', - choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', - 'CRITICAL'], - help='Set the logging level of detail.') - - -class ClientRedirectServer(BaseHTTPServer.HTTPServer): - """A server to handle OAuth 2.0 redirects back to localhost. - - Waits for a single request and parses the query parameters - into query_params and then stops serving. - """ - query_params = {} - - -class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): - """A handler for OAuth 2.0 redirects back to localhost. - - Waits for a single request and parses the query parameters - into the servers query_params and then stops serving. - """ - - def do_GET(s): - """Handle a GET request. - - Parses the query parameters and prints a message - if the flow has completed. Note that we can't detect - if an error occurred. - """ - s.send_response(200) - s.send_header("Content-type", "text/html") - s.end_headers() - query = s.path.split('?', 1)[-1] - query = dict(parse_qsl(query)) - s.server.query_params = query - s.wfile.write("Authentication Status") - s.wfile.write("

The authentication flow has completed.

") - s.wfile.write("") - - def log_message(self, format, *args): - """Do not log messages to stdout while running as command line program.""" - pass - - -@util.positional(3) -def run_flow(flow, storage, flags, http=None): - """Core code for a command-line application. - - The run() function is called from your application and runs through all the - steps to obtain credentials. It takes a Flow argument and attempts to open an - authorization server page in the user's default web browser. The server asks - the user to grant your application access to the user's data. If the user - grants access, the run() function returns new credentials. The new credentials - are also stored in the Storage argument, which updates the file associated - with the Storage object. - - It presumes it is run from a command-line application and supports the - following flags: - - --auth_host_name: Host name to use when running a local web server - to handle redirects during OAuth authorization. - (default: 'localhost') - - --auth_host_port: Port to use when running a local web server to handle - redirects during OAuth authorization.; - repeat this option to specify a list of values - (default: '[8080, 8090]') - (an integer) - - --[no]auth_local_webserver: Run a local web server to handle redirects - during OAuth authorization. - (default: 'true') - - The tools module defines an ArgumentParser the already contains the flag - definitions that run() requires. You can pass that ArgumentParser to your - ArgumentParser constructor: - - parser = argparse.ArgumentParser(description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter, - parents=[tools.run_parser]) - flags = parser.parse_args(argv) - - Args: - flow: Flow, an OAuth 2.0 Flow to step through. - storage: Storage, a Storage to store the credential in. - flags: argparse.ArgumentParser, the command-line flags. - http: An instance of httplib2.Http.request - or something that acts like it. - - Returns: - Credentials, the obtained credential. - """ - logging.getLogger().setLevel(getattr(logging, flags.logging_level)) - if not flags.noauth_local_webserver: - success = False - port_number = 0 - for port in flags.auth_host_port: - port_number = port - try: - httpd = ClientRedirectServer((flags.auth_host_name, port), - ClientRedirectHandler) - except socket.error, e: - pass - else: - success = True - break - flags.noauth_local_webserver = not success - if not success: - print 'Failed to start a local webserver listening on either port 8080' - print 'or port 9090. Please check your firewall settings and locally' - print 'running programs that may be blocking or using those ports.' - print - print 'Falling back to --noauth_local_webserver and continuing with', - print 'authorization.' - print - - if not flags.noauth_local_webserver: - oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number) - else: - oauth_callback = client.OOB_CALLBACK_URN - flow.redirect_uri = oauth_callback - authorize_url = flow.step1_get_authorize_url() - - if not flags.noauth_local_webserver: - webbrowser.open(authorize_url, new=1, autoraise=True) - print 'Your browser has been opened to visit:' - print - print ' ' + authorize_url - print - print 'If your browser is on a different machine then exit and re-run this' - print 'application with the command-line parameter ' - print - print ' --noauth_local_webserver' - print - else: - print 'Go to the following link in your browser:' - print - print ' ' + authorize_url - print - - code = None - if not flags.noauth_local_webserver: - httpd.handle_request() - if 'error' in httpd.query_params: - sys.exit('Authentication request was rejected.') - if 'code' in httpd.query_params: - code = httpd.query_params['code'] - else: - print 'Failed to find "code" in the query parameters of the redirect.' - sys.exit('Try running with --noauth_local_webserver.') - else: - code = raw_input('Enter verification code: ').strip() - - try: - credential = flow.step2_exchange(code, http=http) - except client.FlowExchangeError, e: - sys.exit('Authentication has failed: %s' % e) - - storage.put(credential) - credential.set_store(storage) - print 'Authentication successful.' - - return credential - - -def message_if_missing(filename): - """Helpful message to display if the CLIENT_SECRETS file is missing.""" - - return _CLIENT_SECRETS_MESSAGE % filename - -try: - from old_run import run - from old_run import FLAGS -except ImportError: - def run(*args, **kwargs): - raise NotImplementedError( - 'The gflags library must be installed to use tools.run(). ' - 'Please install gflags or preferrably switch to using ' - 'tools.run_flow().') diff --git a/third_party/oauth2client/util.py b/third_party/oauth2client/util.py deleted file mode 100644 index 90dff155bc..0000000000 --- a/third_party/oauth2client/util.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -"""Common utility library.""" - -__author__ = ['rafek@google.com (Rafe Kaplan)', - 'guido@google.com (Guido van Rossum)', -] -__all__ = [ - 'positional', - 'POSITIONAL_WARNING', - 'POSITIONAL_EXCEPTION', - 'POSITIONAL_IGNORE', -] - -import inspect -import logging -import types -import urllib -import urlparse - -try: - from urlparse import parse_qsl -except ImportError: - from cgi import parse_qsl - -logger = logging.getLogger(__name__) - -POSITIONAL_WARNING = 'WARNING' -POSITIONAL_EXCEPTION = 'EXCEPTION' -POSITIONAL_IGNORE = 'IGNORE' -POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, - POSITIONAL_IGNORE]) - -positional_parameters_enforcement = POSITIONAL_WARNING - -def positional(max_positional_args): - """A decorator to declare that only the first N arguments my be positional. - - This decorator makes it easy to support Python 3 style key-word only - parameters. For example, in Python 3 it is possible to write: - - def fn(pos1, *, kwonly1=None, kwonly1=None): - ... - - All named parameters after * must be a keyword: - - fn(10, 'kw1', 'kw2') # Raises exception. - fn(10, kwonly1='kw1') # Ok. - - Example: - To define a function like above, do: - - @positional(1) - def fn(pos1, kwonly1=None, kwonly2=None): - ... - - If no default value is provided to a keyword argument, it becomes a required - keyword argument: - - @positional(0) - def fn(required_kw): - ... - - This must be called with the keyword parameter: - - fn() # Raises exception. - fn(10) # Raises exception. - fn(required_kw=10) # Ok. - - When defining instance or class methods always remember to account for - 'self' and 'cls': - - class MyClass(object): - - @positional(2) - def my_method(self, pos1, kwonly1=None): - ... - - @classmethod - @positional(2) - def my_method(cls, pos1, kwonly1=None): - ... - - The positional decorator behavior is controlled by - util.positional_parameters_enforcement, which may be set to - POSITIONAL_EXCEPTION, POSITIONAL_WARNING or POSITIONAL_IGNORE to raise an - exception, log a warning, or do nothing, respectively, if a declaration is - violated. - - Args: - max_positional_arguments: Maximum number of positional arguments. All - parameters after the this index must be keyword only. - - Returns: - A decorator that prevents using arguments after max_positional_args from - being used as positional parameters. - - Raises: - TypeError if a key-word only argument is provided as a positional - parameter, but only if util.positional_parameters_enforcement is set to - POSITIONAL_EXCEPTION. - """ - def positional_decorator(wrapped): - def positional_wrapper(*args, **kwargs): - if len(args) > max_positional_args: - plural_s = '' - if max_positional_args != 1: - plural_s = 's' - message = '%s() takes at most %d positional argument%s (%d given)' % ( - wrapped.__name__, max_positional_args, plural_s, len(args)) - if positional_parameters_enforcement == POSITIONAL_EXCEPTION: - raise TypeError(message) - elif positional_parameters_enforcement == POSITIONAL_WARNING: - logger.warning(message) - else: # IGNORE - pass - return wrapped(*args, **kwargs) - return positional_wrapper - - if isinstance(max_positional_args, (int, long)): - return positional_decorator - else: - args, _, _, defaults = inspect.getargspec(max_positional_args) - return positional(len(args) - len(defaults))(max_positional_args) - - -def scopes_to_string(scopes): - """Converts scope value to a string. - - If scopes is a string then it is simply passed through. If scopes is an - iterable then a string is returned that is all the individual scopes - concatenated with spaces. - - Args: - scopes: string or iterable of strings, the scopes. - - Returns: - The scopes formatted as a single string. - """ - if isinstance(scopes, types.StringTypes): - return scopes - else: - return ' '.join(scopes) - - -def dict_to_tuple_key(dictionary): - """Converts a dictionary to a tuple that can be used as an immutable key. - - The resulting key is always sorted so that logically equivalent dictionaries - always produce an identical tuple for a key. - - Args: - dictionary: the dictionary to use as the key. - - Returns: - A tuple representing the dictionary in it's naturally sorted ordering. - """ - return tuple(sorted(dictionary.items())) - - -def _add_query_parameter(url, name, value): - """Adds a query parameter to a url. - - Replaces the current value if it already exists in the URL. - - Args: - url: string, url to add the query parameter to. - name: string, query parameter name. - value: string, query parameter value. - - Returns: - Updated query parameter. Does not update the url if value is None. - """ - if value is None: - return url - else: - parsed = list(urlparse.urlparse(url)) - q = dict(parse_qsl(parsed[4])) - q[name] = value - parsed[4] = urllib.urlencode(q) - return urlparse.urlunparse(parsed) diff --git a/third_party/oauth2client/xsrfutil.py b/third_party/oauth2client/xsrfutil.py deleted file mode 100644 index 7e1fe5c813..0000000000 --- a/third_party/oauth2client/xsrfutil.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/python2.5 -# -# Copyright 2010 the Melange authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Helper methods for creating & verifying XSRF tokens.""" - -__authors__ = [ - '"Doug Coker" ', - '"Joe Gregorio" ', -] - - -import base64 -import hmac -import os # for urandom -import time - -from oauth2client import util - - -# Delimiter character -DELIMITER = ':' - -# 1 hour in seconds -DEFAULT_TIMEOUT_SECS = 1*60*60 - -@util.positional(2) -def generate_token(key, user_id, action_id="", when=None): - """Generates a URL-safe token for the given user, action, time tuple. - - Args: - key: secret key to use. - user_id: the user ID of the authenticated user. - action_id: a string identifier of the action they requested - authorization for. - when: the time in seconds since the epoch at which the user was - authorized for this action. If not set the current time is used. - - Returns: - A string XSRF protection token. - """ - when = when or int(time.time()) - digester = hmac.new(key) - digester.update(str(user_id)) - digester.update(DELIMITER) - digester.update(action_id) - digester.update(DELIMITER) - digester.update(str(when)) - digest = digester.digest() - - token = base64.urlsafe_b64encode('%s%s%d' % (digest, - DELIMITER, - when)) - return token - - -@util.positional(3) -def validate_token(key, token, user_id, action_id="", current_time=None): - """Validates that the given token authorizes the user for the action. - - Tokens are invalid if the time of issue is too old or if the token - does not match what generateToken outputs (i.e. the token was forged). - - Args: - key: secret key to use. - token: a string of the token generated by generateToken. - user_id: the user ID of the authenticated user. - action_id: a string identifier of the action they requested - authorization for. - - Returns: - A boolean - True if the user is authorized for the action, False - otherwise. - """ - if not token: - return False - try: - decoded = base64.urlsafe_b64decode(str(token)) - token_time = long(decoded.split(DELIMITER)[-1]) - except (TypeError, ValueError): - return False - if current_time is None: - current_time = time.time() - # If the token is too old it's not valid. - if current_time - token_time > DEFAULT_TIMEOUT_SECS: - return False - - # The given token should match the generated one with the same time. - expected_token = generate_token(key, user_id, action_id=action_id, - when=token_time) - if len(token) != len(expected_token): - return False - - # Perform constant time comparison to avoid timing attacks - different = 0 - for x, y in zip(token, expected_token): - different |= ord(x) ^ ord(y) - if different: - return False - - return True