From cbfb0a731047566477e6a64d0b45096cf1919b78 Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Thu, 28 Feb 2019 04:05:55 -0800 Subject: [PATCH] Delete duplicated oauth2client library (#852) --- src/oauth2client/oauth2client/__init__.py | 24 - src/oauth2client/oauth2client/_helpers.py | 341 --- .../oauth2client/_openssl_crypt.py | 136 -- src/oauth2client/oauth2client/_pkce.py | 67 - .../oauth2client/_pure_python_crypt.py | 184 -- .../oauth2client/_pycrypto_crypt.py | 124 - src/oauth2client/oauth2client/client.py | 2170 ----------------- .../oauth2client/clientsecrets.py | 173 -- .../oauth2client/contrib/__init__.py | 6 - .../oauth2client/contrib/_appengine_ndb.py | 163 -- .../oauth2client/contrib/_metadata.py | 118 - .../oauth2client/contrib/appengine.py | 910 ------- .../oauth2client/contrib/devshell.py | 152 -- .../contrib/dictionary_storage.py | 65 - .../contrib/django_util/__init__.py | 489 ---- .../oauth2client/contrib/django_util/apps.py | 32 - .../contrib/django_util/decorators.py | 145 -- .../contrib/django_util/models.py | 82 - .../contrib/django_util/signals.py | 28 - .../oauth2client/contrib/django_util/site.py | 26 - .../contrib/django_util/storage.py | 81 - .../oauth2client/contrib/django_util/views.py | 193 -- .../oauth2client/contrib/flask_util.py | 557 ----- src/oauth2client/oauth2client/contrib/gce.py | 156 -- .../oauth2client/contrib/keyring_storage.py | 95 - .../contrib/multiprocess_file_storage.py | 355 --- .../oauth2client/contrib/sqlalchemy.py | 173 -- .../oauth2client/contrib/xsrfutil.py | 101 - src/oauth2client/oauth2client/crypt.py | 250 -- src/oauth2client/oauth2client/file.py | 95 - .../oauth2client/service_account.py | 685 ------ src/oauth2client/oauth2client/tools.py | 256 -- src/oauth2client/oauth2client/transport.py | 285 --- 33 files changed, 8717 deletions(-) delete mode 100644 src/oauth2client/oauth2client/__init__.py delete mode 100644 src/oauth2client/oauth2client/_helpers.py delete mode 100644 src/oauth2client/oauth2client/_openssl_crypt.py delete mode 100644 src/oauth2client/oauth2client/_pkce.py delete mode 100644 src/oauth2client/oauth2client/_pure_python_crypt.py delete mode 100644 src/oauth2client/oauth2client/_pycrypto_crypt.py delete mode 100644 src/oauth2client/oauth2client/client.py delete mode 100644 src/oauth2client/oauth2client/clientsecrets.py delete mode 100644 src/oauth2client/oauth2client/contrib/__init__.py delete mode 100644 src/oauth2client/oauth2client/contrib/_appengine_ndb.py delete mode 100644 src/oauth2client/oauth2client/contrib/_metadata.py delete mode 100644 src/oauth2client/oauth2client/contrib/appengine.py delete mode 100644 src/oauth2client/oauth2client/contrib/devshell.py delete mode 100644 src/oauth2client/oauth2client/contrib/dictionary_storage.py delete mode 100644 src/oauth2client/oauth2client/contrib/django_util/__init__.py delete mode 100644 src/oauth2client/oauth2client/contrib/django_util/apps.py delete mode 100644 src/oauth2client/oauth2client/contrib/django_util/decorators.py delete mode 100644 src/oauth2client/oauth2client/contrib/django_util/models.py delete mode 100644 src/oauth2client/oauth2client/contrib/django_util/signals.py delete mode 100644 src/oauth2client/oauth2client/contrib/django_util/site.py delete mode 100644 src/oauth2client/oauth2client/contrib/django_util/storage.py delete mode 100644 src/oauth2client/oauth2client/contrib/django_util/views.py delete mode 100644 src/oauth2client/oauth2client/contrib/flask_util.py delete mode 100644 src/oauth2client/oauth2client/contrib/gce.py delete mode 100644 src/oauth2client/oauth2client/contrib/keyring_storage.py delete mode 100644 src/oauth2client/oauth2client/contrib/multiprocess_file_storage.py delete mode 100644 src/oauth2client/oauth2client/contrib/sqlalchemy.py delete mode 100644 src/oauth2client/oauth2client/contrib/xsrfutil.py delete mode 100644 src/oauth2client/oauth2client/crypt.py delete mode 100644 src/oauth2client/oauth2client/file.py delete mode 100644 src/oauth2client/oauth2client/service_account.py delete mode 100644 src/oauth2client/oauth2client/tools.py delete mode 100644 src/oauth2client/oauth2client/transport.py diff --git a/src/oauth2client/oauth2client/__init__.py b/src/oauth2client/oauth2client/__init__.py deleted file mode 100644 index 92bc191d..00000000 --- a/src/oauth2client/oauth2client/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -"""Client library for using OAuth2, especially with Google APIs.""" - -__version__ = '4.1.3' - -GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth' -GOOGLE_DEVICE_URI = 'https://oauth2.googleapis.com/device/code' -GOOGLE_REVOKE_URI = 'https://oauth2.googleapis.com/revoke' -GOOGLE_TOKEN_URI = 'https://oauth2.googleapis.com/token' -GOOGLE_TOKEN_INFO_URI = 'https://oauth2.googleapis.com/tokeninfo' - diff --git a/src/oauth2client/oauth2client/_helpers.py b/src/oauth2client/oauth2client/_helpers.py deleted file mode 100644 index e9123971..00000000 --- a/src/oauth2client/oauth2client/_helpers.py +++ /dev/null @@ -1,341 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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 functions for commonly used utilities.""" - -import base64 -import functools -import inspect -import json -import logging -import os -import warnings - -import six -from six.moves import urllib - - -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 - -_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.' -_IS_DIR_MESSAGE = '{0}: Is a directory' -_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory' - - -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 keyword-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 - ``_helpers.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 - _helpers.positional_parameters_enforcement is set to - POSITIONAL_EXCEPTION. - """ - - def positional_decorator(wrapped): - @functools.wraps(wrapped) - def positional_wrapper(*args, **kwargs): - if len(args) > max_positional_args: - plural_s = '' - if max_positional_args != 1: - plural_s = 's' - message = ('{function}() takes at most {args_max} positional ' - 'argument{plural} ({args_given} given)'.format( - function=wrapped.__name__, - args_max=max_positional_args, - args_given=len(args), - plural=plural_s)) - if positional_parameters_enforcement == POSITIONAL_EXCEPTION: - raise TypeError(message) - elif positional_parameters_enforcement == POSITIONAL_WARNING: - logger.warning(message) - return wrapped(*args, **kwargs) - return positional_wrapper - - if isinstance(max_positional_args, six.integer_types): - 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, six.string_types): - return scopes - else: - return ' '.join(scopes) - - -def string_to_scopes(scopes): - """Converts stringifed scope value to a list. - - If scopes is a list then it is simply passed through. If scopes is an - string then a list of each individual scope is returned. - - Args: - scopes: a string or iterable of strings, the scopes. - - Returns: - The scopes in a list. - """ - if not scopes: - return [] - elif isinstance(scopes, six.string_types): - return scopes.split(' ') - else: - return scopes - - -def parse_unique_urlencoded(content): - """Parses unique key-value parameters from urlencoded content. - - Args: - content: string, URL-encoded key-value pairs. - - Returns: - dict, The key-value pairs from ``content``. - - Raises: - ValueError: if one of the keys is repeated. - """ - urlencoded_params = urllib.parse.parse_qs(content) - params = {} - for key, value in six.iteritems(urlencoded_params): - if len(value) != 1: - msg = ('URL-encoded content contains a repeated value:' - '%s -> %s' % (key, ', '.join(value))) - raise ValueError(msg) - params[key] = value[0] - return params - - -def update_query_params(uri, params): - """Updates a URI with new query parameters. - - If a given key from ``params`` is repeated in the ``uri``, then - the URI will be considered invalid and an error will occur. - - If the URI is valid, then each value from ``params`` will - replace the corresponding value in the query parameters (if - it exists). - - 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 = urllib.parse.urlparse(uri) - query_params = parse_unique_urlencoded(parts.query) - query_params.update(params) - new_query = urllib.parse.urlencode(query_params) - new_parts = parts._replace(query=new_query) - return urllib.parse.urlunparse(new_parts) - - -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: - return update_query_params(url, {name: value}) - - -def validate_file(filename): - if os.path.islink(filename): - raise IOError(_SYM_LINK_MESSAGE.format(filename)) - elif os.path.isdir(filename): - raise IOError(_IS_DIR_MESSAGE.format(filename)) - elif not os.path.isfile(filename): - warnings.warn(_MISSING_FILE_MESSAGE.format(filename)) - - -def _parse_pem_key(raw_key_input): - """Identify and extract PEM keys. - - Determines whether the given key is in the format of PEM key, and extracts - the relevant part of the key if it is. - - Args: - raw_key_input: The contents of a private key file (either PEM or - PKCS12). - - Returns: - string, The actual key if the contents are from a PEM file, or - else None. - """ - offset = raw_key_input.find(b'-----BEGIN ') - if offset != -1: - return raw_key_input[offset:] - - -def _json_encode(data): - return json.dumps(data, separators=(',', ':')) - - -def _to_bytes(value, encoding='ascii'): - """Converts a string value to bytes, if necessary. - - Unfortunately, ``six.b`` is insufficient for this task since in - Python2 it does not modify ``unicode`` objects. - - Args: - value: The string/bytes value to be converted. - encoding: The encoding to use to convert unicode to bytes. Defaults - to "ascii", which will not allow any characters from ordinals - larger than 127. Other useful values are "latin-1", which - which will only allows byte ordinals (up to 255) and "utf-8", - which will encode any unicode that needs to be. - - Returns: - The original value converted to bytes (if unicode) or as passed in - if it started out as bytes. - - Raises: - ValueError if the value could not be converted to bytes. - """ - result = (value.encode(encoding) - if isinstance(value, six.text_type) else value) - if isinstance(result, six.binary_type): - return result - else: - raise ValueError('{0!r} could not be converted to bytes'.format(value)) - - -def _from_bytes(value): - """Converts bytes to a string value, if necessary. - - Args: - value: The string/bytes value to be converted. - - Returns: - The original value converted to unicode (if bytes) or as passed in - if it started out as unicode. - - Raises: - ValueError if the value could not be converted to unicode. - """ - result = (value.decode('utf-8') - if isinstance(value, six.binary_type) else value) - if isinstance(result, six.text_type): - return result - else: - raise ValueError( - '{0!r} could not be converted to unicode'.format(value)) - - -def _urlsafe_b64encode(raw_bytes): - raw_bytes = _to_bytes(raw_bytes, encoding='utf-8') - return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=') - - -def _urlsafe_b64decode(b64string): - # Guard against unicode strings, which base64 can't handle. - b64string = _to_bytes(b64string) - padded = b64string + b'=' * (4 - len(b64string) % 4) - return base64.urlsafe_b64decode(padded) diff --git a/src/oauth2client/oauth2client/_openssl_crypt.py b/src/oauth2client/oauth2client/_openssl_crypt.py deleted file mode 100644 index 77fac743..00000000 --- a/src/oauth2client/oauth2client/_openssl_crypt.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. -"""OpenSSL Crypto-related routines for oauth2client.""" - -from OpenSSL import crypto - -from oauth2client import _helpers - - -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 or bytes, The message to verify. If string, will be - encoded to bytes as utf-8. - signature: string or bytes, The signature on the message. If string, - will be encoded to bytes as utf-8. - - Returns: - True if message was signed by the private key associated with the - public key that this object was constructed with. - """ - message = _helpers._to_bytes(message, encoding='utf-8') - signature = _helpers._to_bytes(signature, encoding='utf-8') - try: - crypto.verify(self._pubkey, signature, message, 'sha256') - return True - except crypto.Error: - 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. - """ - key_pem = _helpers._to_bytes(key_pem) - 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: bytes, Message to be signed. - - Returns: - string, The signature of the message for the given key. - """ - message = _helpers._to_bytes(message, encoding='utf-8') - return crypto.sign(self._key, message, 'sha256') - - @staticmethod - def from_string(key, password=b'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. - """ - key = _helpers._to_bytes(key) - parsed_pem_key = _helpers._parse_pem_key(key) - if parsed_pem_key: - pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key) - else: - password = _helpers._to_bytes(password, encoding='utf-8') - pkey = crypto.load_pkcs12(key, password).get_privatekey() - return OpenSSLSigner(pkey) - - -def pkcs12_key_as_pem(private_key_bytes, private_key_password): - """Convert the contents of a PKCS#12 key to PEM using pyOpenSSL. - - Args: - private_key_bytes: Bytes. PKCS#12 key in DER format. - private_key_password: String. Password for PKCS#12 key. - - Returns: - String. PEM contents of ``private_key_bytes``. - """ - private_key_password = _helpers._to_bytes(private_key_password) - pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password) - return crypto.dump_privatekey(crypto.FILETYPE_PEM, - pkcs12.get_privatekey()) diff --git a/src/oauth2client/oauth2client/_pkce.py b/src/oauth2client/oauth2client/_pkce.py deleted file mode 100644 index e4952d8c..00000000 --- a/src/oauth2client/oauth2client/_pkce.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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 functions for implementing Proof Key for Code Exchange (PKCE) by OAuth -Public Clients - -See RFC7636. -""" - -import base64 -import hashlib -import os - - -def code_verifier(n_bytes=64): - """ - Generates a 'code_verifier' as described in section 4.1 of RFC 7636. - - This is a 'high-entropy cryptographic random string' that will be - impractical for an attacker to guess. - - Args: - n_bytes: integer between 31 and 96, inclusive. default: 64 - number of bytes of entropy to include in verifier. - - Returns: - Bytestring, representing urlsafe base64-encoded random data. - """ - verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=') - # https://tools.ietf.org/html/rfc7636#section-4.1 - # minimum length of 43 characters and a maximum length of 128 characters. - if len(verifier) < 43: - raise ValueError("Verifier too short. n_bytes must be > 30.") - elif len(verifier) > 128: - raise ValueError("Verifier too long. n_bytes must be < 97.") - else: - return verifier - - -def code_challenge(verifier): - """ - Creates a 'code_challenge' as described in section 4.2 of RFC 7636 - by taking the sha256 hash of the verifier and then urlsafe - base64-encoding it. - - Args: - verifier: bytestring, representing a code_verifier as generated by - code_verifier(). - - Returns: - Bytestring, representing a urlsafe base64-encoded sha256 hash digest, - without '=' padding. - """ - digest = hashlib.sha256(verifier).digest() - return base64.urlsafe_b64encode(digest).rstrip(b'=') diff --git a/src/oauth2client/oauth2client/_pure_python_crypt.py b/src/oauth2client/oauth2client/_pure_python_crypt.py deleted file mode 100644 index 2c5d43aa..00000000 --- a/src/oauth2client/oauth2client/_pure_python_crypt.py +++ /dev/null @@ -1,184 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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. - -"""Pure Python crypto-related routines for oauth2client. - -Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages -to parse PEM files storing PKCS#1 or PKCS#8 keys as well as -certificates. -""" - -from pyasn1.codec.der import decoder -from pyasn1_modules import pem -from pyasn1_modules.rfc2459 import Certificate -from pyasn1_modules.rfc5208 import PrivateKeyInfo -import rsa -import six - -from oauth2client import _helpers - - -_PKCS12_ERROR = r"""\ -PKCS12 format is not supported by the RSA library. -Either install PyOpenSSL, or please convert .p12 format -to .pem format: - $ cat key.p12 | \ - > openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \ - > openssl rsa > key.pem -""" - -_POW2 = (128, 64, 32, 16, 8, 4, 2, 1) -_PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----', - '-----END RSA PRIVATE KEY-----') -_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----', - '-----END PRIVATE KEY-----') -_PKCS8_SPEC = PrivateKeyInfo() - - -def _bit_list_to_bytes(bit_list): - """Converts an iterable of 1's and 0's to bytes. - - Combines the list 8 at a time, treating each group of 8 bits - as a single byte. - """ - num_bits = len(bit_list) - byte_vals = bytearray() - for start in six.moves.xrange(0, num_bits, 8): - curr_bits = bit_list[start:start + 8] - char_val = sum(val * digit - for val, digit in zip(_POW2, curr_bits)) - byte_vals.append(char_val) - return bytes(byte_vals) - - -class RsaVerifier(object): - """Verifies the signature on a message. - - Args: - pubkey: rsa.key.PublicKey (or equiv), The public key to verify with. - """ - - def __init__(self, pubkey): - self._pubkey = pubkey - - def verify(self, message, signature): - """Verifies a message against a signature. - - Args: - message: string or bytes, The message to verify. If string, will be - encoded to bytes as utf-8. - signature: string or bytes, The signature on the message. If - string, will be encoded to bytes as utf-8. - - Returns: - True if message was signed by the private key associated with the - public key that this object was constructed with. - """ - message = _helpers._to_bytes(message, encoding='utf-8') - try: - return rsa.pkcs1.verify(message, signature, self._pubkey) - except (ValueError, rsa.pkcs1.VerificationError): - return False - - @classmethod - def from_string(cls, key_pem, is_x509_cert): - """Construct an RsaVerifier 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: - RsaVerifier instance. - - Raises: - ValueError: if the key_pem can't be parsed. In either case, error - will begin with 'No PEM start marker'. If - ``is_x509_cert`` is True, will fail to find the - "-----BEGIN CERTIFICATE-----" error, otherwise fails - to find "-----BEGIN RSA PUBLIC KEY-----". - """ - key_pem = _helpers._to_bytes(key_pem) - if is_x509_cert: - der = rsa.pem.load_pem(key_pem, 'CERTIFICATE') - asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate()) - if remaining != b'': - raise ValueError('Unused bytes', remaining) - - cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo'] - key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey']) - pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER') - else: - pubkey = rsa.PublicKey.load_pkcs1(key_pem, 'PEM') - return cls(pubkey) - - -class RsaSigner(object): - """Signs messages with a private key. - - Args: - pkey: rsa.key.PrivateKey (or equiv), The private key to sign with. - """ - - def __init__(self, pkey): - self._key = pkey - - def sign(self, message): - """Signs a message. - - Args: - message: bytes, Message to be signed. - - Returns: - string, The signature of the message for the given key. - """ - message = _helpers._to_bytes(message, encoding='utf-8') - return rsa.pkcs1.sign(message, self._key, 'SHA-256') - - @classmethod - def from_string(cls, key, password='notasecret'): - """Construct an RsaSigner instance from a string. - - Args: - key: string, private key in PEM format. - password: string, password for private key file. Unused for PEM - files. - - Returns: - RsaSigner instance. - - Raises: - ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in - PEM format. - """ - key = _helpers._from_bytes(key) # pem expects str in Py3 - marker_id, key_bytes = pem.readPemBlocksFromFile( - six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER) - - if marker_id == 0: - pkey = rsa.key.PrivateKey.load_pkcs1(key_bytes, - format='DER') - elif marker_id == 1: - key_info, remaining = decoder.decode( - key_bytes, asn1Spec=_PKCS8_SPEC) - if remaining != b'': - raise ValueError('Unused bytes', remaining) - pkey_info = key_info.getComponentByName('privateKey') - pkey = rsa.key.PrivateKey.load_pkcs1(pkey_info.asOctets(), - format='DER') - else: - raise ValueError('No key could be detected.') - - return cls(pkey) diff --git a/src/oauth2client/oauth2client/_pycrypto_crypt.py b/src/oauth2client/oauth2client/_pycrypto_crypt.py deleted file mode 100644 index fd2ce0cd..00000000 --- a/src/oauth2client/oauth2client/_pycrypto_crypt.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. -"""pyCrypto Crypto-related routines for oauth2client.""" - -from Crypto.Hash import SHA256 -from Crypto.PublicKey import RSA -from Crypto.Signature import PKCS1_v1_5 -from Crypto.Util.asn1 import DerSequence - -from oauth2client import _helpers - - -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 or bytes, The message to verify. If string, will be - encoded to bytes as utf-8. - signature: string or bytes, 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. - """ - message = _helpers._to_bytes(message, encoding='utf-8') - return PKCS1_v1_5.new(self._pubkey).verify( - SHA256.new(message), signature) - - @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. - """ - if is_x509_cert: - key_pem = _helpers._to_bytes(key_pem) - pemLines = key_pem.replace(b' ', b'').split() - certDer = _helpers._urlsafe_b64decode(b''.join(pemLines[1:-1])) - certSeq = DerSequence() - certSeq.decode(certDer) - tbsSeq = DerSequence() - tbsSeq.decode(certSeq[0]) - pubkey = RSA.importKey(tbsSeq[6]) - 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. - """ - message = _helpers._to_bytes(message, encoding='utf-8') - 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 the key isn't in PEM format. - """ - parsed_pem_key = _helpers._parse_pem_key(_helpers._to_bytes(key)) - if parsed_pem_key: - pkey = RSA.importKey(parsed_pem_key) - else: - raise NotImplementedError( - 'No key in PEM format was detected. This implementation ' - 'can only use the PyCrypto library for keys in PEM ' - 'format.') - return PyCryptoSigner(pkey) diff --git a/src/oauth2client/oauth2client/client.py b/src/oauth2client/oauth2client/client.py deleted file mode 100644 index 7618960e..00000000 --- a/src/oauth2client/oauth2client/client.py +++ /dev/null @@ -1,2170 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. -""" - -import collections -import copy -import datetime -import json -import logging -import os -import shutil -import socket -import sys -import tempfile - -import six -from six.moves import http_client -from six.moves import urllib - -import oauth2client -from oauth2client import _helpers -from oauth2client import _pkce -from oauth2client import clientsecrets -from oauth2client import transport - - -HAS_OPENSSL = False -HAS_CRYPTO = False -try: - from oauth2client import crypt - HAS_CRYPTO = True - HAS_OPENSSL = crypt.OpenSSLVerifier is not None -except ImportError: # pragma: NO COVER - pass - - -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_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' -# This symbol previously had a typo in the name; we keep the old name -# around for now, but will remove it in the future. -ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS - -# Constant to use for the out of band OAuth 2.0 flow. -OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' - -# The value representing user credentials. -AUTHORIZED_USER = 'authorized_user' - -# The value representing service account credentials. -SERVICE_ACCOUNT = 'service_account' - -# The environment variable pointing the file with local -# Application Default Credentials. -GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS' -# The ~/.config subdirectory containing gcloud credentials. Intended -# to be swapped out in tests. -_CLOUDSDK_CONFIG_DIRECTORY = 'gcloud' -# The environment variable name which can replace ~/.config if set. -_CLOUDSDK_CONFIG_ENV_VAR = 'CLOUDSDK_CONFIG' - -# The error message we show users when we can't find the Application -# Default Credentials. -ADC_HELP_MSG = ( - 'The Application Default Credentials are not available. They are ' - 'available if running in Google Compute Engine. Otherwise, the ' - 'environment variable ' + - GOOGLE_APPLICATION_CREDENTIALS + - ' must be defined pointing to a file defining the credentials. See ' - 'https://developers.google.com/accounts/docs/' - 'application-default-credentials for more information.') - -_WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json' - -# The access token along with the seconds in which it expires. -AccessTokenInfo = collections.namedtuple( - 'AccessTokenInfo', ['access_token', 'expires_in']) - -DEFAULT_ENV_NAME = 'UNKNOWN' - -# If set to True _get_environment avoid GCE check (_detect_gce_environment) -NO_GCE_CHECK = os.getenv('NO_GCE_CHECK', 'False') - -# Timeout in seconds to wait for the GCE metadata server when detecting the -# GCE environment. -try: - GCE_METADATA_TIMEOUT = int(os.getenv('GCE_METADATA_TIMEOUT', 3)) -except ValueError: # pragma: NO COVER - GCE_METADATA_TIMEOUT = 3 - -_SERVER_SOFTWARE = 'SERVER_SOFTWARE' -_GCE_METADATA_URI = 'http://' + os.getenv('GCE_METADATA_IP', '169.254.169.254') -_METADATA_FLAVOR_HEADER = 'metadata-flavor' # lowercase header -_DESIRED_METADATA_FLAVOR = 'Google' -_GCE_HEADERS = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR} - -# Expose utcnow() at module level to allow for -# easier testing (by replacing with a stub). -_UTCNOW = datetime.datetime.utcnow - -# NOTE: These names were previously defined in this module but have been -# moved into `oauth2client.transport`, -clean_headers = transport.clean_headers -MemoryCache = transport.MemoryCache -REFRESH_STATUS_CODES = transport.REFRESH_STATUS_CODES - - -class SETTINGS(object): - """Settings namespace for globally defined values.""" - env_name = None - - -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 HttpAccessTokenRefreshError(AccessTokenRefreshError): - """Error (with HTTP status) trying to refresh an expired access token.""" - def __init__(self, *args, **kwargs): - super(HttpAccessTokenRefreshError, self).__init__(*args) - self.status = kwargs.get('status') - - -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 not retrieve certificates for validation.""" - - -class NonAsciiHeaderError(Error): - """Header names and values must be ASCII strings.""" - - -class ApplicationDefaultCredentialsError(Error): - """Error retrieving the Application Default Credentials.""" - - -class OAuth2DeviceCodeError(Error): - """Error trying to retrieve a device code.""" - - -class CryptoUnavailableError(Error, NotImplementedError): - """Raised when a crypto library is required, but none is available.""" - - -def _parse_expiry(expiry): - if expiry and isinstance(expiry, datetime.datetime): - return expiry.strftime(EXPIRY_FORMAT) - else: - return 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 instantiated Credentials object. - """ - - NON_SERIALIZED_MEMBERS = frozenset(['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. - """ - raise NotImplementedError - - 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. - """ - raise NotImplementedError - - 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. - """ - raise NotImplementedError - - def apply(self, headers): - """Add the authorization to the headers. - - Args: - headers: dict, the headers to add the Authorization header to. - """ - raise NotImplementedError - - def _to_json(self, strip, to_serialize=None): - """Utility function that creates JSON repr. of a Credentials object. - - Args: - strip: array, An array of names of members to exclude from the - JSON. - to_serialize: dict, (Optional) The properties for this object - that will be serialized. This allows callers to - modify before serializing. - - Returns: - string, a JSON representation of this instance, suitable to pass to - from_json(). - """ - curr_type = self.__class__ - if to_serialize is None: - to_serialize = copy.copy(self.__dict__) - else: - # Assumes it is a str->str dictionary, so we don't deep copy. - to_serialize = copy.copy(to_serialize) - for member in strip: - if member in to_serialize: - del to_serialize[member] - to_serialize['token_expiry'] = _parse_expiry( - to_serialize.get('token_expiry')) - # Add in information we will need later to reconstitute this instance. - to_serialize['_class'] = curr_type.__name__ - to_serialize['_module'] = curr_type.__module__ - for key, val in to_serialize.items(): - if isinstance(val, bytes): - to_serialize[key] = val.decode('utf-8') - if isinstance(val, set): - to_serialize[key] = list(val) - return json.dumps(to_serialize) - - 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(self.NON_SERIALIZED_MEMBERS) - - @classmethod - def new_from_json(cls, json_data): - """Utility class method to instantiate a Credentials subclass from JSON. - - Expects the JSON string to have been produced by to_json(). - - Args: - json_data: string or bytes, JSON from to_json(). - - Returns: - An instance of the subclass of Credentials that was serialized with - to_json(). - """ - json_data_as_unicode = _helpers._from_bytes(json_data) - data = json.loads(json_data_as_unicode) - # Find and call the right classmethod from_json() to restore - # the object. - module_name = data['_module'] - try: - module_obj = __import__(module_name) - except ImportError: - # In case there's an object from the old package structure, - # update it - module_name = module_name.replace('.googleapiclient', '') - module_obj = __import__(module_name) - - module_obj = __import__(module_name, - fromlist=module_name.split('.')[:-1]) - kls = getattr(module_obj, data['_class']) - return kls.from_json(json_data_as_unicode) - - @classmethod - def from_json(cls, unused_data): - """Instantiate a Credentials object from a JSON description of it. - - The JSON should have been produced by calling .to_json() on the object. - - Args: - unused_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 __init__(self, lock=None): - """Create a Storage instance. - - Args: - lock: An optional threading.Lock-like object. Must implement at - least acquire() and release(). Does not need to be - re-entrant. - """ - self._lock = lock - - def acquire_lock(self): - """Acquires any lock necessary to access this Storage. - - This lock is not reentrant. - """ - if self._lock is not None: - 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 in the case of a threading.Lock or multiprocessing.Lock. - """ - if self._lock is not None: - self._lock.release() - - def locked_get(self): - """Retrieve credential. - - The Storage lock must be held when this is called. - - Returns: - oauth2client.client.Credentials - """ - raise NotImplementedError - - 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. - """ - raise NotImplementedError - - def locked_delete(self): - """Delete a credential. - - The Storage lock must be held when this is called. - """ - raise NotImplementedError - - 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() - - -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. - """ - - @_helpers.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, scopes=None, - token_info_uri=None, id_token_jwt=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. - scopes: list, authorized scopes for these credentials. - token_info_uri: string, the URI for the token info endpoint. - Defaults to None; scopes can not be refreshed if - this is None. - id_token_jwt: string, the encoded and signed identity JWT. The - decoded version of this is stored in id_token. - - 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.id_token_jwt = id_token_jwt - self.token_response = token_response - self.scopes = set(_helpers.string_to_scopes(scopes or [])) - self.token_info_uri = token_info_uri - - # 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.Authentication - 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()'. - """ - transport.wrap_http_for_auth(self, http) - 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) - - 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) - - 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 has_scopes(self, scopes): - """Verify that the credentials are authorized for the given scopes. - - Returns True if the credentials authorized scopes contain all of the - scopes given. - - Args: - scopes: list or string, the scopes to check. - - Notes: - There are cases where the credentials are unaware of which scopes - are authorized. Notably, credentials obtained and stored before - this code was added will not have scopes, AccessTokenCredentials do - not have scopes. In both cases, you can use refresh_scopes() to - obtain the canonical set of scopes. - """ - scopes = _helpers.string_to_scopes(scopes) - return set(scopes).issubset(self.scopes) - - def retrieve_scopes(self, http): - """Retrieves the canonical list of scopes for this access token. - - Gets the scopes from the OAuth2 provider. - - Args: - http: httplib2.Http, an http object to be used to make the refresh - request. - - Returns: - A set of strings containing the canonical list of scopes. - """ - self._retrieve_scopes(http) - return self.scopes - - @classmethod - def from_json(cls, json_data): - """Instantiate a Credentials object from a JSON description of it. - - The JSON should have been produced by calling .to_json() on the object. - - Args: - json_data: string or bytes, JSON to deserialize. - - Returns: - An instance of a Credentials subclass. - """ - data = json.loads(_helpers._from_bytes(json_data)) - if (data.get('token_expiry') and - not isinstance(data['token_expiry'], datetime.datetime)): - try: - data['token_expiry'] = datetime.datetime.strptime( - data['token_expiry'], EXPIRY_FORMAT) - except ValueError: - 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), - id_token_jwt=data.get('id_token_jwt', None), - token_response=data.get('token_response', None), - scopes=data.get('scopes', None), - token_info_uri=data.get('token_info_uri', 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 = _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 get_access_token(self, http=None): - """Return the access token and its expiration information. - - If the token does not exist, get one. - If the token expired, refresh it. - """ - if not self.access_token or self.access_token_expired: - if not http: - http = transport.get_http_object() - self.refresh(http) - return AccessTokenInfo(access_token=self.access_token, - expires_in=self._expires_in()) - - def set_store(self, store): - """Set the Storage for the credential. - - Args: - store: Storage, an implementation of Storage 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 _expires_in(self): - """Return the number of seconds until this token expires. - - If token_expiry is in the past, this method will return 0, meaning the - token has already expired. - - If token_expiry is None, this method will return None. Note that - returning 0 in such a case would not be fair: the token may still be - valid; we just don't know anything about it. - """ - if self.token_expiry: - now = _UTCNOW() - if self.token_expiry > now: - time_delta = self.token_expiry - now - # TODO(orestica): return time_delta.total_seconds() - # once dropping support for Python 2.6 - return time_delta.days * 86400 + time_delta.seconds - else: - return 0 - - 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.parse.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): - """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: an object to be used to make HTTP requests. - - Raises: - HttpAccessTokenRefreshError: When the refresh fails. - """ - if not self.store: - self._do_refresh_request(http) - 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 and - not new_cred.access_token_expired): - logger.info('Updated access_token read from Storage') - self._updateFromCredential(new_cred) - else: - self._do_refresh_request(http) - finally: - self.store.release_lock() - - def _do_refresh_request(self, http): - """Refresh the access_token using the refresh_token. - - Args: - http: an object to be used to make HTTP requests. - - Raises: - HttpAccessTokenRefreshError: When the refresh fails. - """ - body = self._generate_refresh_request_body() - headers = self._generate_refresh_request_headers() - - logger.info('Refreshing access_token') - resp, content = transport.request( - http, self.token_uri, method='POST', - body=body, headers=headers) - content = _helpers._from_bytes(content) - if resp.status == http_client.OK: - d = json.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: - delta = datetime.timedelta(seconds=int(d['expires_in'])) - self.token_expiry = delta + _UTCNOW() - else: - self.token_expiry = None - if 'id_token' in d: - self.id_token = _extract_id_token(d['id_token']) - self.id_token_jwt = d['id_token'] - else: - self.id_token = None - self.id_token_jwt = None - # On temporary refresh errors, the user does not actually have to - # re-authorize, so we unflag here. - self.invalid = False - 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 {0}.'.format(resp.status) - try: - d = json.loads(content) - if 'error' in d: - error_msg = d['error'] - if 'error_description' in d: - error_msg += ': ' + d['error_description'] - self.invalid = True - if self.store is not None: - self.store.locked_put(self) - except (TypeError, ValueError): - pass - raise HttpAccessTokenRefreshError(error_msg, status=resp.status) - - def _revoke(self, http): - """Revokes this credential and deletes the stored copy (if it exists). - - Args: - http: an object to be used to make HTTP requests. - """ - self._do_revoke(http, self.refresh_token or self.access_token) - - def _do_revoke(self, http, token): - """Revokes this credential and deletes the stored copy (if it exists). - - Args: - http: an object to be used to make HTTP requests. - 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 = _helpers.update_query_params( - self.revoke_uri, query_params) - resp, content = transport.request(http, token_revoke_uri) - if resp.status == http_client.METHOD_NOT_ALLOWED: - body = urllib.parse.urlencode(query_params) - resp, content = transport.request(http, token_revoke_uri, - method='POST', body=body) - if resp.status == http_client.OK: - self.invalid = True - else: - error_msg = 'Invalid response {0}.'.format(resp.status) - try: - d = json.loads(_helpers._from_bytes(content)) - if 'error' in d: - error_msg = d['error'] - except (TypeError, ValueError): - pass - raise TokenRevokeError(error_msg) - - if self.store: - self.store.delete() - - def _retrieve_scopes(self, http): - """Retrieves the list of authorized scopes from the OAuth2 provider. - - Args: - http: an object to be used to make HTTP requests. - """ - self._do_retrieve_scopes(http, self.access_token) - - def _do_retrieve_scopes(self, http, token): - """Retrieves the list of authorized scopes from the OAuth2 provider. - - Args: - http: an object to be used to make HTTP requests. - token: A string used as the token to identify the credentials to - the provider. - - Raises: - Error: When refresh fails, indicating the the access token is - invalid. - """ - logger.info('Refreshing scopes') - query_params = {'access_token': token, 'fields': 'scope'} - token_info_uri = _helpers.update_query_params( - self.token_info_uri, query_params) - resp, content = transport.request(http, token_info_uri) - content = _helpers._from_bytes(content) - if resp.status == http_client.OK: - d = json.loads(content) - self.scopes = set(_helpers.string_to_scopes(d.get('scope', ''))) - else: - error_msg = 'Invalid response {0}.'.format(resp.status) - try: - d = json.loads(content) - if 'error_description' in d: - error_msg = d['error_description'] - except (TypeError, ValueError): - pass - raise Error(error_msg) - - -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) - - Raises: - 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, json_data): - data = json.loads(_helpers._from_bytes(json_data)) - retval = AccessTokenCredentials( - data['access_token'], - data['user_agent']) - return retval - - def _refresh(self, http): - """Refreshes the access token. - - Args: - http: unused HTTP object. - - Raises: - AccessTokenCredentialsError: always - """ - raise AccessTokenCredentialsError( - 'The access_token is expired or invalid and can\'t be refreshed.') - - def _revoke(self, http): - """Revokes the access_token and deletes the store if available. - - Args: - http: an object to be used to make HTTP requests. - """ - self._do_revoke(http, self.access_token) - - -def _detect_gce_environment(): - """Determine if the current environment is Compute Engine. - - Returns: - Boolean indicating whether or not the current environment is Google - Compute Engine. - """ - # NOTE: The explicit ``timeout`` is a workaround. The underlying - # issue is that resolving an unknown host on some networks will take - # 20-30 seconds; making this timeout short fixes the issue, but - # could lead to false negatives in the event that we are on GCE, but - # the metadata resolution was particularly slow. The latter case is - # "unlikely". - http = transport.get_http_object(timeout=GCE_METADATA_TIMEOUT) - try: - response, _ = transport.request( - http, _GCE_METADATA_URI, headers=_GCE_HEADERS) - return ( - response.status == http_client.OK and - response.get(_METADATA_FLAVOR_HEADER) == _DESIRED_METADATA_FLAVOR) - except socket.error: # socket.timeout or socket.error(64, 'Host is down') - logger.info('Timeout attempting to reach GCE metadata service.') - return False - - -def _in_gae_environment(): - """Detects if the code is running in the App Engine environment. - - Returns: - True if running in the GAE environment, False otherwise. - """ - if SETTINGS.env_name is not None: - return SETTINGS.env_name in ('GAE_PRODUCTION', 'GAE_LOCAL') - - try: - import google.appengine # noqa: unused import - except ImportError: - pass - else: - server_software = os.environ.get(_SERVER_SOFTWARE, '') - if server_software.startswith('Google App Engine/'): - SETTINGS.env_name = 'GAE_PRODUCTION' - return True - elif server_software.startswith('Development/'): - SETTINGS.env_name = 'GAE_LOCAL' - return True - - return False - - -def _in_gce_environment(): - """Detect if the code is running in the Compute Engine environment. - - Returns: - True if running in the GCE environment, False otherwise. - """ - if SETTINGS.env_name is not None: - return SETTINGS.env_name == 'GCE_PRODUCTION' - - if NO_GCE_CHECK != 'True' and _detect_gce_environment(): - SETTINGS.env_name = 'GCE_PRODUCTION' - return True - return False - - -class GoogleCredentials(OAuth2Credentials): - """Application Default Credentials for use in calling Google APIs. - - The Application Default Credentials are being constructed as a function of - the environment where the code is being run. - More details can be found on this page: - https://developers.google.com/accounts/docs/application-default-credentials - - Here is an example of how to use the Application Default Credentials for a - service that requires authentication:: - - from googleapiclient.discovery import build - from oauth2client.client import GoogleCredentials - - credentials = GoogleCredentials.get_application_default() - service = build('compute', 'v1', credentials=credentials) - - PROJECT = 'bamboo-machine-422' - ZONE = 'us-central1-a' - request = service.instances().list(project=PROJECT, zone=ZONE) - response = request.execute() - - print(response) - """ - - NON_SERIALIZED_MEMBERS = ( - frozenset(['_private_key']) | - OAuth2Credentials.NON_SERIALIZED_MEMBERS) - """Members that aren't serialized when object is converted to JSON.""" - - def __init__(self, access_token, client_id, client_secret, refresh_token, - token_expiry, token_uri, user_agent, - revoke_uri=oauth2client.GOOGLE_REVOKE_URI): - """Create an instance of GoogleCredentials. - - This constructor is not usually called by the user, instead - GoogleCredentials objects are instantiated by - GoogleCredentials.from_stream() or - GoogleCredentials.get_application_default(). - - 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 - oauth2client.GOOGLE_REVOKE_URI; a token can't be - revoked if this is None. - """ - super(GoogleCredentials, self).__init__( - access_token, client_id, client_secret, refresh_token, - token_expiry, token_uri, user_agent, revoke_uri=revoke_uri) - - def create_scoped_required(self): - """Whether this Credentials object is scopeless. - - create_scoped(scopes) method needs to be called in order to create - a Credentials object for API calls. - """ - return False - - def create_scoped(self, scopes): - """Create a Credentials object for the given scopes. - - The Credentials type is preserved. - """ - return self - - @classmethod - def from_json(cls, json_data): - # TODO(issue 388): eliminate the circularity that is the reason for - # this non-top-level import. - from oauth2client import service_account - data = json.loads(_helpers._from_bytes(json_data)) - - # We handle service_account.ServiceAccountCredentials since it is a - # possible return type of GoogleCredentials.get_application_default() - if (data['_module'] == 'oauth2client.service_account' and - data['_class'] == 'ServiceAccountCredentials'): - return service_account.ServiceAccountCredentials.from_json(data) - elif (data['_module'] == 'oauth2client.service_account' and - data['_class'] == '_JWTAccessCredentials'): - return service_account._JWTAccessCredentials.from_json(data) - - token_expiry = _parse_expiry(data.get('token_expiry')) - google_credentials = cls( - data['access_token'], - data['client_id'], - data['client_secret'], - data['refresh_token'], - token_expiry, - data['token_uri'], - data['user_agent'], - revoke_uri=data.get('revoke_uri', None)) - google_credentials.invalid = data['invalid'] - return google_credentials - - @property - def serialization_data(self): - """Get the fields and values identifying the current credentials.""" - return { - 'type': 'authorized_user', - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'refresh_token': self.refresh_token - } - - @staticmethod - def _implicit_credentials_from_gae(): - """Attempts to get implicit credentials in Google App Engine env. - - If the current environment is not detected as App Engine, returns None, - indicating no Google App Engine credentials can be detected from the - current environment. - - Returns: - None, if not in GAE, else an appengine.AppAssertionCredentials - object. - """ - if not _in_gae_environment(): - return None - - return _get_application_default_credential_GAE() - - @staticmethod - def _implicit_credentials_from_gce(): - """Attempts to get implicit credentials in Google Compute Engine env. - - If the current environment is not detected as Compute Engine, returns - None, indicating no Google Compute Engine credentials can be detected - from the current environment. - - Returns: - None, if not in GCE, else a gce.AppAssertionCredentials object. - """ - if not _in_gce_environment(): - return None - - return _get_application_default_credential_GCE() - - @staticmethod - def _implicit_credentials_from_files(): - """Attempts to get implicit credentials from local credential files. - - First checks if the environment variable GOOGLE_APPLICATION_CREDENTIALS - is set with a filename and then falls back to a configuration file (the - "well known" file) associated with the 'gcloud' command line tool. - - Returns: - Credentials object associated with the - GOOGLE_APPLICATION_CREDENTIALS file or the "well known" file if - either exist. If neither file is define, returns None, indicating - no credentials from a file can detected from the current - environment. - """ - credentials_filename = _get_environment_variable_file() - if not credentials_filename: - credentials_filename = _get_well_known_file() - if os.path.isfile(credentials_filename): - extra_help = (' (produced automatically when running' - ' "gcloud auth login" command)') - else: - credentials_filename = None - else: - extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS + - ' environment variable)') - - if not credentials_filename: - return - - # If we can read the credentials from a file, we don't need to know - # what environment we are in. - SETTINGS.env_name = DEFAULT_ENV_NAME - - try: - return _get_application_default_credential_from_file( - credentials_filename) - except (ApplicationDefaultCredentialsError, ValueError) as error: - _raise_exception_for_reading_json(credentials_filename, - extra_help, error) - - @classmethod - def _get_implicit_credentials(cls): - """Gets credentials implicitly from the environment. - - Checks environment in order of precedence: - - Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to - a file with stored credentials information. - - Stored "well known" file associated with `gcloud` command line tool. - - Google App Engine (production and testing) - - Google Compute Engine production environment. - - Raises: - ApplicationDefaultCredentialsError: raised when the credentials - fail to be retrieved. - """ - # Environ checks (in order). - environ_checkers = [ - cls._implicit_credentials_from_files, - cls._implicit_credentials_from_gae, - cls._implicit_credentials_from_gce, - ] - - for checker in environ_checkers: - credentials = checker() - if credentials is not None: - return credentials - - # If no credentials, fail. - raise ApplicationDefaultCredentialsError(ADC_HELP_MSG) - - @staticmethod - def get_application_default(): - """Get the Application Default Credentials for the current environment. - - Raises: - ApplicationDefaultCredentialsError: raised when the credentials - fail to be retrieved. - """ - return GoogleCredentials._get_implicit_credentials() - - @staticmethod - def from_stream(credential_filename): - """Create a Credentials object by reading information from a file. - - It returns an object of type GoogleCredentials. - - Args: - credential_filename: the path to the file from where the - credentials are to be read - - Raises: - ApplicationDefaultCredentialsError: raised when the credentials - fail to be retrieved. - """ - if credential_filename and os.path.isfile(credential_filename): - try: - return _get_application_default_credential_from_file( - credential_filename) - except (ApplicationDefaultCredentialsError, ValueError) as error: - extra_help = (' (provided as parameter to the ' - 'from_stream() method)') - _raise_exception_for_reading_json(credential_filename, - extra_help, - error) - else: - raise ApplicationDefaultCredentialsError( - 'The parameter passed to the from_stream() ' - 'method should point to a file.') - - -def _save_private_file(filename, json_contents): - """Saves a file with read-write permissions on for the owner. - - Args: - filename: String. Absolute path to file. - json_contents: JSON serializable object to be saved. - """ - temp_filename = tempfile.mktemp() - file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600) - with os.fdopen(file_desc, 'w') as file_handle: - json.dump(json_contents, file_handle, sort_keys=True, - indent=2, separators=(',', ': ')) - shutil.move(temp_filename, filename) - - -def save_to_well_known_file(credentials, well_known_file=None): - """Save the provided GoogleCredentials to the well known file. - - Args: - credentials: the credentials to be saved to the well known file; - it should be an instance of GoogleCredentials - well_known_file: the name of the file where the credentials are to be - saved; this parameter is supposed to be used for - testing only - """ - # TODO(orestica): move this method to tools.py - # once the argparse import gets fixed (it is not present in Python 2.6) - - if well_known_file is None: - well_known_file = _get_well_known_file() - - config_dir = os.path.dirname(well_known_file) - if not os.path.isdir(config_dir): - raise OSError( - 'Config directory does not exist: {0}'.format(config_dir)) - - credentials_data = credentials.serialization_data - _save_private_file(well_known_file, credentials_data) - - -def _get_environment_variable_file(): - application_default_credential_filename = ( - os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, None)) - - if application_default_credential_filename: - if os.path.isfile(application_default_credential_filename): - return application_default_credential_filename - else: - raise ApplicationDefaultCredentialsError( - 'File ' + application_default_credential_filename + - ' (pointed by ' + - GOOGLE_APPLICATION_CREDENTIALS + - ' environment variable) does not exist!') - - -def _get_well_known_file(): - """Get the well known file produced by command 'gcloud auth login'.""" - # TODO(orestica): Revisit this method once gcloud provides a better way - # of pinpointing the exact location of the file. - default_config_dir = os.getenv(_CLOUDSDK_CONFIG_ENV_VAR) - if default_config_dir is None: - if os.name == 'nt': - try: - default_config_dir = os.path.join(os.environ['APPDATA'], - _CLOUDSDK_CONFIG_DIRECTORY) - except KeyError: - # This should never happen unless someone is really - # messing with things. - drive = os.environ.get('SystemDrive', 'C:') - default_config_dir = os.path.join(drive, '\\', - _CLOUDSDK_CONFIG_DIRECTORY) - else: - default_config_dir = os.path.join(os.path.expanduser('~'), - '.config', - _CLOUDSDK_CONFIG_DIRECTORY) - - return os.path.join(default_config_dir, _WELL_KNOWN_CREDENTIALS_FILE) - - -def _get_application_default_credential_from_file(filename): - """Build the Application Default Credentials from file.""" - # read the credentials from the file - with open(filename) as file_obj: - client_credentials = json.load(file_obj) - - credentials_type = client_credentials.get('type') - if credentials_type == AUTHORIZED_USER: - required_fields = set(['client_id', 'client_secret', 'refresh_token']) - elif credentials_type == SERVICE_ACCOUNT: - required_fields = set(['client_id', 'client_email', 'private_key_id', - 'private_key']) - else: - raise ApplicationDefaultCredentialsError( - "'type' field should be defined (and have one of the '" + - AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)") - - missing_fields = required_fields.difference(client_credentials.keys()) - - if missing_fields: - _raise_exception_for_missing_fields(missing_fields) - - if client_credentials['type'] == AUTHORIZED_USER: - return GoogleCredentials( - access_token=None, - client_id=client_credentials['client_id'], - client_secret=client_credentials['client_secret'], - refresh_token=client_credentials['refresh_token'], - token_expiry=None, - token_uri=oauth2client.GOOGLE_TOKEN_URI, - user_agent='Python client library') - else: # client_credentials['type'] == SERVICE_ACCOUNT - from oauth2client import service_account - return service_account._JWTAccessCredentials.from_json_keyfile_dict( - client_credentials) - - -def _raise_exception_for_missing_fields(missing_fields): - raise ApplicationDefaultCredentialsError( - 'The following field(s) must be defined: ' + ', '.join(missing_fields)) - - -def _raise_exception_for_reading_json(credential_file, - extra_help, - error): - raise ApplicationDefaultCredentialsError( - 'An error was encountered while reading json file: ' + - credential_file + extra_help + ': ' + str(error)) - - -def _get_application_default_credential_GAE(): - from oauth2client.contrib.appengine import AppAssertionCredentials - - return AppAssertionCredentials([]) - - -def _get_application_default_credential_GCE(): - from oauth2client.contrib.gce import AppAssertionCredentials - - return AppAssertionCredentials() - - -class AssertionCredentials(GoogleCredentials): - """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. - """ - - @_helpers.positional(2) - def __init__(self, assertion_type, user_agent=None, - token_uri=oauth2client.GOOGLE_TOKEN_URI, - revoke_uri=oauth2client.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.parse.urlencode({ - 'assertion': assertion, - 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', - }) - - return body - - def _generate_assertion(self): - """Generate assertion string to be used in the access token request.""" - raise NotImplementedError - - def _revoke(self, http): - """Revokes the access_token and deletes the store if available. - - Args: - http: an object to be used to make HTTP requests. - """ - self._do_revoke(http, self.access_token) - - def sign_blob(self, blob): - """Cryptographically sign a blob (of bytes). - - Args: - blob: bytes, Message to be signed. - - Returns: - tuple, A pair of the private key ID used to sign the blob and - the signed contents. - """ - raise NotImplementedError('This method is abstract.') - - -def _require_crypto_or_die(): - """Ensure we have a crypto library, or throw CryptoUnavailableError. - - The oauth2client.crypt module requires either PyCrypto or PyOpenSSL - to be available in order to function, but these are optional - dependencies. - """ - if not HAS_CRYPTO: - raise CryptoUnavailableError('No crypto library available') - - -@_helpers.positional(2) -def verify_id_token(id_token, audience, http=None, - cert_uri=ID_TOKEN_VERIFICATION_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. - CryptoUnavailableError: if no crypto library is available. - """ - _require_crypto_or_die() - if http is None: - http = transport.get_cached_http() - - resp, content = transport.request(http, cert_uri) - if resp.status == http_client.OK: - certs = json.loads(_helpers._from_bytes(content)) - return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) - else: - raise VerifyJwtTokenError('Status code: {0}'.format(resp.status)) - - -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 or bytestring, OAuth 2.0 id_token. - - Returns: - object, The deserialized JSON payload. - """ - if type(id_token) == bytes: - segments = id_token.split(b'.') - else: - segments = id_token.split(u'.') - - if len(segments) != 3: - raise VerifyJwtTokenError( - 'Wrong number of segments in token: {0}'.format(id_token)) - - return json.loads( - _helpers._from_bytes(_helpers._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 = {} - content = _helpers._from_bytes(content) - try: - resp = json.loads(content) - except Exception: - # different JSON libs raise different exceptions, - # so we just do a catch-all here - resp = _helpers.parse_unique_urlencoded(content) - - # some providers respond with 'expires', others with 'expires_in' - if resp and 'expires' in resp: - resp['expires_in'] = resp.pop('expires') - - return resp - - -@_helpers.positional(4) -def credentials_from_code(client_id, client_secret, scope, code, - redirect_uri='postmessage', http=None, - user_agent=None, - token_uri=oauth2client.GOOGLE_TOKEN_URI, - auth_uri=oauth2client.GOOGLE_AUTH_URI, - revoke_uri=oauth2client.GOOGLE_REVOKE_URI, - device_uri=oauth2client.GOOGLE_DEVICE_URI, - token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI, - pkce=False, - code_verifier=None): - """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 authorization 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. - device_uri: string, URI for device authorization endpoint. For - convenience defaults to Google's endpoints but any OAuth - 2.0 provider can be used. - pkce: boolean, default: False, Generate and include a "Proof Key - for Code Exchange" (PKCE) with your authorization and token - requests. This adds security for installed applications that - cannot protect a client_secret. See RFC 7636 for details. - code_verifier: bytestring or None, default: None, parameter passed - as part of the code exchange when pkce=True. If - None, a code_verifier will automatically be - generated as part of step1_get_authorize_url(). See - RFC 7636 for details. - - 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, - device_uri=device_uri, - token_info_uri=token_info_uri, - pkce=pkce, - code_verifier=code_verifier) - - credentials = flow.step2_exchange(code, http=http) - return credentials - - -@_helpers.positional(3) -def credentials_from_clientsecrets_and_code(filename, scope, code, - message=None, - redirect_uri='postmessage', - http=None, - cache=None, - device_uri=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. - device_uri: string, OAuth 2.0 device authorization endpoint - pkce: boolean, default: False, Generate and include a "Proof Key - for Code Exchange" (PKCE) with your authorization and token - requests. This adds security for installed applications that - cannot protect a client_secret. See RFC 7636 for details. - code_verifier: bytestring or None, default: None, parameter passed - as part of the code exchange when pkce=True. If - None, a code_verifier will automatically be - generated as part of step1_get_authorize_url(). See - RFC 7636 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, - device_uri=device_uri) - credentials = flow.step2_exchange(code, http=http) - return credentials - - -class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', ( - 'device_code', 'user_code', 'interval', 'verification_url', - 'user_code_expiry'))): - """Intermediate information the OAuth2 for devices flow.""" - - @classmethod - def FromResponse(cls, response): - """Create a DeviceFlowInfo from a server response. - - The response should be a dict containing entries as described here: - - http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1 - """ - # device_code, user_code, and verification_url are required. - kwargs = { - 'device_code': response['device_code'], - 'user_code': response['user_code'], - } - # The response may list the verification address as either - # verification_url or verification_uri, so we check for both. - verification_url = response.get( - 'verification_url', response.get('verification_uri')) - if verification_url is None: - raise OAuth2DeviceCodeError( - 'No verification_url provided in server response') - kwargs['verification_url'] = verification_url - # expires_in and interval are optional. - kwargs.update({ - 'interval': response.get('interval'), - 'user_code_expiry': None, - }) - if 'expires_in' in response: - kwargs['user_code_expiry'] = ( - _UTCNOW() + - datetime.timedelta(seconds=int(response['expires_in']))) - return cls(**kwargs) - - -def _oauth2_web_server_flow_params(kwargs): - """Configures redirect URI parameters for OAuth2WebServerFlow.""" - params = { - 'access_type': 'offline', - 'response_type': 'code', - } - - params.update(kwargs) - - # Check for the presence of the deprecated approval_prompt param and - # warn appropriately. - approval_prompt = params.get('approval_prompt') - if approval_prompt is not None: - logger.warning( - 'The approval_prompt parameter for OAuth2WebServerFlow is ' - 'deprecated. Please use the prompt parameter instead.') - - if approval_prompt == 'force': - logger.warning( - 'approval_prompt="force" has been adjusted to ' - 'prompt="consent"') - params['prompt'] = 'consent' - del params['approval_prompt'] - - return params - - -class OAuth2WebServerFlow(Flow): - """Does the Web Server Flow for OAuth 2.0. - - OAuth2WebServerFlow objects may be safely pickled and unpickled. - """ - - @_helpers.positional(4) - def __init__(self, client_id, - client_secret=None, - scope=None, - redirect_uri=None, - user_agent=None, - auth_uri=oauth2client.GOOGLE_AUTH_URI, - token_uri=oauth2client.GOOGLE_TOKEN_URI, - revoke_uri=oauth2client.GOOGLE_REVOKE_URI, - login_hint=None, - device_uri=oauth2client.GOOGLE_DEVICE_URI, - token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI, - authorization_header=None, - pkce=False, - code_verifier=None, - **kwargs): - """Constructor for OAuth2WebServerFlow. - - The kwargs argument is used to set extra query parameters on the - auth_uri. For example, the access_type and 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. - login_hint: string, Either an email address or domain. Passing this - hint will either pre-fill the email box on the sign-in - form or select the proper multi-login session, thereby - simplifying the login flow. - device_uri: string, URI for device authorization endpoint. For - convenience defaults to Google's endpoints but any - OAuth 2.0 provider can be used. - authorization_header: string, For use with OAuth 2.0 providers that - require a client to authenticate using a - header value instead of passing client_secret - in the POST body. - pkce: boolean, default: False, Generate and include a "Proof Key - for Code Exchange" (PKCE) with your authorization and token - requests. This adds security for installed applications that - cannot protect a client_secret. See RFC 7636 for details. - code_verifier: bytestring or None, default: None, parameter passed - as part of the code exchange when pkce=True. If - None, a code_verifier will automatically be - generated as part of step1_get_authorize_url(). See - RFC 7636 for details. - **kwargs: dict, The keyword arguments are all optional and required - parameters for the OAuth calls. - """ - # scope is a required argument, but to preserve backwards-compatibility - # we don't want to rearrange the positional arguments - if scope is None: - raise TypeError("The value of scope must not be None") - self.client_id = client_id - self.client_secret = client_secret - self.scope = _helpers.scopes_to_string(scope) - self.redirect_uri = redirect_uri - self.login_hint = login_hint - self.user_agent = user_agent - self.auth_uri = auth_uri - self.token_uri = token_uri - self.revoke_uri = revoke_uri - self.device_uri = device_uri - self.token_info_uri = token_info_uri - self.authorization_header = authorization_header - self._pkce = pkce - self.code_verifier = code_verifier - self.params = _oauth2_web_server_flow_params(kwargs) - - @_helpers.positional(1) - def step1_get_authorize_url(self, redirect_uri=None, state=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. - state: string, Opaque state string which is passed through the - OAuth2 flow and returned to the client as a query parameter - in the callback. - - 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, - } - if state is not None: - query_params['state'] = state - if self.login_hint is not None: - query_params['login_hint'] = self.login_hint - if self._pkce: - if not self.code_verifier: - self.code_verifier = _pkce.code_verifier() - challenge = _pkce.code_challenge(self.code_verifier) - query_params['code_challenge'] = challenge - query_params['code_challenge_method'] = 'S256' - - query_params.update(self.params) - return _helpers.update_query_params(self.auth_uri, query_params) - - @_helpers.positional(1) - def step1_get_device_and_user_codes(self, http=None): - """Returns a user code and the verification URL where to enter it - - Returns: - A user code as a string for the user to authorize the application - An URL as a string where the user has to enter the code - """ - if self.device_uri is None: - raise ValueError('The value of device_uri must not be None.') - - body = urllib.parse.urlencode({ - 'client_id': self.client_id, - '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 = transport.get_http_object() - - resp, content = transport.request( - http, self.device_uri, method='POST', body=body, headers=headers) - content = _helpers._from_bytes(content) - if resp.status == http_client.OK: - try: - flow_info = json.loads(content) - except ValueError as exc: - raise OAuth2DeviceCodeError( - 'Could not parse server response as JSON: "{0}", ' - 'error: "{1}"'.format(content, exc)) - return DeviceFlowInfo.FromResponse(flow_info) - else: - error_msg = 'Invalid response {0}.'.format(resp.status) - try: - error_dict = json.loads(content) - if 'error' in error_dict: - error_msg += ' Error: {0}'.format(error_dict['error']) - except ValueError: - # Couldn't decode a JSON response, stick with the - # default message. - pass - raise OAuth2DeviceCodeError(error_msg) - - @_helpers.positional(2) - def step2_exchange(self, code=None, http=None, device_flow_info=None): - """Exchanges a code for OAuth2Credentials. - - Args: - code: string, a dict-like object, or None. For a non-device - flow, this is either the response code as a string, or a - dictionary of query parameters to the redirect_uri. For a - device flow, this should be None. - http: httplib2.Http, optional http instance to use when fetching - credentials. - device_flow_info: DeviceFlowInfo, return value from step1 in the - case of a device flow. - - Returns: - An OAuth2Credentials object that can be used to authorize requests. - - Raises: - FlowExchangeError: if a problem occurred exchanging the code for a - refresh_token. - ValueError: if code and device_flow_info are both provided or both - missing. - """ - if code is None and device_flow_info is None: - raise ValueError('No code or device_flow_info provided.') - if code is not None and device_flow_info is not None: - raise ValueError('Cannot provide both code and device_flow_info.') - - if code is None: - code = device_flow_info.device_code - elif not isinstance(code, (six.string_types, six.binary_type)): - if 'code' not in code: - raise FlowExchangeError(code.get( - 'error', 'No code was supplied in the query parameters.')) - code = code['code'] - - post_data = { - 'client_id': self.client_id, - 'code': code, - 'scope': self.scope, - } - if self.client_secret is not None: - post_data['client_secret'] = self.client_secret - if self._pkce: - post_data['code_verifier'] = self.code_verifier - if device_flow_info is not None: - post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0' - else: - post_data['grant_type'] = 'authorization_code' - post_data['redirect_uri'] = self.redirect_uri - body = urllib.parse.urlencode(post_data) - headers = { - 'content-type': 'application/x-www-form-urlencoded', - } - if self.authorization_header is not None: - headers['Authorization'] = self.authorization_header - if self.user_agent is not None: - headers['user-agent'] = self.user_agent - - if http is None: - http = transport.get_http_object() - - resp, content = transport.request( - http, self.token_uri, method='POST', body=body, headers=headers) - d = _parse_exchange_token_response(content) - if resp.status == http_client.OK and 'access_token' in d: - access_token = d['access_token'] - refresh_token = d.get('refresh_token', None) - if not refresh_token: - logger.info( - 'Received token response with no refresh_token. Consider ' - "reauthenticating with prompt='consent'.") - token_expiry = None - if 'expires_in' in d: - delta = datetime.timedelta(seconds=int(d['expires_in'])) - token_expiry = delta + _UTCNOW() - - extracted_id_token = None - id_token_jwt = None - if 'id_token' in d: - extracted_id_token = _extract_id_token(d['id_token']) - id_token_jwt = 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=extracted_id_token, - id_token_jwt=id_token_jwt, token_response=d, scopes=self.scope, - token_info_uri=self.token_info_uri) - 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 = (str(d['error']) + - str(d.get('error_description', ''))) - else: - error_msg = 'Invalid response: {0}.'.format(str(resp.status)) - raise FlowExchangeError(error_msg) - - -@_helpers.positional(2) -def flow_from_clientsecrets(filename, scope, redirect_uri=None, - message=None, cache=None, login_hint=None, - device_uri=None, pkce=None, code_verifier=None, - prompt=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. - login_hint: string, Either an email address or domain. Passing this - hint will either pre-fill the email box on the sign-in form - or select the proper multi-login session, thereby - simplifying the login flow. - device_uri: string, URI for device authorization endpoint. For - convenience defaults to Google's endpoints but any - OAuth 2.0 provider can be used. - - 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'], - 'login_hint': login_hint, - } - revoke_uri = client_info.get('revoke_uri') - optional = ( - 'revoke_uri', - 'device_uri', - 'pkce', - 'code_verifier', - 'prompt' - ) - for param in optional: - if locals()[param] is not None: - constructor_kwargs[param] = locals()[param] - - return OAuth2WebServerFlow( - client_info['client_id'], client_info['client_secret'], - scope, **constructor_kwargs) - - except clientsecrets.InvalidClientSecretsError as e: - if message is not None: - if e.args: - message = ('The client secrets were invalid: ' - '\n{0}\n{1}'.format(e, message)) - sys.exit(message) - else: - raise - else: - raise UnknownClientSecretsFlowError( - 'This OAuth 2.0 flow is unsupported: {0!r}'.format(client_type)) diff --git a/src/oauth2client/oauth2client/clientsecrets.py b/src/oauth2client/oauth2client/clientsecrets.py deleted file mode 100644 index 1598142e..00000000 --- a/src/oauth2client/oauth2client/clientsecrets.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. -""" - -import json - -import six - - -# 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.""" - - -class InvalidClientSecretsError(Error): - """Format of ClientSecrets file is invalid.""" - - -def _validate_clientsecrets(clientsecrets_dict): - """Validate parsed client secrets from a file. - - Args: - clientsecrets_dict: dict, a dictionary holding the client secrets. - - Returns: - tuple, a string of the client type and the information parsed - from the file. - """ - _INVALID_FILE_FORMAT_MSG = ( - 'Invalid file format. See ' - 'https://developers.google.com/api-client-library/' - 'python/guide/aaa_client_secrets') - - if clientsecrets_dict is None: - raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG) - try: - (client_type, client_info), = clientsecrets_dict.items() - except (ValueError, AttributeError): - raise InvalidClientSecretsError( - _INVALID_FILE_FORMAT_MSG + ' ' - 'Expected a JSON object with a single property for a "web" or ' - '"installed" application') - - if client_type not in VALID_CLIENT: - raise InvalidClientSecretsError( - 'Unknown client type: {0}.'.format(client_type)) - - for prop_name in VALID_CLIENT[client_type]['required']: - if prop_name not in client_info: - raise InvalidClientSecretsError( - 'Missing property "{0}" in a client type of "{1}".'.format( - prop_name, client_type)) - for prop_name in VALID_CLIENT[client_type]['string']: - if client_info[prop_name].startswith('[['): - raise InvalidClientSecretsError( - 'Property "{0}" is not configured.'.format(prop_name)) - return client_type, client_info - - -def load(fp): - obj = json.load(fp) - return _validate_clientsecrets(obj) - - -def loads(s): - obj = json.loads(s) - return _validate_clientsecrets(obj) - - -def _loadfile(filename): - try: - with open(filename, 'r') as fp: - obj = json.load(fp) - except IOError as exc: - raise InvalidClientSecretsError('Error opening file', exc.filename, - exc.strerror, exc.errno) - 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 next(six.iteritems(obj)) diff --git a/src/oauth2client/oauth2client/contrib/__init__.py b/src/oauth2client/oauth2client/contrib/__init__.py deleted file mode 100644 index ecfd06c9..00000000 --- a/src/oauth2client/oauth2client/contrib/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Contributed modules. - -Contrib contains modules that are not considered part of the core oauth2client -library but provide additional functionality. These modules are intended to -make it easier to use oauth2client. -""" diff --git a/src/oauth2client/oauth2client/contrib/_appengine_ndb.py b/src/oauth2client/oauth2client/contrib/_appengine_ndb.py deleted file mode 100644 index c863e8f4..00000000 --- a/src/oauth2client/oauth2client/contrib/_appengine_ndb.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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. - -"""Google App Engine utilities helper. - -Classes that directly require App Engine's ndb library. Provided -as a separate module in case of failure to import ndb while -other App Engine libraries are present. -""" - -import logging - -from google.appengine.ext import ndb - -from oauth2client import client - - -NDB_KEY = ndb.Key -"""Key constant used by :mod:`oauth2client.contrib.appengine`.""" - -NDB_MODEL = ndb.Model -"""Model constant used by :mod:`oauth2client.contrib.appengine`.""" - -_LOGGER = logging.getLogger(__name__) - - -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' - - -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, client.Flow): - raise TypeError( - 'Property {0} must be convertible to a flow ' - 'instance; received: {1}.'.format(self._name, value)) - - -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, client.Credentials): - raise TypeError( - 'Property {0} must be convertible to a credentials ' - 'instance; received: {1}.'.format(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 = client.Credentials.new_from_json(value) - except ValueError: - credentials = None - return credentials - - -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' diff --git a/src/oauth2client/oauth2client/contrib/_metadata.py b/src/oauth2client/oauth2client/contrib/_metadata.py deleted file mode 100644 index 564cd398..00000000 --- a/src/oauth2client/oauth2client/contrib/_metadata.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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. - -"""Provides helper methods for talking to the Compute Engine metadata server. - -See https://cloud.google.com/compute/docs/metadata -""" - -import datetime -import json -import os - -from six.moves import http_client -from six.moves.urllib import parse as urlparse - -from oauth2client import _helpers -from oauth2client import client -from oauth2client import transport - - -METADATA_ROOT = 'http://{}/computeMetadata/v1/'.format( - os.getenv('GCE_METADATA_ROOT', 'metadata.google.internal')) -METADATA_HEADERS = {'Metadata-Flavor': 'Google'} - - -def get(http, path, root=METADATA_ROOT, recursive=None): - """Fetch a resource from the metadata server. - - Args: - http: an object to be used to make HTTP requests. - path: A string indicating the resource to retrieve. For example, - 'instance/service-accounts/default' - root: A string indicating the full path to the metadata server root. - recursive: A boolean indicating whether to do a recursive query of - metadata. See - https://cloud.google.com/compute/docs/metadata#aggcontents - - Returns: - A dictionary if the metadata server returns JSON, otherwise a string. - - Raises: - http_client.HTTPException if an error corrured while - retrieving metadata. - """ - url = urlparse.urljoin(root, path) - url = _helpers._add_query_parameter(url, 'recursive', recursive) - - response, content = transport.request( - http, url, headers=METADATA_HEADERS) - - if response.status == http_client.OK: - decoded = _helpers._from_bytes(content) - if response['content-type'] == 'application/json': - return json.loads(decoded) - else: - return decoded - else: - raise http_client.HTTPException( - 'Failed to retrieve {0} from the Google Compute Engine' - 'metadata service. Response:\n{1}'.format(url, response)) - - -def get_service_account_info(http, service_account='default'): - """Get information about a service account from the metadata server. - - Args: - http: an object to be used to make HTTP requests. - service_account: An email specifying the service account for which to - look up information. Default will be information for the "default" - service account of the current compute engine instance. - - Returns: - A dictionary with information about the specified service account, - for example: - - { - 'email': '...', - 'scopes': ['scope', ...], - 'aliases': ['default', '...'] - } - """ - return get( - http, - 'instance/service-accounts/{0}/'.format(service_account), - recursive=True) - - -def get_token(http, service_account='default'): - """Fetch an oauth token for the - - Args: - http: an object to be used to make HTTP requests. - service_account: An email specifying the service account this token - should represent. Default will be a token for the "default" service - account of the current compute engine instance. - - Returns: - A tuple of (access token, token expiration), where access token is the - access token as a string and token expiration is a datetime object - that indicates when the access token will expire. - """ - token_json = get( - http, - 'instance/service-accounts/{0}/token'.format(service_account)) - token_expiry = client._UTCNOW() + datetime.timedelta( - seconds=token_json['expires_in']) - return token_json['access_token'], token_expiry diff --git a/src/oauth2client/oauth2client/contrib/appengine.py b/src/oauth2client/oauth2client/contrib/appengine.py deleted file mode 100644 index c1326eeb..00000000 --- a/src/oauth2client/oauth2client/contrib/appengine.py +++ /dev/null @@ -1,910 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. -""" - -import cgi -import json -import logging -import os -import pickle -import threading - -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.webapp.util import login_required -import webapp2 as webapp - -import oauth2client -from oauth2client import _helpers -from oauth2client import client -from oauth2client import clientsecrets -from oauth2client import transport -from oauth2client.contrib import xsrfutil - -# This is a temporary fix for a Google internal issue. -try: - from oauth2client.contrib import _appengine_ndb -except ImportError: # pragma: NO COVER - _appengine_ndb = None - - -logger = logging.getLogger(__name__) - -OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' - -XSRF_MEMCACHE_ID = 'xsrf_secret_key' - -if _appengine_ndb is None: # pragma: NO COVER - CredentialsNDBModel = None - CredentialsNDBProperty = None - FlowNDBProperty = None - _NDB_KEY = None - _NDB_MODEL = None - SiteXsrfSecretKeyNDB = None -else: - CredentialsNDBModel = _appengine_ndb.CredentialsNDBModel - CredentialsNDBProperty = _appengine_ndb.CredentialsNDBProperty - FlowNDBProperty = _appengine_ndb.FlowNDBProperty - _NDB_KEY = _appengine_ndb.NDB_KEY - _NDB_MODEL = _appengine_ndb.NDB_MODEL - SiteXsrfSecretKeyNDB = _appengine_ndb.SiteXsrfSecretKeyNDB - - -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 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() - - -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(client.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. - """ - - @_helpers.positional(2) - def __init__(self, scope, **kwargs): - """Constructor for AppAssertionCredentials - - Args: - scope: string or iterable of strings, scope(s) of the credentials - being requested. - **kwargs: optional keyword args, including: - service_account_id: service account id of the application. If None - or unspecified, the default service account for - the app is used. - """ - self.scope = _helpers.scopes_to_string(scope) - self._kwargs = kwargs - self.service_account_id = kwargs.get('service_account_id', None) - self._service_account_email = None - - # 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): - data = json.loads(json_data) - return AppAssertionCredentials(data['scope']) - - def _refresh(self, http): - """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: unused HTTP object - - Raises: - AccessTokenRefreshError: When the refresh fails. - """ - try: - scopes = self.scope.split() - (token, _) = app_identity.get_access_token( - scopes, service_account_id=self.service_account_id) - except app_identity.Error as e: - raise client.AccessTokenRefreshError(str(e)) - self.access_token = token - - @property - def serialization_data(self): - raise NotImplementedError('Cannot serialize credentials ' - 'for Google App Engine.') - - def create_scoped_required(self): - return not self.scope - - def create_scoped(self, scopes): - return AppAssertionCredentials(scopes, **self._kwargs) - - def sign_blob(self, blob): - """Cryptographically sign a blob (of bytes). - - Implements abstract method - :meth:`oauth2client.client.AssertionCredentials.sign_blob`. - - Args: - blob: bytes, Message to be signed. - - Returns: - tuple, A pair of the private key ID used to sign the blob and - the signed contents. - """ - return app_identity.sign_blob(blob) - - @property - def service_account_email(self): - """Get the email for the current service account. - - Returns: - string, The email associated with the Google App Engine - service account. - """ - if self._service_account_email is None: - self._service_account_email = ( - app_identity.get_service_account_name()) - return self._service_account_email - - -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 = client.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, client.Flow): - raise db.BadValueError( - 'Property {0} must be convertible ' - 'to a FlowThreeLegged instance ({1})'.format(self.name, value)) - return super(FlowProperty, self).validate(value) - - def empty(self, value): - return not value - - -class CredentialsProperty(db.Property): - """App Engine datastore Property for Credentials. - - Utility property that allows easy storage and retrieval of - oauth2client.Credentials - """ - - # Tell what the user type is. - data_type = client.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 = client.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, client.Credentials): - raise db.BadValueError( - 'Property {0} must be convertible ' - 'to a Credentials instance ({1})'.format(self.name, value)) - return value - - -class StorageByKeyName(client.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. - """ - - @_helpers.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. - """ - super(StorageByKeyName, self).__init__() - - 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_MODEL 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: {0}.'.format(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) - - @db.non_transactional(allow_existing=True) - 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 = client.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 - - @db.non_transactional(allow_existing=True) - 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()) - - @db.non_transactional(allow_existing=True) - 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() - - -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. - - Returns: - The redirect URI, or None if XSRF token is not valid. - """ - uri, token = state.rsplit(':', 1) - if xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(), - action_id=uri): - return uri - else: - return None - - -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. - - :: - - 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) - - @_helpers.positional(4) - def __init__(self, client_id, client_secret, scope, - auth_uri=oauth2client.GOOGLE_AUTH_URI, - token_uri=oauth2client.GOOGLE_TOKEN_URI, - revoke_uri=oauth2client.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 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 = _helpers.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 client.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 = client.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, *args, **kwargs): - """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. - - Args: - *args: Positional arguments passed to httplib2.Http constructor. - **kwargs: Positional arguments passed to httplib2.Http constructor. - """ - return self.credentials.authorize( - transport.get_http_object(*args, **kwargs)) - - @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: {0}'.format( - _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 redirect_uri is None: - self.response.out.write( - 'The authorization request failed') - return - - if (decorator._token_response_param and - credentials.token_response): - resp_json = json.dumps(credentials.token_response) - redirect_uri = _helpers._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. - - :: - - 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 - - """ - - @_helpers.positional(3) - def __init__(self, filename, scope, message=None, cache=None, **kwargs): - """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. - **kwargs: dict, Keyword arguments are passed along as kwargs to - the OAuth2WebServerFlow constructor. - """ - client_type, client_info = clientsecrets.loadfile(filename, - cache=cache) - if client_type not in (clientsecrets.TYPE_WEB, - clientsecrets.TYPE_INSTALLED): - raise clientsecrets.InvalidClientSecretsError( - "OAuth2Decorator doesn't support this OAuth 2.0 flow.") - - constructor_kwargs = dict(kwargs) - constructor_kwargs.update({ - '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.' - - -@_helpers.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/src/oauth2client/oauth2client/contrib/devshell.py b/src/oauth2client/oauth2client/contrib/devshell.py deleted file mode 100644 index 691765f0..00000000 --- a/src/oauth2client/oauth2client/contrib/devshell.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright 2015 Google Inc. All Rights Reserved. -# -# 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 utitilies for Google Developer Shell environment.""" - -import datetime -import json -import os -import socket - -from oauth2client import _helpers -from oauth2client import client - -DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT' - - -class Error(Exception): - """Errors for this module.""" - pass - - -class CommunicationError(Error): - """Errors for communication with the Developer Shell server.""" - - -class NoDevshellServer(Error): - """Error when no Developer Shell server can be contacted.""" - - -# The request for credential information to the Developer Shell client socket -# is always an empty PBLite-formatted JSON object, so just define it as a -# constant. -CREDENTIAL_INFO_REQUEST_JSON = '[]' - - -class CredentialInfoResponse(object): - """Credential information response from Developer Shell server. - - The credential information response from Developer Shell socket is a - PBLite-formatted JSON array with fields encoded by their index in the - array: - - * Index 0 - user email - * Index 1 - default project ID. None if the project context is not known. - * Index 2 - OAuth2 access token. None if there is no valid auth context. - * Index 3 - Seconds until the access token expires. None if not present. - """ - - def __init__(self, json_string): - """Initialize the response data from JSON PBLite array.""" - pbl = json.loads(json_string) - if not isinstance(pbl, list): - raise ValueError('Not a list: ' + str(pbl)) - pbl_len = len(pbl) - self.user_email = pbl[0] if pbl_len > 0 else None - self.project_id = pbl[1] if pbl_len > 1 else None - self.access_token = pbl[2] if pbl_len > 2 else None - self.expires_in = pbl[3] if pbl_len > 3 else None - - -def _SendRecv(): - """Communicate with the Developer Shell server socket.""" - - port = int(os.getenv(DEVSHELL_ENV, 0)) - if port == 0: - raise NoDevshellServer() - - sock = socket.socket() - sock.connect(('localhost', port)) - - data = CREDENTIAL_INFO_REQUEST_JSON - msg = '{0}\n{1}'.format(len(data), data) - sock.sendall(_helpers._to_bytes(msg, encoding='utf-8')) - - header = sock.recv(6).decode() - if '\n' not in header: - raise CommunicationError('saw no newline in the first 6 bytes') - len_str, json_str = header.split('\n', 1) - to_read = int(len_str) - len(json_str) - if to_read > 0: - json_str += sock.recv(to_read, socket.MSG_WAITALL).decode() - - return CredentialInfoResponse(json_str) - - -class DevshellCredentials(client.GoogleCredentials): - """Credentials object for Google Developer Shell environment. - - This object will allow a Google Developer Shell session to identify its - user to Google and other OAuth 2.0 servers that can verify assertions. It - can be used for the purpose of accessing data stored under the user - account. - - 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. - """ - - def __init__(self, user_agent=None): - super(DevshellCredentials, self).__init__( - None, # access_token, initialized below - None, # client_id - None, # client_secret - None, # refresh_token - None, # token_expiry - None, # token_uri - user_agent) - self._refresh(None) - - def _refresh(self, http): - """Refreshes the access token. - - Args: - http: unused HTTP object - """ - self.devshell_response = _SendRecv() - self.access_token = self.devshell_response.access_token - expires_in = self.devshell_response.expires_in - if expires_in is not None: - delta = datetime.timedelta(seconds=expires_in) - self.token_expiry = client._UTCNOW() + delta - else: - self.token_expiry = None - - @property - def user_email(self): - return self.devshell_response.user_email - - @property - def project_id(self): - return self.devshell_response.project_id - - @classmethod - def from_json(cls, json_data): - raise NotImplementedError( - 'Cannot load Developer Shell credentials from JSON.') - - @property - def serialization_data(self): - raise NotImplementedError( - 'Cannot serialize Developer Shell credentials.') diff --git a/src/oauth2client/oauth2client/contrib/dictionary_storage.py b/src/oauth2client/oauth2client/contrib/dictionary_storage.py deleted file mode 100644 index 6ee333fa..00000000 --- a/src/oauth2client/oauth2client/contrib/dictionary_storage.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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. - -"""Dictionary storage for OAuth2 Credentials.""" - -from oauth2client import client - - -class DictionaryStorage(client.Storage): - """Store and retrieve credentials to and from a dictionary-like object. - - Args: - dictionary: A dictionary or dictionary-like object. - key: A string or other hashable. The credentials will be stored in - ``dictionary[key]``. - lock: An optional threading.Lock-like object. The lock will be - acquired before anything is written or read from the - dictionary. - """ - - def __init__(self, dictionary, key, lock=None): - """Construct a DictionaryStorage instance.""" - super(DictionaryStorage, self).__init__(lock=lock) - self._dictionary = dictionary - self._key = key - - def locked_get(self): - """Retrieve the credentials from the dictionary, if they exist. - - Returns: A :class:`oauth2client.client.OAuth2Credentials` instance. - """ - serialized = self._dictionary.get(self._key) - - if serialized is None: - return None - - credentials = client.OAuth2Credentials.from_json(serialized) - credentials.set_store(self) - - return credentials - - def locked_put(self, credentials): - """Save the credentials to the dictionary. - - Args: - credentials: A :class:`oauth2client.client.OAuth2Credentials` - instance. - """ - serialized = credentials.to_json() - self._dictionary[self._key] = serialized - - def locked_delete(self): - """Remove the credentials from the dictionary, if they exist.""" - self._dictionary.pop(self._key, None) diff --git a/src/oauth2client/oauth2client/contrib/django_util/__init__.py b/src/oauth2client/oauth2client/contrib/django_util/__init__.py deleted file mode 100644 index 644a8f9f..00000000 --- a/src/oauth2client/oauth2client/contrib/django_util/__init__.py +++ /dev/null @@ -1,489 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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 the Django web framework. - -Provides Django views and helpers the make using the OAuth2 web server -flow easier. It includes an ``oauth_required`` decorator to automatically -ensure that user credentials are available, and an ``oauth_enabled`` decorator -to check if the user has authorized, and helper shortcuts to create the -authorization URL otherwise. - -There are two basic use cases supported. The first is using Google OAuth as the -primary form of authentication, which is the simpler approach recommended -for applications without their own user system. - -The second use case is adding Google OAuth credentials to an -existing Django model containing a Django user field. Most of the -configuration is the same, except for `GOOGLE_OAUTH_MODEL_STORAGE` in -settings.py. See "Adding Credentials To An Existing Django User System" for -usage differences. - -Only Django versions 1.8+ are supported. - -Configuration -=============== - -To configure, you'll need a set of OAuth2 web application credentials from -`Google Developer's Console `. - -Add the helper to your INSTALLED_APPS: - -.. code-block:: python - :caption: settings.py - :name: installed_apps - - INSTALLED_APPS = ( - # other apps - "django.contrib.sessions.middleware" - "oauth2client.contrib.django_util" - ) - -This helper also requires the Django Session Middleware, so -``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well. -MIDDLEWARE or MIDDLEWARE_CLASSES (in Django versions <1.10) should also -contain the string 'django.contrib.sessions.middleware.SessionMiddleware'. - - -Add the client secrets created earlier to the settings. You can either -specify the path to the credentials file in JSON format - -.. code-block:: python - :caption: settings.py - :name: secrets_file - - GOOGLE_OAUTH2_CLIENT_SECRETS_JSON=/path/to/client-secret.json - -Or, directly configure the client Id and client secret. - - -.. code-block:: python - :caption: settings.py - :name: secrets_config - - GOOGLE_OAUTH2_CLIENT_ID=client-id-field - GOOGLE_OAUTH2_CLIENT_SECRET=client-secret-field - -By default, the default scopes for the required decorator only contains the -``email`` scopes. You can change that default in the settings. - -.. code-block:: python - :caption: settings.py - :name: scopes - - GOOGLE_OAUTH2_SCOPES = ('email', 'https://www.googleapis.com/auth/calendar',) - -By default, the decorators will add an `oauth` object to the Django request -object, and include all of its state and helpers inside that object. If the -`oauth` name conflicts with another usage, it can be changed - -.. code-block:: python - :caption: settings.py - :name: request_prefix - - # changes request.oauth to request.google_oauth - GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'google_oauth' - -Add the oauth2 routes to your application's urls.py urlpatterns. - -.. code-block:: python - :caption: urls.py - :name: urls - - from oauth2client.contrib.django_util.site import urls as oauth2_urls - - urlpatterns += [url(r'^oauth2/', include(oauth2_urls))] - -To require OAuth2 credentials for a view, use the `oauth2_required` decorator. -This creates a credentials object with an id_token, and allows you to create -an `http` object to build service clients with. These are all attached to the -request.oauth - -.. code-block:: python - :caption: views.py - :name: views_required - - from oauth2client.contrib.django_util.decorators import oauth_required - - @oauth_required - def requires_default_scopes(request): - email = request.oauth.credentials.id_token['email'] - service = build(serviceName='calendar', version='v3', - http=request.oauth.http, - developerKey=API_KEY) - events = service.events().list(calendarId='primary').execute()['items'] - return HttpResponse("email: {0} , calendar: {1}".format( - email,str(events))) - return HttpResponse( - "email: {0} , calendar: {1}".format(email, str(events))) - -To make OAuth2 optional and provide an authorization link in your own views. - -.. code-block:: python - :caption: views.py - :name: views_enabled2 - - from oauth2client.contrib.django_util.decorators import oauth_enabled - - @oauth_enabled - def optional_oauth2(request): - if request.oauth.has_credentials(): - # this could be passed into a view - # request.oauth.http is also initialized - return HttpResponse("User email: {0}".format( - request.oauth.credentials.id_token['email'])) - else: - return HttpResponse( - 'Here is an OAuth Authorize link: Authorize' - ''.format(request.oauth.get_authorize_redirect())) - -If a view needs a scope not included in the default scopes specified in -the settings, you can use [incremental auth](https://developers.google.com/identity/sign-in/web/incremental-auth) -and specify additional scopes in the decorator arguments. - -.. code-block:: python - :caption: views.py - :name: views_required_additional_scopes - - @oauth_enabled(scopes=['https://www.googleapis.com/auth/drive']) - def drive_required(request): - if request.oauth.has_credentials(): - service = build(serviceName='drive', version='v2', - http=request.oauth.http, - developerKey=API_KEY) - events = service.files().list().execute()['items'] - return HttpResponse(str(events)) - else: - return HttpResponse( - 'Here is an OAuth Authorize link: Authorize' - ''.format(request.oauth.get_authorize_redirect())) - - -To provide a callback on authorization being completed, use the -oauth2_authorized signal: - -.. code-block:: python - :caption: views.py - :name: signals - - from oauth2client.contrib.django_util.signals import oauth2_authorized - - def test_callback(sender, request, credentials, **kwargs): - print("Authorization Signal Received {0}".format( - credentials.id_token['email'])) - - oauth2_authorized.connect(test_callback) - -Adding Credentials To An Existing Django User System -===================================================== - -As an alternative to storing the credentials in the session, the helper -can be configured to store the fields on a Django model. This might be useful -if you need to use the credentials outside the context of a user request. It -also prevents the need for a logged in user to repeat the OAuth flow when -starting a new session. - -To use, change ``settings.py`` - -.. code-block:: python - :caption: settings.py - :name: storage_model_config - - GOOGLE_OAUTH2_STORAGE_MODEL = { - 'model': 'path.to.model.MyModel', - 'user_property': 'user_id', - 'credentials_property': 'credential' - } - -Where ``path.to.model`` class is the fully qualified name of a -``django.db.model`` class containing a ``django.contrib.auth.models.User`` -field with the name specified by `user_property` and a -:class:`oauth2client.contrib.django_util.models.CredentialsField` with the name -specified by `credentials_property`. For the sample configuration given, -our model would look like - -.. code-block:: python - :caption: models.py - :name: storage_model_model - - from django.contrib.auth.models import User - from oauth2client.contrib.django_util.models import CredentialsField - - class MyModel(models.Model): - # ... other fields here ... - user = models.OneToOneField(User) - credential = CredentialsField() -""" - -import importlib - -import django.conf -from django.core import exceptions -from django.core import urlresolvers -from six.moves.urllib import parse - -from oauth2client import clientsecrets -from oauth2client import transport -from oauth2client.contrib import dictionary_storage -from oauth2client.contrib.django_util import storage - -GOOGLE_OAUTH2_DEFAULT_SCOPES = ('email',) -GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth' - - -def _load_client_secrets(filename): - """Loads client secrets from the given filename. - - Args: - filename: The name of the file containing the JSON secret key. - - Returns: - A 2-tuple, the first item containing the client id, and the second - item containing a client secret. - """ - client_type, client_info = clientsecrets.loadfile(filename) - - if client_type != clientsecrets.TYPE_WEB: - raise ValueError( - 'The flow specified in {} is not supported, only the WEB flow ' - 'type is supported.'.format(client_type)) - return client_info['client_id'], client_info['client_secret'] - - -def _get_oauth2_client_id_and_secret(settings_instance): - """Initializes client id and client secret based on the settings. - - Args: - settings_instance: An instance of ``django.conf.settings``. - - Returns: - A 2-tuple, the first item is the client id and the second - item is the client secret. - """ - secret_json = getattr(settings_instance, - 'GOOGLE_OAUTH2_CLIENT_SECRETS_JSON', None) - if secret_json is not None: - return _load_client_secrets(secret_json) - else: - client_id = getattr(settings_instance, "GOOGLE_OAUTH2_CLIENT_ID", - None) - client_secret = getattr(settings_instance, - "GOOGLE_OAUTH2_CLIENT_SECRET", None) - if client_id is not None and client_secret is not None: - return client_id, client_secret - else: - raise exceptions.ImproperlyConfigured( - "Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or " - "both GOOGLE_OAUTH2_CLIENT_ID and " - "GOOGLE_OAUTH2_CLIENT_SECRET in settings.py") - - -def _get_storage_model(): - """This configures whether the credentials will be stored in the session - or the Django ORM based on the settings. By default, the credentials - will be stored in the session, unless `GOOGLE_OAUTH2_STORAGE_MODEL` - is found in the settings. Usually, the ORM storage is used to integrate - credentials into an existing Django user system. - - Returns: - A tuple containing three strings, or None. If - ``GOOGLE_OAUTH2_STORAGE_MODEL`` is configured, the tuple - will contain the fully qualifed path of the `django.db.model`, - the name of the ``django.contrib.auth.models.User`` field on the - model, and the name of the - :class:`oauth2client.contrib.django_util.models.CredentialsField` - field on the model. If Django ORM storage is not configured, - this function returns None. - """ - storage_model_settings = getattr(django.conf.settings, - 'GOOGLE_OAUTH2_STORAGE_MODEL', None) - if storage_model_settings is not None: - return (storage_model_settings['model'], - storage_model_settings['user_property'], - storage_model_settings['credentials_property']) - else: - return None, None, None - - -class OAuth2Settings(object): - """Initializes Django OAuth2 Helper Settings - - This class loads the OAuth2 Settings from the Django settings, and then - provides those settings as attributes to the rest of the views and - decorators in the module. - - Attributes: - scopes: A list of OAuth2 scopes that the decorators and views will use - as defaults. - request_prefix: The name of the attribute that the decorators use to - attach the UserOAuth2 object to the Django request object. - client_id: The OAuth2 Client ID. - client_secret: The OAuth2 Client Secret. - """ - - def __init__(self, settings_instance): - self.scopes = getattr(settings_instance, 'GOOGLE_OAUTH2_SCOPES', - GOOGLE_OAUTH2_DEFAULT_SCOPES) - self.request_prefix = getattr(settings_instance, - 'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE', - GOOGLE_OAUTH2_REQUEST_ATTRIBUTE) - info = _get_oauth2_client_id_and_secret(settings_instance) - self.client_id, self.client_secret = info - - # Django 1.10 deprecated MIDDLEWARE_CLASSES in favor of MIDDLEWARE - middleware_settings = getattr(settings_instance, 'MIDDLEWARE', None) - if middleware_settings is None: - middleware_settings = getattr( - settings_instance, 'MIDDLEWARE_CLASSES', None) - if middleware_settings is None: - raise exceptions.ImproperlyConfigured( - 'Django settings has neither MIDDLEWARE nor MIDDLEWARE_CLASSES' - 'configured') - - if ('django.contrib.sessions.middleware.SessionMiddleware' not in - middleware_settings): - raise exceptions.ImproperlyConfigured( - 'The Google OAuth2 Helper requires session middleware to ' - 'be installed. Edit your MIDDLEWARE_CLASSES or MIDDLEWARE ' - 'setting to include \'django.contrib.sessions.middleware.' - 'SessionMiddleware\'.') - (self.storage_model, self.storage_model_user_property, - self.storage_model_credentials_property) = _get_storage_model() - - -oauth2_settings = OAuth2Settings(django.conf.settings) - -_CREDENTIALS_KEY = 'google_oauth2_credentials' - - -def get_storage(request): - """ Gets a Credentials storage object provided by the Django OAuth2 Helper - object. - - Args: - request: Reference to the current request object. - - Returns: - An :class:`oauth2.client.Storage` object. - """ - storage_model = oauth2_settings.storage_model - user_property = oauth2_settings.storage_model_user_property - credentials_property = oauth2_settings.storage_model_credentials_property - - if storage_model: - module_name, class_name = storage_model.rsplit('.', 1) - module = importlib.import_module(module_name) - storage_model_class = getattr(module, class_name) - return storage.DjangoORMStorage(storage_model_class, - user_property, - request.user, - credentials_property) - else: - # use session - return dictionary_storage.DictionaryStorage( - request.session, key=_CREDENTIALS_KEY) - - -def _redirect_with_params(url_name, *args, **kwargs): - """Helper method to create a redirect response with URL params. - - This builds a redirect string that converts kwargs into a - query string. - - Args: - url_name: The name of the url to redirect to. - kwargs: the query string param and their values to build. - - Returns: - A properly formatted redirect string. - """ - url = urlresolvers.reverse(url_name, args=args) - params = parse.urlencode(kwargs, True) - return "{0}?{1}".format(url, params) - - -def _credentials_from_request(request): - """Gets the authorized credentials for this flow, if they exist.""" - # ORM storage requires a logged in user - if (oauth2_settings.storage_model is None or - request.user.is_authenticated()): - return get_storage(request).get() - else: - return None - - -class UserOAuth2(object): - """Class to create oauth2 objects on Django request objects containing - credentials and helper methods. - """ - - def __init__(self, request, scopes=None, return_url=None): - """Initialize the Oauth2 Object. - - Args: - request: Django request object. - scopes: Scopes desired for this OAuth2 flow. - return_url: The url to return to after the OAuth flow is complete, - defaults to the request's current URL path. - """ - self.request = request - self.return_url = return_url or request.get_full_path() - if scopes: - self._scopes = set(oauth2_settings.scopes) | set(scopes) - else: - self._scopes = set(oauth2_settings.scopes) - - def get_authorize_redirect(self): - """Creates a URl to start the OAuth2 authorization flow.""" - get_params = { - 'return_url': self.return_url, - 'scopes': self._get_scopes() - } - - return _redirect_with_params('google_oauth:authorize', **get_params) - - def has_credentials(self): - """Returns True if there are valid credentials for the current user - and required scopes.""" - credentials = _credentials_from_request(self.request) - return (credentials and not credentials.invalid and - credentials.has_scopes(self._get_scopes())) - - def _get_scopes(self): - """Returns the scopes associated with this object, kept up to - date for incremental auth.""" - if _credentials_from_request(self.request): - return (self._scopes | - _credentials_from_request(self.request).scopes) - else: - return self._scopes - - @property - def scopes(self): - """Returns the scopes associated with this OAuth2 object.""" - # make sure previously requested custom scopes are maintained - # in future authorizations - return self._get_scopes() - - @property - def credentials(self): - """Gets the authorized credentials for this flow, if they exist.""" - return _credentials_from_request(self.request) - - @property - def http(self): - """Helper: create HTTP client authorized with OAuth2 credentials.""" - if self.has_credentials(): - return self.credentials.authorize(transport.get_http_object()) - return None diff --git a/src/oauth2client/oauth2client/contrib/django_util/apps.py b/src/oauth2client/oauth2client/contrib/django_util/apps.py deleted file mode 100644 index 86676b91..00000000 --- a/src/oauth2client/oauth2client/contrib/django_util/apps.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -"""Application Config For Django OAuth2 Helper. - -Django 1.7+ provides an -[applications](https://docs.djangoproject.com/en/1.8/ref/applications/) -API so that Django projects can introspect on installed applications using a -stable API. This module exists to follow that convention. -""" - -import sys - -# Django 1.7+ only supports Python 2.7+ -if sys.hexversion >= 0x02070000: # pragma: NO COVER - from django.apps import AppConfig - - class GoogleOAuth2HelperConfig(AppConfig): - """ App Config for Django Helper""" - name = 'oauth2client.django_util' - verbose_name = "Google OAuth2 Django Helper" diff --git a/src/oauth2client/oauth2client/contrib/django_util/decorators.py b/src/oauth2client/oauth2client/contrib/django_util/decorators.py deleted file mode 100644 index e62e1710..00000000 --- a/src/oauth2client/oauth2client/contrib/django_util/decorators.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -"""Decorators for Django OAuth2 Flow. - -Contains two decorators, ``oauth_required`` and ``oauth_enabled``. - -``oauth_required`` will ensure that a user has an oauth object containing -credentials associated with the request, and if not, redirect to the -authorization flow. - -``oauth_enabled`` will attach the oauth2 object containing credentials if it -exists. If it doesn't, the view will still render, but helper methods will be -attached to start the oauth2 flow. -""" - -from django import shortcuts -import django.conf -from six import wraps -from six.moves.urllib import parse - -from oauth2client.contrib import django_util - - -def oauth_required(decorated_function=None, scopes=None, **decorator_kwargs): - """ Decorator to require OAuth2 credentials for a view. - - - .. code-block:: python - :caption: views.py - :name: views_required_2 - - - from oauth2client.django_util.decorators import oauth_required - - @oauth_required - def requires_default_scopes(request): - email = request.credentials.id_token['email'] - service = build(serviceName='calendar', version='v3', - http=request.oauth.http, - developerKey=API_KEY) - events = service.events().list( - calendarId='primary').execute()['items'] - return HttpResponse( - "email: {0}, calendar: {1}".format(email, str(events))) - - Args: - decorated_function: View function to decorate, must have the Django - request object as the first argument. - scopes: Scopes to require, will default. - decorator_kwargs: Can include ``return_url`` to specify the URL to - return to after OAuth2 authorization is complete. - - Returns: - An OAuth2 Authorize view if credentials are not found or if the - credentials are missing the required scopes. Otherwise, - the decorated view. - """ - def curry_wrapper(wrapped_function): - @wraps(wrapped_function) - def required_wrapper(request, *args, **kwargs): - if not (django_util.oauth2_settings.storage_model is None or - request.user.is_authenticated()): - redirect_str = '{0}?next={1}'.format( - django.conf.settings.LOGIN_URL, - parse.quote(request.path)) - return shortcuts.redirect(redirect_str) - - return_url = decorator_kwargs.pop('return_url', - request.get_full_path()) - user_oauth = django_util.UserOAuth2(request, scopes, return_url) - if not user_oauth.has_credentials(): - return shortcuts.redirect(user_oauth.get_authorize_redirect()) - setattr(request, django_util.oauth2_settings.request_prefix, - user_oauth) - return wrapped_function(request, *args, **kwargs) - - return required_wrapper - - if decorated_function: - return curry_wrapper(decorated_function) - else: - return curry_wrapper - - -def oauth_enabled(decorated_function=None, scopes=None, **decorator_kwargs): - """ Decorator to enable OAuth Credentials if authorized, and setup - the oauth object on the request object to provide helper functions - to start the flow otherwise. - - .. code-block:: python - :caption: views.py - :name: views_enabled3 - - from oauth2client.django_util.decorators import oauth_enabled - - @oauth_enabled - def optional_oauth2(request): - if request.oauth.has_credentials(): - # this could be passed into a view - # request.oauth.http is also initialized - return HttpResponse("User email: {0}".format( - request.oauth.credentials.id_token['email']) - else: - return HttpResponse('Here is an OAuth Authorize link: - Authorize'.format( - request.oauth.get_authorize_redirect())) - - - Args: - decorated_function: View function to decorate. - scopes: Scopes to require, will default. - decorator_kwargs: Can include ``return_url`` to specify the URL to - return to after OAuth2 authorization is complete. - - Returns: - The decorated view function. - """ - def curry_wrapper(wrapped_function): - @wraps(wrapped_function) - def enabled_wrapper(request, *args, **kwargs): - return_url = decorator_kwargs.pop('return_url', - request.get_full_path()) - user_oauth = django_util.UserOAuth2(request, scopes, return_url) - setattr(request, django_util.oauth2_settings.request_prefix, - user_oauth) - return wrapped_function(request, *args, **kwargs) - - return enabled_wrapper - - if decorated_function: - return curry_wrapper(decorated_function) - else: - return curry_wrapper diff --git a/src/oauth2client/oauth2client/contrib/django_util/models.py b/src/oauth2client/oauth2client/contrib/django_util/models.py deleted file mode 100644 index 37cc6970..00000000 --- a/src/oauth2client/oauth2client/contrib/django_util/models.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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. - -"""Contains classes used for the Django ORM storage.""" - -import base64 -import pickle - -from django.db import models -from django.utils import encoding -import jsonpickle - -import oauth2client - - -class CredentialsField(models.Field): - """Django ORM field for storing OAuth2 Credentials.""" - - 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 'BinaryField' - - def from_db_value(self, value, expression, connection, context): - """Overrides ``models.Field`` method. This converts the value - returned from the database to an instance of this class. - """ - return self.to_python(value) - - def to_python(self, value): - """Overrides ``models.Field`` method. This is used to convert - bytes (from serialization etc) to an instance of this class""" - if value is None: - return None - elif isinstance(value, oauth2client.client.Credentials): - return value - else: - try: - return jsonpickle.decode( - base64.b64decode(encoding.smart_bytes(value)).decode()) - except ValueError: - return pickle.loads( - base64.b64decode(encoding.smart_bytes(value))) - - def get_prep_value(self, value): - """Overrides ``models.Field`` method. This is used to convert - the value from an instances of this class to bytes that can be - inserted into the database. - """ - if value is None: - return None - else: - return encoding.smart_text( - base64.b64encode(jsonpickle.encode(value).encode())) - - def value_to_string(self, obj): - """Convert the field value from the provided model to a string. - - Used during model serialization. - - Args: - obj: db.Model, model object - - Returns: - string, the serialized field value - """ - value = self._get_val_from_obj(obj) - return self.get_prep_value(value) diff --git a/src/oauth2client/oauth2client/contrib/django_util/signals.py b/src/oauth2client/oauth2client/contrib/django_util/signals.py deleted file mode 100644 index e9356b4d..00000000 --- a/src/oauth2client/oauth2client/contrib/django_util/signals.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -"""Signals for Google OAuth2 Helper. - -This module contains signals for Google OAuth2 Helper. Currently it only -contains one, which fires when an OAuth2 authorization flow has completed. -""" - -import django.dispatch - -"""Signal that fires when OAuth2 Flow has completed. -It passes the Django request object and the OAuth2 credentials object to the - receiver. -""" -oauth2_authorized = django.dispatch.Signal( - providing_args=["request", "credentials"]) diff --git a/src/oauth2client/oauth2client/contrib/django_util/site.py b/src/oauth2client/oauth2client/contrib/django_util/site.py deleted file mode 100644 index 631f79be..00000000 --- a/src/oauth2client/oauth2client/contrib/django_util/site.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -"""Contains Django URL patterns used for OAuth2 flow.""" - -from django.conf import urls - -from oauth2client.contrib.django_util import views - -urlpatterns = [ - urls.url(r'oauth2callback/', views.oauth2_callback, name="callback"), - urls.url(r'oauth2authorize/', views.oauth2_authorize, name="authorize") -] - -urls = (urlpatterns, "google_oauth", "google_oauth") diff --git a/src/oauth2client/oauth2client/contrib/django_util/storage.py b/src/oauth2client/oauth2client/contrib/django_util/storage.py deleted file mode 100644 index 5682919b..00000000 --- a/src/oauth2client/oauth2client/contrib/django_util/storage.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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. - -"""Contains a storage module that stores credentials using the Django ORM.""" - -from oauth2client import client - - -class DjangoORMStorage(client.Storage): - """Store and retrieve a single credential to and from the Django datastore. - - This Storage helper presumes the Credentials - have been stored as a CredentialsField - on a db model class. - """ - - def __init__(self, model_class, key_name, key_value, property_name): - """Constructor for Storage. - - Args: - model: string, fully qualified name of 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. - """ - super(DjangoORMStorage, self).__init__() - 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 stored credential from the Django ORM. - - Returns: - oauth2client.Credentials retrieved from the Django ORM, associated - with the ``model``, ``key_value``->``key_name`` pair used to query - for the model, and ``property_name`` identifying the - ``CredentialsProperty`` field, all of which are defined in the - constructor for this Storage object. - - """ - 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 getattr(credential, 'set_store', None) is not None: - credential.set_store(self) - return credential - else: - return None - - def locked_put(self, credentials): - """Write a Credentials to the Django datastore. - - Args: - credentials: Credentials, the credentials to store. - """ - entity, _ = self.model_class.objects.get_or_create( - **{self.key_name: self.key_value}) - - setattr(entity, self.property_name, credentials) - entity.save() - - def locked_delete(self): - """Delete Credentials from the datastore.""" - query = {self.key_name: self.key_value} - self.model_class.objects.filter(**query).delete() diff --git a/src/oauth2client/oauth2client/contrib/django_util/views.py b/src/oauth2client/oauth2client/contrib/django_util/views.py deleted file mode 100644 index 1835208a..00000000 --- a/src/oauth2client/oauth2client/contrib/django_util/views.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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 contains the views used by the OAuth2 flows. - -Their are two views used by the OAuth2 flow, the authorize and the callback -view. The authorize view kicks off the three-legged OAuth flow, and the -callback view validates the flow and if successful stores the credentials -in the configured storage.""" - -import hashlib -import json -import os - -from django import http -from django import shortcuts -from django.conf import settings -from django.core import urlresolvers -from django.shortcuts import redirect -from django.utils import html -import jsonpickle -from six.moves.urllib import parse - -from oauth2client import client -from oauth2client.contrib import django_util -from oauth2client.contrib.django_util import get_storage -from oauth2client.contrib.django_util import signals - -_CSRF_KEY = 'google_oauth2_csrf_token' -_FLOW_KEY = 'google_oauth2_flow_{0}' - - -def _make_flow(request, scopes, return_url=None): - """Creates a Web Server Flow - - Args: - request: A Django request object. - scopes: the request oauth2 scopes. - return_url: The URL to return to after the flow is complete. Defaults - to the path of the current request. - - Returns: - An OAuth2 flow object that has been stored in the session. - """ - # Generate a CSRF token to prevent malicious requests. - csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest() - - request.session[_CSRF_KEY] = csrf_token - - state = json.dumps({ - 'csrf_token': csrf_token, - 'return_url': return_url, - }) - - flow = client.OAuth2WebServerFlow( - client_id=django_util.oauth2_settings.client_id, - client_secret=django_util.oauth2_settings.client_secret, - scope=scopes, - state=state, - redirect_uri=request.build_absolute_uri( - urlresolvers.reverse("google_oauth:callback"))) - - flow_key = _FLOW_KEY.format(csrf_token) - request.session[flow_key] = jsonpickle.encode(flow) - return flow - - -def _get_flow_for_token(csrf_token, request): - """ Looks up the flow in session to recover information about requested - scopes. - - Args: - csrf_token: The token passed in the callback request that should - match the one previously generated and stored in the request on the - initial authorization view. - - Returns: - The OAuth2 Flow object associated with this flow based on the - CSRF token. - """ - flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None) - return None if flow_pickle is None else jsonpickle.decode(flow_pickle) - - -def oauth2_callback(request): - """ View that handles the user's return from OAuth2 provider. - - This view verifies the CSRF state and OAuth authorization code, and on - success stores the credentials obtained in the storage provider, - and redirects to the return_url specified in the authorize view and - stored in the session. - - Args: - request: Django request. - - Returns: - A redirect response back to the return_url. - """ - if 'error' in request.GET: - reason = request.GET.get( - 'error_description', request.GET.get('error', '')) - reason = html.escape(reason) - return http.HttpResponseBadRequest( - 'Authorization failed {0}'.format(reason)) - - try: - encoded_state = request.GET['state'] - code = request.GET['code'] - except KeyError: - return http.HttpResponseBadRequest( - 'Request missing state or authorization code') - - try: - server_csrf = request.session[_CSRF_KEY] - except KeyError: - return http.HttpResponseBadRequest( - 'No existing session for this flow.') - - try: - state = json.loads(encoded_state) - client_csrf = state['csrf_token'] - return_url = state['return_url'] - except (ValueError, KeyError): - return http.HttpResponseBadRequest('Invalid state parameter.') - - if client_csrf != server_csrf: - return http.HttpResponseBadRequest('Invalid CSRF token.') - - flow = _get_flow_for_token(client_csrf, request) - - if not flow: - return http.HttpResponseBadRequest('Missing Oauth2 flow.') - - try: - credentials = flow.step2_exchange(code) - except client.FlowExchangeError as exchange_error: - return http.HttpResponseBadRequest( - 'An error has occurred: {0}'.format(exchange_error)) - - get_storage(request).put(credentials) - - signals.oauth2_authorized.send(sender=signals.oauth2_authorized, - request=request, credentials=credentials) - - return shortcuts.redirect(return_url) - - -def oauth2_authorize(request): - """ View to start the OAuth2 Authorization flow. - - This view starts the OAuth2 authorization flow. If scopes is passed in - as a GET URL parameter, it will authorize those scopes, otherwise the - default scopes specified in settings. The return_url can also be - specified as a GET parameter, otherwise the referer header will be - checked, and if that isn't found it will return to the root path. - - Args: - request: The Django request object. - - Returns: - A redirect to Google OAuth2 Authorization. - """ - return_url = request.GET.get('return_url', None) - if not return_url: - return_url = request.META.get('HTTP_REFERER', '/') - - scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes) - # Model storage (but not session storage) requires a logged in user - if django_util.oauth2_settings.storage_model: - if not request.user.is_authenticated(): - return redirect('{0}?next={1}'.format( - settings.LOGIN_URL, parse.quote(request.get_full_path()))) - # This checks for the case where we ended up here because of a logged - # out user but we had credentials for it in the first place - else: - user_oauth = django_util.UserOAuth2(request, scopes, return_url) - if user_oauth.has_credentials(): - return redirect(return_url) - - flow = _make_flow(request=request, scopes=scopes, return_url=return_url) - auth_url = flow.step1_get_authorize_url() - return shortcuts.redirect(auth_url) diff --git a/src/oauth2client/oauth2client/contrib/flask_util.py b/src/oauth2client/oauth2client/contrib/flask_util.py deleted file mode 100644 index fabd613b..00000000 --- a/src/oauth2client/oauth2client/contrib/flask_util.py +++ /dev/null @@ -1,557 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# 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 the Flask web framework - -Provides a Flask extension that makes using OAuth2 web server flow easier. -The extension includes views that handle the entire auth flow and a -``@required`` decorator to automatically ensure that user credentials are -available. - - -Configuration -============= - -To configure, you'll need a set of OAuth2 web application credentials from the -`Google Developer's Console `__. - -.. code-block:: python - - from oauth2client.contrib.flask_util import UserOAuth2 - - app = Flask(__name__) - - app.config['SECRET_KEY'] = 'your-secret-key' - - app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE'] = 'client_secrets.json' - - # or, specify the client id and secret separately - app.config['GOOGLE_OAUTH2_CLIENT_ID'] = 'your-client-id' - app.config['GOOGLE_OAUTH2_CLIENT_SECRET'] = 'your-client-secret' - - oauth2 = UserOAuth2(app) - - -Usage -===== - -Once configured, you can use the :meth:`UserOAuth2.required` decorator to -ensure that credentials are available within a view. - -.. code-block:: python - :emphasize-lines: 3,7,10 - - # Note that app.route should be the outermost decorator. - @app.route('/needs_credentials') - @oauth2.required - def example(): - # http is authorized with the user's credentials and can be used - # to make http calls. - http = oauth2.http() - - # Or, you can access the credentials directly - credentials = oauth2.credentials - -If you want credentials to be optional for a view, you can leave the decorator -off and use :meth:`UserOAuth2.has_credentials` to check. - -.. code-block:: python - :emphasize-lines: 3 - - @app.route('/optional') - def optional(): - if oauth2.has_credentials(): - return 'Credentials found!' - else: - return 'No credentials!' - - -When credentials are available, you can use :attr:`UserOAuth2.email` and -:attr:`UserOAuth2.user_id` to access information from the `ID Token -`__, if -available. - -.. code-block:: python - :emphasize-lines: 4 - - @app.route('/info') - @oauth2.required - def info(): - return "Hello, {} ({})".format(oauth2.email, oauth2.user_id) - - -URLs & Trigging Authorization -============================= - -The extension will add two new routes to your application: - - * ``"oauth2.authorize"`` -> ``/oauth2authorize`` - * ``"oauth2.callback"`` -> ``/oauth2callback`` - -When configuring your OAuth2 credentials on the Google Developer's Console, be -sure to add ``http[s]://[your-app-url]/oauth2callback`` as an authorized -callback url. - -Typically you don't not need to use these routes directly, just be sure to -decorate any views that require credentials with ``@oauth2.required``. If -needed, you can trigger authorization at any time by redirecting the user -to the URL returned by :meth:`UserOAuth2.authorize_url`. - -.. code-block:: python - :emphasize-lines: 3 - - @app.route('/login') - def login(): - return oauth2.authorize_url("/") - - -Incremental Auth -================ - -This extension also supports `Incremental Auth `__. To enable it, -configure the extension with ``include_granted_scopes``. - -.. code-block:: python - - oauth2 = UserOAuth2(app, include_granted_scopes=True) - -Then specify any additional scopes needed on the decorator, for example: - -.. code-block:: python - :emphasize-lines: 2,7 - - @app.route('/drive') - @oauth2.required(scopes=["https://www.googleapis.com/auth/drive"]) - def requires_drive(): - ... - - @app.route('/calendar') - @oauth2.required(scopes=["https://www.googleapis.com/auth/calendar"]) - def requires_calendar(): - ... - -The decorator will ensure that the the user has authorized all specified scopes -before allowing them to access the view, and will also ensure that credentials -do not lose any previously authorized scopes. - - -Storage -======= - -By default, the extension uses a Flask session-based storage solution. This -means that credentials are only available for the duration of a session. It -also means that with Flask's default configuration, the credentials will be -visible in the session cookie. It's highly recommended to use database-backed -session and to use https whenever handling user credentials. - -If you need the credentials to be available longer than a user session or -available outside of a request context, you will need to implement your own -:class:`oauth2client.Storage`. -""" - -from functools import wraps -import hashlib -import json -import os -import pickle - -try: - from flask import Blueprint - from flask import _app_ctx_stack - from flask import current_app - from flask import redirect - from flask import request - from flask import session - from flask import url_for - import markupsafe -except ImportError: # pragma: NO COVER - raise ImportError('The flask utilities require flask 0.9 or newer.') - -import six.moves.http_client as httplib - -from oauth2client import client -from oauth2client import clientsecrets -from oauth2client import transport -from oauth2client.contrib import dictionary_storage - - -_DEFAULT_SCOPES = ('email',) -_CREDENTIALS_KEY = 'google_oauth2_credentials' -_FLOW_KEY = 'google_oauth2_flow_{0}' -_CSRF_KEY = 'google_oauth2_csrf_token' - - -def _get_flow_for_token(csrf_token): - """Retrieves the flow instance associated with a given CSRF token from - the Flask session.""" - flow_pickle = session.pop( - _FLOW_KEY.format(csrf_token), None) - - if flow_pickle is None: - return None - else: - return pickle.loads(flow_pickle) - - -class UserOAuth2(object): - """Flask extension for making OAuth 2.0 easier. - - Configuration values: - - * ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` path to a client secrets json - file, obtained from the credentials screen in the Google Developers - console. - * ``GOOGLE_OAUTH2_CLIENT_ID`` the oauth2 credentials' client ID. This - is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` is not - specified. - * ``GOOGLE_OAUTH2_CLIENT_SECRET`` the oauth2 credentials' client - secret. This is only needed if ``GOOGLE_OAUTH2_CLIENT_SECRETS_FILE`` - is not specified. - - If app is specified, all arguments will be passed along to init_app. - - If no app is specified, then you should call init_app in your application - factory to finish initialization. - """ - - def __init__(self, app=None, *args, **kwargs): - self.app = app - if app is not None: - self.init_app(app, *args, **kwargs) - - def init_app(self, app, scopes=None, client_secrets_file=None, - client_id=None, client_secret=None, authorize_callback=None, - storage=None, **kwargs): - """Initialize this extension for the given app. - - Arguments: - app: A Flask application. - scopes: Optional list of scopes to authorize. - client_secrets_file: Path to a file containing client secrets. You - can also specify the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE config - value. - client_id: If not specifying a client secrets file, specify the - OAuth2 client id. You can also specify the - GOOGLE_OAUTH2_CLIENT_ID config value. You must also provide a - client secret. - client_secret: The OAuth2 client secret. You can also specify the - GOOGLE_OAUTH2_CLIENT_SECRET config value. - authorize_callback: A function that is executed after successful - user authorization. - storage: A oauth2client.client.Storage subclass for storing the - credentials. By default, this is a Flask session based storage. - kwargs: Any additional args are passed along to the Flow - constructor. - """ - self.app = app - self.authorize_callback = authorize_callback - self.flow_kwargs = kwargs - - if storage is None: - storage = dictionary_storage.DictionaryStorage( - session, key=_CREDENTIALS_KEY) - self.storage = storage - - if scopes is None: - scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES) - self.scopes = scopes - - self._load_config(client_secrets_file, client_id, client_secret) - - app.register_blueprint(self._create_blueprint()) - - def _load_config(self, client_secrets_file, client_id, client_secret): - """Loads oauth2 configuration in order of priority. - - Priority: - 1. Config passed to the constructor or init_app. - 2. Config passed via the GOOGLE_OAUTH2_CLIENT_SECRETS_FILE app - config. - 3. Config passed via the GOOGLE_OAUTH2_CLIENT_ID and - GOOGLE_OAUTH2_CLIENT_SECRET app config. - - Raises: - ValueError if no config could be found. - """ - if client_id and client_secret: - self.client_id, self.client_secret = client_id, client_secret - return - - if client_secrets_file: - self._load_client_secrets(client_secrets_file) - return - - if 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE' in self.app.config: - self._load_client_secrets( - self.app.config['GOOGLE_OAUTH2_CLIENT_SECRETS_FILE']) - return - - try: - self.client_id, self.client_secret = ( - self.app.config['GOOGLE_OAUTH2_CLIENT_ID'], - self.app.config['GOOGLE_OAUTH2_CLIENT_SECRET']) - except KeyError: - raise ValueError( - 'OAuth2 configuration could not be found. Either specify the ' - 'client_secrets_file or client_id and client_secret or set ' - 'the app configuration variables ' - 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or ' - 'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.') - - def _load_client_secrets(self, filename): - """Loads client secrets from the given filename.""" - client_type, client_info = clientsecrets.loadfile(filename) - if client_type != clientsecrets.TYPE_WEB: - raise ValueError( - 'The flow specified in {0} is not supported.'.format( - client_type)) - - self.client_id = client_info['client_id'] - self.client_secret = client_info['client_secret'] - - def _make_flow(self, return_url=None, **kwargs): - """Creates a Web Server Flow""" - # Generate a CSRF token to prevent malicious requests. - csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest() - - session[_CSRF_KEY] = csrf_token - - state = json.dumps({ - 'csrf_token': csrf_token, - 'return_url': return_url - }) - - kw = self.flow_kwargs.copy() - kw.update(kwargs) - - extra_scopes = kw.pop('scopes', []) - scopes = set(self.scopes).union(set(extra_scopes)) - - flow = client.OAuth2WebServerFlow( - client_id=self.client_id, - client_secret=self.client_secret, - scope=scopes, - state=state, - redirect_uri=url_for('oauth2.callback', _external=True), - **kw) - - flow_key = _FLOW_KEY.format(csrf_token) - session[flow_key] = pickle.dumps(flow) - - return flow - - def _create_blueprint(self): - bp = Blueprint('oauth2', __name__) - bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view) - bp.add_url_rule('/oauth2callback', 'callback', self.callback_view) - - return bp - - def authorize_view(self): - """Flask view that starts the authorization flow. - - Starts flow by redirecting the user to the OAuth2 provider. - """ - args = request.args.to_dict() - - # Scopes will be passed as mutliple args, and to_dict() will only - # return one. So, we use getlist() to get all of the scopes. - args['scopes'] = request.args.getlist('scopes') - - return_url = args.pop('return_url', None) - if return_url is None: - return_url = request.referrer or '/' - - flow = self._make_flow(return_url=return_url, **args) - auth_url = flow.step1_get_authorize_url() - - return redirect(auth_url) - - def callback_view(self): - """Flask view that handles the user's return from OAuth2 provider. - - On return, exchanges the authorization code for credentials and stores - the credentials. - """ - if 'error' in request.args: - reason = request.args.get( - 'error_description', request.args.get('error', '')) - reason = markupsafe.escape(reason) - return ('Authorization failed: {0}'.format(reason), - httplib.BAD_REQUEST) - - try: - encoded_state = request.args['state'] - server_csrf = session[_CSRF_KEY] - code = request.args['code'] - except KeyError: - return 'Invalid request', httplib.BAD_REQUEST - - try: - state = json.loads(encoded_state) - client_csrf = state['csrf_token'] - return_url = state['return_url'] - except (ValueError, KeyError): - return 'Invalid request state', httplib.BAD_REQUEST - - if client_csrf != server_csrf: - return 'Invalid request state', httplib.BAD_REQUEST - - flow = _get_flow_for_token(server_csrf) - - if flow is None: - return 'Invalid request state', httplib.BAD_REQUEST - - # Exchange the auth code for credentials. - try: - credentials = flow.step2_exchange(code) - except client.FlowExchangeError as exchange_error: - current_app.logger.exception(exchange_error) - content = 'An error occurred: {0}'.format(exchange_error) - return content, httplib.BAD_REQUEST - - # Save the credentials to the storage. - self.storage.put(credentials) - - if self.authorize_callback: - self.authorize_callback(credentials) - - return redirect(return_url) - - @property - def credentials(self): - """The credentials for the current user or None if unavailable.""" - ctx = _app_ctx_stack.top - - if not hasattr(ctx, _CREDENTIALS_KEY): - ctx.google_oauth2_credentials = self.storage.get() - - return ctx.google_oauth2_credentials - - def has_credentials(self): - """Returns True if there are valid credentials for the current user.""" - if not self.credentials: - return False - # Is the access token expired? If so, do we have an refresh token? - elif (self.credentials.access_token_expired and - not self.credentials.refresh_token): - return False - else: - return True - - @property - def email(self): - """Returns the user's email address or None if there are no credentials. - - The email address is provided by the current credentials' id_token. - This should not be used as unique identifier as the user can change - their email. If you need a unique identifier, use user_id. - """ - if not self.credentials: - return None - try: - return self.credentials.id_token['email'] - except KeyError: - current_app.logger.error( - 'Invalid id_token {0}'.format(self.credentials.id_token)) - - @property - def user_id(self): - """Returns the a unique identifier for the user - - Returns None if there are no credentials. - - The id is provided by the current credentials' id_token. - """ - if not self.credentials: - return None - try: - return self.credentials.id_token['sub'] - except KeyError: - current_app.logger.error( - 'Invalid id_token {0}'.format(self.credentials.id_token)) - - def authorize_url(self, return_url, **kwargs): - """Creates a URL that can be used to start the authorization flow. - - When the user is directed to the URL, the authorization flow will - begin. Once complete, the user will be redirected to the specified - return URL. - - Any kwargs are passed into the flow constructor. - """ - return url_for('oauth2.authorize', return_url=return_url, **kwargs) - - def required(self, decorated_function=None, scopes=None, - **decorator_kwargs): - """Decorator to require OAuth2 credentials for a view. - - If credentials are not available for the current user, then they will - be redirected to the authorization flow. Once complete, the user will - be redirected back to the original page. - """ - - def curry_wrapper(wrapped_function): - @wraps(wrapped_function) - def required_wrapper(*args, **kwargs): - return_url = decorator_kwargs.pop('return_url', request.url) - - requested_scopes = set(self.scopes) - if scopes is not None: - requested_scopes |= set(scopes) - if self.has_credentials(): - requested_scopes |= self.credentials.scopes - - requested_scopes = list(requested_scopes) - - # Does the user have credentials and does the credentials have - # all of the needed scopes? - if (self.has_credentials() and - self.credentials.has_scopes(requested_scopes)): - return wrapped_function(*args, **kwargs) - # Otherwise, redirect to authorization - else: - auth_url = self.authorize_url( - return_url, - scopes=requested_scopes, - **decorator_kwargs) - - return redirect(auth_url) - - return required_wrapper - - if decorated_function: - return curry_wrapper(decorated_function) - else: - return curry_wrapper - - def http(self, *args, **kwargs): - """Returns an authorized http instance. - - Can only be called if there are valid credentials for the user, such - as inside of a view that is decorated with @required. - - Args: - *args: Positional arguments passed to httplib2.Http constructor. - **kwargs: Positional arguments passed to httplib2.Http constructor. - - Raises: - ValueError if no credentials are available. - """ - if not self.credentials: - raise ValueError('No credentials available.') - return self.credentials.authorize( - transport.get_http_object(*args, **kwargs)) diff --git a/src/oauth2client/oauth2client/contrib/gce.py b/src/oauth2client/oauth2client/contrib/gce.py deleted file mode 100644 index aaab15ff..00000000 --- a/src/oauth2client/oauth2client/contrib/gce.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. -""" - -import logging -import warnings - -from six.moves import http_client - -from oauth2client import client -from oauth2client.contrib import _metadata - - -logger = logging.getLogger(__name__) - -_SCOPES_WARNING = """\ -You have requested explicit scopes to be used with a GCE service account. -Using this argument will have no effect on the actual scopes for tokens -requested. These scopes are set at VM instance creation time and -can't be overridden in the request. -""" - - -class AppAssertionCredentials(client.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. - - Note that :attr:`service_account_email` and :attr:`scopes` - will both return None until the credentials have been refreshed. - To check whether credentials have previously been refreshed use - :attr:`invalid`. - """ - - def __init__(self, email=None, *args, **kwargs): - """Constructor for AppAssertionCredentials - - Args: - email: an email that specifies the service account to use. - Only necessary if using custom service accounts - (see https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createdefaultserviceaccount). - """ - if 'scopes' in kwargs: - warnings.warn(_SCOPES_WARNING) - kwargs['scopes'] = None - - # Assertion type is no longer used, but still in the - # parent class signature. - super(AppAssertionCredentials, self).__init__(None, *args, **kwargs) - - self.service_account_email = email - self.scopes = None - self.invalid = True - - @classmethod - def from_json(cls, json_data): - raise NotImplementedError( - 'Cannot serialize credentials for GCE service accounts.') - - def to_json(self): - raise NotImplementedError( - 'Cannot serialize credentials for GCE service accounts.') - - def retrieve_scopes(self, http): - """Retrieves the canonical list of scopes for this access token. - - Overrides client.Credentials.retrieve_scopes. Fetches scopes info - from the metadata server. - - Args: - http: httplib2.Http, an http object to be used to make the refresh - request. - - Returns: - A set of strings containing the canonical list of scopes. - """ - self._retrieve_info(http) - return self.scopes - - def _retrieve_info(self, http): - """Retrieves service account info for invalid credentials. - - Args: - http: an object to be used to make HTTP requests. - """ - if self.invalid: - info = _metadata.get_service_account_info( - http, - service_account=self.service_account_email or 'default') - self.invalid = False - self.service_account_email = info['email'] - self.scopes = info['scopes'] - - def _refresh(self, http): - """Refreshes the access token. - - Skip all the storage hoops and just refresh using the API. - - Args: - http: an object to be used to make HTTP requests. - - Raises: - HttpAccessTokenRefreshError: When the refresh fails. - """ - try: - self._retrieve_info(http) - self.access_token, self.token_expiry = _metadata.get_token( - http, service_account=self.service_account_email) - except http_client.HTTPException as err: - raise client.HttpAccessTokenRefreshError(str(err)) - - @property - def serialization_data(self): - raise NotImplementedError( - 'Cannot serialize credentials for GCE service accounts.') - - def create_scoped_required(self): - return False - - def sign_blob(self, blob): - """Cryptographically sign a blob (of bytes). - - This method is provided to support a common interface, but - the actual key used for a Google Compute Engine service account - is not available, so it can't be used to sign content. - - Args: - blob: bytes, Message to be signed. - - Raises: - NotImplementedError, always. - """ - raise NotImplementedError( - 'Compute Engine service accounts cannot sign blobs') diff --git a/src/oauth2client/oauth2client/contrib/keyring_storage.py b/src/oauth2client/oauth2client/contrib/keyring_storage.py deleted file mode 100644 index 4af94488..00000000 --- a/src/oauth2client/oauth2client/contrib/keyring_storage.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. -""" - -import threading - -import keyring - -from oauth2client import client - - -class Storage(client.Storage): - """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 import keyring_storage - - s = keyring_storage.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. - """ - super(Storage, self).__init__(lock=threading.Lock()) - self._service_name = service_name - self._user_name = user_name - - 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 = client.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/src/oauth2client/oauth2client/contrib/multiprocess_file_storage.py b/src/oauth2client/oauth2client/contrib/multiprocess_file_storage.py deleted file mode 100644 index e9e8c8cd..00000000 --- a/src/oauth2client/oauth2client/contrib/multiprocess_file_storage.py +++ /dev/null @@ -1,355 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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. - -"""Multiprocess file credential storage. - -This module provides file-based storage that supports multiple credentials and -cross-thread and process access. - -This module supersedes the functionality previously found in `multistore_file`. - -This module provides :class:`MultiprocessFileStorage` which: - * Is tied to a single credential via a user-specified key. This key can be - used to distinguish between multiple users, client ids, and/or scopes. - * Can be safely accessed and refreshed across threads and processes. - -Process & thread safety guarantees the following behavior: - * If one thread or process refreshes a credential, subsequent refreshes - from other processes will re-fetch the credentials from the file instead - of performing an http request. - * If two processes or threads attempt to refresh concurrently, only one - will be able to acquire the lock and refresh, with the deadlock caveat - below. - * The interprocess lock will not deadlock, instead, the if a process can - not acquire the interprocess lock within ``INTERPROCESS_LOCK_DEADLINE`` - it will allow refreshing the credential but will not write the updated - credential to disk, This logic happens during every lock cycle - if the - credentials are refreshed again it will retry locking and writing as - normal. - -Usage -===== - -Before using the storage, you need to decide how you want to key the -credentials. A few common strategies include: - - * If you're storing credentials for multiple users in a single file, use - a unique identifier for each user as the key. - * If you're storing credentials for multiple client IDs in a single file, - use the client ID as the key. - * If you're storing multiple credentials for one user, use the scopes as - the key. - * If you have a complicated setup, use a compound key. For example, you - can use a combination of the client ID and scopes as the key. - -Create an instance of :class:`MultiprocessFileStorage` for each credential you -want to store, for example:: - - filename = 'credentials' - key = '{}-{}'.format(client_id, user_id) - storage = MultiprocessFileStorage(filename, key) - -To store the credentials:: - - storage.put(credentials) - -If you're going to continue to use the credentials after storing them, be sure -to call :func:`set_store`:: - - credentials.set_store(storage) - -To retrieve the credentials:: - - storage.get(credentials) - -""" - -import base64 -import json -import logging -import os -import threading - -import fasteners -from six import iteritems - -from oauth2client import _helpers -from oauth2client import client - - -#: The maximum amount of time, in seconds, to wait when acquire the -#: interprocess lock before falling back to read-only mode. -INTERPROCESS_LOCK_DEADLINE = 1 - -logger = logging.getLogger(__name__) -_backends = {} -_backends_lock = threading.Lock() - - -def _create_file_if_needed(filename): - """Creates the an empty file if it does not already exist. - - Returns: - True if the file was created, False otherwise. - """ - if os.path.exists(filename): - return False - else: - # Equivalent to "touch". - open(filename, 'a+b').close() - logger.info('Credential file {0} created'.format(filename)) - return True - - -def _load_credentials_file(credentials_file): - """Load credentials from the given file handle. - - The file is expected to be in this format: - - { - "file_version": 2, - "credentials": { - "key": "base64 encoded json representation of credentials." - } - } - - This function will warn and return empty credentials instead of raising - exceptions. - - Args: - credentials_file: An open file handle. - - Returns: - A dictionary mapping user-defined keys to an instance of - :class:`oauth2client.client.Credentials`. - """ - try: - credentials_file.seek(0) - data = json.load(credentials_file) - except Exception: - logger.warning( - 'Credentials file could not be loaded, will ignore and ' - 'overwrite.') - return {} - - if data.get('file_version') != 2: - logger.warning( - 'Credentials file is not version 2, will ignore and ' - 'overwrite.') - return {} - - credentials = {} - - for key, encoded_credential in iteritems(data.get('credentials', {})): - try: - credential_json = base64.b64decode(encoded_credential) - credential = client.Credentials.new_from_json(credential_json) - credentials[key] = credential - except: - logger.warning( - 'Invalid credential {0} in file, ignoring.'.format(key)) - - return credentials - - -def _write_credentials_file(credentials_file, credentials): - """Writes credentials to a file. - - Refer to :func:`_load_credentials_file` for the format. - - Args: - credentials_file: An open file handle, must be read/write. - credentials: A dictionary mapping user-defined keys to an instance of - :class:`oauth2client.client.Credentials`. - """ - data = {'file_version': 2, 'credentials': {}} - - for key, credential in iteritems(credentials): - credential_json = credential.to_json() - encoded_credential = _helpers._from_bytes(base64.b64encode( - _helpers._to_bytes(credential_json))) - data['credentials'][key] = encoded_credential - - credentials_file.seek(0) - json.dump(data, credentials_file) - credentials_file.truncate() - - -class _MultiprocessStorageBackend(object): - """Thread-local backend for multiprocess storage. - - Each process has only one instance of this backend per file. All threads - share a single instance of this backend. This ensures that all threads - use the same thread lock and process lock when accessing the file. - """ - - def __init__(self, filename): - self._file = None - self._filename = filename - self._process_lock = fasteners.InterProcessLock( - '{0}.lock'.format(filename)) - self._thread_lock = threading.Lock() - self._read_only = False - self._credentials = {} - - def _load_credentials(self): - """(Re-)loads the credentials from the file.""" - if not self._file: - return - - loaded_credentials = _load_credentials_file(self._file) - self._credentials.update(loaded_credentials) - - logger.debug('Read credential file') - - def _write_credentials(self): - if self._read_only: - logger.debug('In read-only mode, not writing credentials.') - return - - _write_credentials_file(self._file, self._credentials) - logger.debug('Wrote credential file {0}.'.format(self._filename)) - - def acquire_lock(self): - self._thread_lock.acquire() - locked = self._process_lock.acquire(timeout=INTERPROCESS_LOCK_DEADLINE) - - if locked: - _create_file_if_needed(self._filename) - self._file = open(self._filename, 'r+') - self._read_only = False - - else: - logger.warn( - 'Failed to obtain interprocess lock for credentials. ' - 'If a credential is being refreshed, other processes may ' - 'not see the updated access token and refresh as well.') - if os.path.exists(self._filename): - self._file = open(self._filename, 'r') - else: - self._file = None - self._read_only = True - - self._load_credentials() - - def release_lock(self): - if self._file is not None: - self._file.close() - self._file = None - - if not self._read_only: - self._process_lock.release() - - self._thread_lock.release() - - def _refresh_predicate(self, credentials): - if credentials is None: - return True - elif credentials.invalid: - return True - elif credentials.access_token_expired: - return True - else: - return False - - def locked_get(self, key): - # Check if the credential is already in memory. - credentials = self._credentials.get(key, None) - - # Use the refresh predicate to determine if the entire store should be - # reloaded. This basically checks if the credentials are invalid - # or expired. This covers the situation where another process has - # refreshed the credentials and this process doesn't know about it yet. - # In that case, this process won't needlessly refresh the credentials. - if self._refresh_predicate(credentials): - self._load_credentials() - credentials = self._credentials.get(key, None) - - return credentials - - def locked_put(self, key, credentials): - self._load_credentials() - self._credentials[key] = credentials - self._write_credentials() - - def locked_delete(self, key): - self._load_credentials() - self._credentials.pop(key, None) - self._write_credentials() - - -def _get_backend(filename): - """A helper method to get or create a backend with thread locking. - - This ensures that only one backend is used per-file per-process, so that - thread and process locks are appropriately shared. - - Args: - filename: The full path to the credential storage file. - - Returns: - An instance of :class:`_MultiprocessStorageBackend`. - """ - filename = os.path.abspath(filename) - - with _backends_lock: - if filename not in _backends: - _backends[filename] = _MultiprocessStorageBackend(filename) - return _backends[filename] - - -class MultiprocessFileStorage(client.Storage): - """Multiprocess file credential storage. - - Args: - filename: The path to the file where credentials will be stored. - key: An arbitrary string used to uniquely identify this set of - credentials. For example, you may use the user's ID as the key or - a combination of the client ID and user ID. - """ - def __init__(self, filename, key): - self._key = key - self._backend = _get_backend(filename) - - def acquire_lock(self): - self._backend.acquire_lock() - - def release_lock(self): - self._backend.release_lock() - - def locked_get(self): - """Retrieves the current credentials from the store. - - Returns: - An instance of :class:`oauth2client.client.Credentials` or `None`. - """ - credential = self._backend.locked_get(self._key) - - if credential is not None: - credential.set_store(self) - - return credential - - def locked_put(self, credentials): - """Writes the given credentials to the store. - - Args: - credentials: an instance of - :class:`oauth2client.client.Credentials`. - """ - return self._backend.locked_put(self._key, credentials) - - def locked_delete(self): - """Deletes the current credentials from the store.""" - return self._backend.locked_delete(self._key) diff --git a/src/oauth2client/oauth2client/contrib/sqlalchemy.py b/src/oauth2client/oauth2client/contrib/sqlalchemy.py deleted file mode 100644 index 7d9fd4b2..00000000 --- a/src/oauth2client/oauth2client/contrib/sqlalchemy.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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 SQLAlchemy. - -Utilities for using OAuth 2.0 in conjunction with a SQLAlchemy. - -Configuration -============= - -In order to use this storage, you'll need to create table -with :class:`oauth2client.contrib.sqlalchemy.CredentialsType` column. -It's recommended to either put this column on some sort of user info -table or put the column in a table with a belongs-to relationship to -a user info table. - -Here's an example of a simple table with a :class:`CredentialsType` -column that's related to a user table by the `user_id` key. - -.. code-block:: python - - from sqlalchemy import Column, ForeignKey, Integer - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.orm import relationship - - from oauth2client.contrib.sqlalchemy import CredentialsType - - - Base = declarative_base() - - - class Credentials(Base): - __tablename__ = 'credentials' - - user_id = Column(Integer, ForeignKey('user.id')) - credentials = Column(CredentialsType) - - - class User(Base): - id = Column(Integer, primary_key=True) - # bunch of other columns - credentials = relationship('Credentials') - - -Usage -===== - -With tables ready, you are now able to store credentials in database. -We will reuse tables defined above. - -.. code-block:: python - - from sqlalchemy.orm import Session - - from oauth2client.client import OAuth2Credentials - from oauth2client.contrib.sql_alchemy import Storage - - session = Session() - user = session.query(User).first() - storage = Storage( - session=session, - model_class=Credentials, - # This is the key column used to identify - # the row that stores the credentials. - key_name='user_id', - key_value=user.id, - property_name='credentials', - ) - - # Store - credentials = OAuth2Credentials(...) - storage.put(credentials) - - # Retrieve - credentials = storage.get() - - # Delete - storage.delete() - -""" - -from __future__ import absolute_import - -import sqlalchemy.types - -from oauth2client import client - - -class CredentialsType(sqlalchemy.types.PickleType): - """Type representing credentials. - - Alias for :class:`sqlalchemy.types.PickleType`. - """ - - -class Storage(client.Storage): - """Store and retrieve a single credential to and from SQLAlchemy. - This helper presumes the Credentials - have been stored as a Credentials column - on a db model class. - """ - - def __init__(self, session, model_class, key_name, - key_value, property_name): - """Constructor for Storage. - - Args: - session: An instance of :class:`sqlalchemy.orm.Session`. - model_class: SQLAlchemy declarative mapping. - key_name: string, key name for the entity that has the credentials - key_value: key value for the entity that has the credentials - property_name: A string indicating which property on the - ``model_class`` to store the credentials. - This property must be a - :class:`CredentialsType` column. - """ - super(Storage, self).__init__() - - self.session = session - 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 stored credential. - - Returns: - A :class:`oauth2client.Credentials` instance or `None`. - """ - filters = {self.key_name: self.key_value} - query = self.session.query(self.model_class).filter_by(**filters) - entity = query.first() - - if entity: - credential = getattr(entity, self.property_name) - if credential and hasattr(credential, 'set_store'): - credential.set_store(self) - return credential - else: - return None - - def locked_put(self, credentials): - """Write a credentials to the SQLAlchemy datastore. - - Args: - credentials: :class:`oauth2client.Credentials` - """ - filters = {self.key_name: self.key_value} - query = self.session.query(self.model_class).filter_by(**filters) - entity = query.first() - - if not entity: - entity = self.model_class(**filters) - - setattr(entity, self.property_name, credentials) - self.session.add(entity) - - def locked_delete(self): - """Delete credentials from the SQLAlchemy datastore.""" - filters = {self.key_name: self.key_value} - self.session.query(self.model_class).filter_by(**filters).delete() diff --git a/src/oauth2client/oauth2client/contrib/xsrfutil.py b/src/oauth2client/oauth2client/contrib/xsrfutil.py deleted file mode 100644 index 7c3ec035..00000000 --- a/src/oauth2client/oauth2client/contrib/xsrfutil.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2014 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.""" - -import base64 -import binascii -import hmac -import time - -from oauth2client import _helpers - - -# Delimiter character -DELIMITER = b':' - -# 1 hour in seconds -DEFAULT_TIMEOUT_SECS = 60 * 60 - - -@_helpers.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. - """ - digester = hmac.new(_helpers._to_bytes(key, encoding='utf-8')) - digester.update(_helpers._to_bytes(str(user_id), encoding='utf-8')) - digester.update(DELIMITER) - digester.update(_helpers._to_bytes(action_id, encoding='utf-8')) - digester.update(DELIMITER) - when = _helpers._to_bytes(str(when or int(time.time())), encoding='utf-8') - digester.update(when) - digest = digester.digest() - - token = base64.urlsafe_b64encode(digest + DELIMITER + when) - return token - - -@_helpers.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(token) - token_time = int(decoded.split(DELIMITER)[-1]) - except (TypeError, ValueError, binascii.Error): - 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(bytearray(token), bytearray(expected_token)): - different |= x ^ y - return not different diff --git a/src/oauth2client/oauth2client/crypt.py b/src/oauth2client/oauth2client/crypt.py deleted file mode 100644 index 13260982..00000000 --- a/src/oauth2client/oauth2client/crypt.py +++ /dev/null @@ -1,250 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. -"""Crypto-related routines for oauth2client.""" - -import json -import logging -import time - -from oauth2client import _helpers -from oauth2client import _pure_python_crypt - - -RsaSigner = _pure_python_crypt.RsaSigner -RsaVerifier = _pure_python_crypt.RsaVerifier - -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): - """Error to indicate crypto failure.""" - - -def _bad_pkcs12_key_as_pem(*args, **kwargs): - raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.') - - -try: - from oauth2client import _openssl_crypt - OpenSSLSigner = _openssl_crypt.OpenSSLSigner - OpenSSLVerifier = _openssl_crypt.OpenSSLVerifier - pkcs12_key_as_pem = _openssl_crypt.pkcs12_key_as_pem -except ImportError: # pragma: NO COVER - OpenSSLVerifier = None - OpenSSLSigner = None - pkcs12_key_as_pem = _bad_pkcs12_key_as_pem - -try: - from oauth2client import _pycrypto_crypt - PyCryptoSigner = _pycrypto_crypt.PyCryptoSigner - PyCryptoVerifier = _pycrypto_crypt.PyCryptoVerifier -except ImportError: # pragma: NO COVER - PyCryptoVerifier = None - PyCryptoSigner = None - - -if OpenSSLSigner: - Signer = OpenSSLSigner - Verifier = OpenSSLVerifier -elif PyCryptoSigner: # pragma: NO COVER - Signer = PyCryptoSigner - Verifier = PyCryptoVerifier -else: # pragma: NO COVER - Signer = RsaSigner - Verifier = RsaVerifier - - -def make_signed_jwt(signer, payload, key_id=None): - """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. - key_id: string, (Optional) Key ID header. - - Returns: - string, The JWT for the payload. - """ - header = {'typ': 'JWT', 'alg': 'RS256'} - if key_id is not None: - header['kid'] = key_id - - segments = [ - _helpers._urlsafe_b64encode(_helpers._json_encode(header)), - _helpers._urlsafe_b64encode(_helpers._json_encode(payload)), - ] - signing_input = b'.'.join(segments) - - signature = signer.sign(signing_input) - segments.append(_helpers._urlsafe_b64encode(signature)) - - logger.debug(str(segments)) - - return b'.'.join(segments) - - -def _verify_signature(message, signature, certs): - """Verifies signed content using a list of certificates. - - Args: - message: string or bytes, The message to verify. - signature: string or bytes, The signature on the message. - certs: iterable, certificates in PEM format. - - Raises: - AppIdentityError: If none of the certificates can verify the message - against the signature. - """ - for pem in certs: - verifier = Verifier.from_string(pem, is_x509_cert=True) - if verifier.verify(message, signature): - return - - # If we have not returned, no certificate confirms the signature. - raise AppIdentityError('Invalid token signature') - - -def _check_audience(payload_dict, audience): - """Checks audience field from a JWT payload. - - Does nothing if the passed in ``audience`` is null. - - Args: - payload_dict: dict, A dictionary containing a JWT payload. - audience: string or NoneType, an audience to check for in - the JWT payload. - - Raises: - AppIdentityError: If there is no ``'aud'`` field in the payload - dictionary but there is an ``audience`` to check. - AppIdentityError: If the ``'aud'`` field in the payload dictionary - does not match the ``audience``. - """ - if audience is None: - return - - audience_in_payload = payload_dict.get('aud') - if audience_in_payload is None: - raise AppIdentityError( - 'No aud field in token: {0}'.format(payload_dict)) - if audience_in_payload != audience: - raise AppIdentityError('Wrong recipient, {0} != {1}: {2}'.format( - audience_in_payload, audience, payload_dict)) - - -def _verify_time_range(payload_dict): - """Verifies the issued at and expiration from a JWT payload. - - Makes sure the current time (in UTC) falls between the issued at and - expiration for the JWT (with some skew allowed for via - ``CLOCK_SKEW_SECS``). - - Args: - payload_dict: dict, A dictionary containing a JWT payload. - - Raises: - AppIdentityError: If there is no ``'iat'`` field in the payload - dictionary. - AppIdentityError: If there is no ``'exp'`` field in the payload - dictionary. - AppIdentityError: If the JWT expiration is too far in the future (i.e. - if the expiration would imply a token lifetime - longer than what is allowed.) - AppIdentityError: If the token appears to have been issued in the - future (up to clock skew). - AppIdentityError: If the token appears to have expired in the past - (up to clock skew). - """ - # Get the current time to use throughout. - now = int(time.time()) - - # Make sure issued at and expiration are in the payload. - issued_at = payload_dict.get('iat') - if issued_at is None: - raise AppIdentityError( - 'No iat field in token: {0}'.format(payload_dict)) - expiration = payload_dict.get('exp') - if expiration is None: - raise AppIdentityError( - 'No exp field in token: {0}'.format(payload_dict)) - - # Make sure the expiration gives an acceptable token lifetime. - if expiration >= now + MAX_TOKEN_LIFETIME_SECS: - raise AppIdentityError( - 'exp field too far in future: {0}'.format(payload_dict)) - - # Make sure (up to clock skew) that the token wasn't issued in the future. - earliest = issued_at - CLOCK_SKEW_SECS - if now < earliest: - raise AppIdentityError('Token used too early, {0} < {1}: {2}'.format( - now, earliest, payload_dict)) - # Make sure (up to clock skew) that the token isn't already expired. - latest = expiration + CLOCK_SKEW_SECS - if now > latest: - raise AppIdentityError('Token used too late, {0} > {1}: {2}'.format( - now, latest, payload_dict)) - - -def verify_signed_jwt_with_certs(jwt, certs, audience=None): - """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. - """ - jwt = _helpers._to_bytes(jwt) - - if jwt.count(b'.') != 2: - raise AppIdentityError( - 'Wrong number of segments in token: {0}'.format(jwt)) - - header, payload, signature = jwt.split(b'.') - message_to_sign = header + b'.' + payload - signature = _helpers._urlsafe_b64decode(signature) - - # Parse token. - payload_bytes = _helpers._urlsafe_b64decode(payload) - try: - payload_dict = json.loads(_helpers._from_bytes(payload_bytes)) - except: - raise AppIdentityError('Can\'t parse token: {0}'.format(payload_bytes)) - - # Verify that the signature matches the message. - _verify_signature(message_to_sign, signature, certs.values()) - - # Verify the issued at and created times in the payload. - _verify_time_range(payload_dict) - - # Check audience. - _check_audience(payload_dict, audience) - - return payload_dict diff --git a/src/oauth2client/oauth2client/file.py b/src/oauth2client/oauth2client/file.py deleted file mode 100644 index 3551c80d..00000000 --- a/src/oauth2client/oauth2client/file.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. -""" - -import os -import threading - -from oauth2client import _helpers -from oauth2client import client - - -class Storage(client.Storage): - """Store and retrieve a single credential to and from a file.""" - - def __init__(self, filename): - super(Storage, self).__init__(lock=threading.Lock()) - self._filename = filename - - def locked_get(self): - """Retrieve Credential from file. - - Returns: - oauth2client.client.Credentials - - Raises: - IOError if the file is a symbolic link. - """ - credentials = None - _helpers.validate_file(self._filename) - try: - f = open(self._filename, 'rb') - content = f.read() - f.close() - except IOError: - return credentials - - try: - credentials = client.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(0o177) - 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: - IOError if the file is a symbolic link. - """ - self._create_file_if_needed() - _helpers.validate_file(self._filename) - f = open(self._filename, 'w') - 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/src/oauth2client/oauth2client/service_account.py b/src/oauth2client/oauth2client/service_account.py deleted file mode 100644 index 540bfaaa..00000000 --- a/src/oauth2client/oauth2client/service_account.py +++ /dev/null @@ -1,685 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. - -"""oauth2client Service account credentials class.""" - -import base64 -import copy -import datetime -import json -import time - -import oauth2client -from oauth2client import _helpers -from oauth2client import client -from oauth2client import crypt -from oauth2client import transport - - -_PASSWORD_DEFAULT = 'notasecret' -_PKCS12_KEY = '_private_key_pkcs12' -_PKCS12_ERROR = r""" -This library only implements PKCS#12 support via the pyOpenSSL library. -Either install pyOpenSSL, or please convert the .p12 file -to .pem format: - $ cat key.p12 | \ - > openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \ - > openssl rsa > key.pem -""" - - -class ServiceAccountCredentials(client.AssertionCredentials): - """Service Account credential for OAuth 2.0 signed JWT grants. - - Supports - - * JSON keyfile (typically contains a PKCS8 key stored as - PEM text) - * ``.p12`` key (stores PKCS12 key and certificate) - - Makes an assertion to server using a signed JWT assertion in exchange - for an access token. - - 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. - - Args: - service_account_email: string, The email associated with the - service account. - signer: ``crypt.Signer``, A signer which can be used to sign content. - scopes: List or string, (Optional) Scopes to use when acquiring - an access token. - private_key_id: string, (Optional) Private key identifier. Typically - only used with a JSON keyfile. Can be sent in the - header of a JWT token assertion. - client_id: string, (Optional) Client ID for the project that owns the - service account. - user_agent: string, (Optional) User agent to use when sending - request. - 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, Extra key-value pairs (both strings) to send in the - payload body when making an assertion. - """ - - MAX_TOKEN_LIFETIME_SECS = 3600 - """Max lifetime of the token (one hour, in seconds).""" - - NON_SERIALIZED_MEMBERS = ( - frozenset(['_signer']) | - client.AssertionCredentials.NON_SERIALIZED_MEMBERS) - """Members that aren't serialized when object is converted to JSON.""" - - # Can be over-ridden by factory constructors. Used for - # serialization/deserialization purposes. - _private_key_pkcs8_pem = None - _private_key_pkcs12 = None - _private_key_password = None - - def __init__(self, - service_account_email, - signer, - scopes='', - private_key_id=None, - client_id=None, - user_agent=None, - token_uri=oauth2client.GOOGLE_TOKEN_URI, - revoke_uri=oauth2client.GOOGLE_REVOKE_URI, - **kwargs): - - super(ServiceAccountCredentials, self).__init__( - None, user_agent=user_agent, token_uri=token_uri, - revoke_uri=revoke_uri) - - self._service_account_email = service_account_email - self._signer = signer - self._scopes = _helpers.scopes_to_string(scopes) - self._private_key_id = private_key_id - self.client_id = client_id - self._user_agent = user_agent - self._kwargs = kwargs - - def _to_json(self, strip, to_serialize=None): - """Utility function that creates JSON repr. of a credentials object. - - Over-ride is needed since PKCS#12 keys will not in general be JSON - serializable. - - Args: - strip: array, An array of names of members to exclude from the - JSON. - to_serialize: dict, (Optional) The properties for this object - that will be serialized. This allows callers to - modify before serializing. - - Returns: - string, a JSON representation of this instance, suitable to pass to - from_json(). - """ - if to_serialize is None: - to_serialize = copy.copy(self.__dict__) - pkcs12_val = to_serialize.get(_PKCS12_KEY) - if pkcs12_val is not None: - to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val) - return super(ServiceAccountCredentials, self)._to_json( - strip, to_serialize=to_serialize) - - @classmethod - def _from_parsed_json_keyfile(cls, keyfile_dict, scopes, - token_uri=None, revoke_uri=None): - """Helper for factory constructors from JSON keyfile. - - Args: - keyfile_dict: dict-like object, The parsed dictionary-like object - containing the contents of the JSON keyfile. - scopes: List or string, Scopes to use when acquiring an - access token. - token_uri: string, URI for OAuth 2.0 provider token endpoint. - If unset and not present in keyfile_dict, defaults - to Google's endpoints. - revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint. - If unset and not present in keyfile_dict, defaults - to Google's endpoints. - - Returns: - ServiceAccountCredentials, a credentials object created from - the keyfile contents. - - Raises: - ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`. - KeyError, if one of the expected keys is not present in - the keyfile. - """ - creds_type = keyfile_dict.get('type') - if creds_type != client.SERVICE_ACCOUNT: - raise ValueError('Unexpected credentials type', creds_type, - 'Expected', client.SERVICE_ACCOUNT) - - service_account_email = keyfile_dict['client_email'] - private_key_pkcs8_pem = keyfile_dict['private_key'] - private_key_id = keyfile_dict['private_key_id'] - client_id = keyfile_dict['client_id'] - if not token_uri: - token_uri = keyfile_dict.get('token_uri', - oauth2client.GOOGLE_TOKEN_URI) - if not revoke_uri: - revoke_uri = keyfile_dict.get('revoke_uri', - oauth2client.GOOGLE_REVOKE_URI) - - signer = crypt.Signer.from_string(private_key_pkcs8_pem) - credentials = cls(service_account_email, signer, scopes=scopes, - private_key_id=private_key_id, - client_id=client_id, token_uri=token_uri, - revoke_uri=revoke_uri) - credentials._private_key_pkcs8_pem = private_key_pkcs8_pem - return credentials - - @classmethod - def from_json_keyfile_name(cls, filename, scopes='', - token_uri=None, revoke_uri=None): - - """Factory constructor from JSON keyfile by name. - - Args: - filename: string, The location of the keyfile. - scopes: List or string, (Optional) Scopes to use when acquiring an - access token. - token_uri: string, URI for OAuth 2.0 provider token endpoint. - If unset and not present in the key file, defaults - to Google's endpoints. - revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint. - If unset and not present in the key file, defaults - to Google's endpoints. - - Returns: - ServiceAccountCredentials, a credentials object created from - the keyfile. - - Raises: - ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`. - KeyError, if one of the expected keys is not present in - the keyfile. - """ - with open(filename, 'r') as file_obj: - client_credentials = json.load(file_obj) - return cls._from_parsed_json_keyfile(client_credentials, scopes, - token_uri=token_uri, - revoke_uri=revoke_uri) - - @classmethod - def from_json_keyfile_dict(cls, keyfile_dict, scopes='', - token_uri=None, revoke_uri=None): - """Factory constructor from parsed JSON keyfile. - - Args: - keyfile_dict: dict-like object, The parsed dictionary-like object - containing the contents of the JSON keyfile. - scopes: List or string, (Optional) Scopes to use when acquiring an - access token. - token_uri: string, URI for OAuth 2.0 provider token endpoint. - If unset and not present in keyfile_dict, defaults - to Google's endpoints. - revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint. - If unset and not present in keyfile_dict, defaults - to Google's endpoints. - - Returns: - ServiceAccountCredentials, a credentials object created from - the keyfile. - - Raises: - ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`. - KeyError, if one of the expected keys is not present in - the keyfile. - """ - return cls._from_parsed_json_keyfile(keyfile_dict, scopes, - token_uri=token_uri, - revoke_uri=revoke_uri) - - @classmethod - def _from_p12_keyfile_contents(cls, service_account_email, - private_key_pkcs12, - private_key_password=None, scopes='', - token_uri=oauth2client.GOOGLE_TOKEN_URI, - revoke_uri=oauth2client.GOOGLE_REVOKE_URI): - """Factory constructor from JSON keyfile. - - Args: - service_account_email: string, The email associated with the - service account. - private_key_pkcs12: string, The contents of a PKCS#12 keyfile. - private_key_password: string, (Optional) Password for PKCS#12 - private key. Defaults to ``notasecret``. - scopes: List or string, (Optional) Scopes to use when acquiring an - access token. - 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. - - Returns: - ServiceAccountCredentials, a credentials object created from - the keyfile. - - Raises: - NotImplementedError if pyOpenSSL is not installed / not the - active crypto library. - """ - if private_key_password is None: - private_key_password = _PASSWORD_DEFAULT - if crypt.Signer is not crypt.OpenSSLSigner: - raise NotImplementedError(_PKCS12_ERROR) - signer = crypt.Signer.from_string(private_key_pkcs12, - private_key_password) - credentials = cls(service_account_email, signer, scopes=scopes, - token_uri=token_uri, revoke_uri=revoke_uri) - credentials._private_key_pkcs12 = private_key_pkcs12 - credentials._private_key_password = private_key_password - return credentials - - @classmethod - def from_p12_keyfile(cls, service_account_email, filename, - private_key_password=None, scopes='', - token_uri=oauth2client.GOOGLE_TOKEN_URI, - revoke_uri=oauth2client.GOOGLE_REVOKE_URI): - - """Factory constructor from JSON keyfile. - - Args: - service_account_email: string, The email associated with the - service account. - filename: string, The location of the PKCS#12 keyfile. - private_key_password: string, (Optional) Password for PKCS#12 - private key. Defaults to ``notasecret``. - scopes: List or string, (Optional) Scopes to use when acquiring an - access token. - 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. - - Returns: - ServiceAccountCredentials, a credentials object created from - the keyfile. - - Raises: - NotImplementedError if pyOpenSSL is not installed / not the - active crypto library. - """ - with open(filename, 'rb') as file_obj: - private_key_pkcs12 = file_obj.read() - return cls._from_p12_keyfile_contents( - service_account_email, private_key_pkcs12, - private_key_password=private_key_password, scopes=scopes, - token_uri=token_uri, revoke_uri=revoke_uri) - - @classmethod - def from_p12_keyfile_buffer(cls, service_account_email, file_buffer, - private_key_password=None, scopes='', - token_uri=oauth2client.GOOGLE_TOKEN_URI, - revoke_uri=oauth2client.GOOGLE_REVOKE_URI): - """Factory constructor from JSON keyfile. - - Args: - service_account_email: string, The email associated with the - service account. - file_buffer: stream, A buffer that implements ``read()`` - and contains the PKCS#12 key contents. - private_key_password: string, (Optional) Password for PKCS#12 - private key. Defaults to ``notasecret``. - scopes: List or string, (Optional) Scopes to use when acquiring an - access token. - 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. - - Returns: - ServiceAccountCredentials, a credentials object created from - the keyfile. - - Raises: - NotImplementedError if pyOpenSSL is not installed / not the - active crypto library. - """ - private_key_pkcs12 = file_buffer.read() - return cls._from_p12_keyfile_contents( - service_account_email, private_key_pkcs12, - private_key_password=private_key_password, scopes=scopes, - token_uri=token_uri, revoke_uri=revoke_uri) - - def _generate_assertion(self): - """Generate the assertion that will be used in the request.""" - now = int(time.time()) - payload = { - 'aud': self.token_uri, - 'scope': self._scopes, - 'iat': now, - 'exp': now + self.MAX_TOKEN_LIFETIME_SECS, - 'iss': self._service_account_email, - } - payload.update(self._kwargs) - return crypt.make_signed_jwt(self._signer, payload, - key_id=self._private_key_id) - - def sign_blob(self, blob): - """Cryptographically sign a blob (of bytes). - - Implements abstract method - :meth:`oauth2client.client.AssertionCredentials.sign_blob`. - - Args: - blob: bytes, Message to be signed. - - Returns: - tuple, A pair of the private key ID used to sign the blob and - the signed contents. - """ - return self._private_key_id, self._signer.sign(blob) - - @property - def service_account_email(self): - """Get the email for the current service account. - - Returns: - string, The email associated with the service account. - """ - return self._service_account_email - - @property - def serialization_data(self): - # NOTE: This is only useful for JSON keyfile. - return { - 'type': 'service_account', - 'client_email': self._service_account_email, - 'private_key_id': self._private_key_id, - 'private_key': self._private_key_pkcs8_pem, - 'client_id': self.client_id, - } - - @classmethod - def from_json(cls, json_data): - """Deserialize a JSON-serialized instance. - - Inverse to :meth:`to_json`. - - Args: - json_data: dict or string, Serialized JSON (as a string or an - already parsed dictionary) representing a credential. - - Returns: - ServiceAccountCredentials from the serialized data. - """ - if not isinstance(json_data, dict): - json_data = json.loads(_helpers._from_bytes(json_data)) - - private_key_pkcs8_pem = None - pkcs12_val = json_data.get(_PKCS12_KEY) - password = None - if pkcs12_val is None: - private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem'] - signer = crypt.Signer.from_string(private_key_pkcs8_pem) - else: - # NOTE: This assumes that private_key_pkcs8_pem is not also - # in the serialized data. This would be very incorrect - # state. - pkcs12_val = base64.b64decode(pkcs12_val) - password = json_data['_private_key_password'] - signer = crypt.Signer.from_string(pkcs12_val, password) - - credentials = cls( - json_data['_service_account_email'], - signer, - scopes=json_data['_scopes'], - private_key_id=json_data['_private_key_id'], - client_id=json_data['client_id'], - user_agent=json_data['_user_agent'], - **json_data['_kwargs'] - ) - if private_key_pkcs8_pem is not None: - credentials._private_key_pkcs8_pem = private_key_pkcs8_pem - if pkcs12_val is not None: - credentials._private_key_pkcs12 = pkcs12_val - if password is not None: - credentials._private_key_password = password - credentials.invalid = json_data['invalid'] - credentials.access_token = json_data['access_token'] - credentials.token_uri = json_data['token_uri'] - credentials.revoke_uri = json_data['revoke_uri'] - token_expiry = json_data.get('token_expiry', None) - if token_expiry is not None: - credentials.token_expiry = datetime.datetime.strptime( - token_expiry, client.EXPIRY_FORMAT) - return credentials - - def create_scoped_required(self): - return not self._scopes - - def create_scoped(self, scopes): - result = self.__class__(self._service_account_email, - self._signer, - scopes=scopes, - private_key_id=self._private_key_id, - client_id=self.client_id, - user_agent=self._user_agent, - **self._kwargs) - result.token_uri = self.token_uri - result.revoke_uri = self.revoke_uri - result._private_key_pkcs8_pem = self._private_key_pkcs8_pem - result._private_key_pkcs12 = self._private_key_pkcs12 - result._private_key_password = self._private_key_password - return result - - def create_with_claims(self, claims): - """Create credentials that specify additional claims. - - Args: - claims: dict, key-value pairs for claims. - - Returns: - ServiceAccountCredentials, a copy of the current service account - credentials with updated claims to use when obtaining access - tokens. - """ - new_kwargs = dict(self._kwargs) - new_kwargs.update(claims) - result = self.__class__(self._service_account_email, - self._signer, - scopes=self._scopes, - private_key_id=self._private_key_id, - client_id=self.client_id, - user_agent=self._user_agent, - **new_kwargs) - result.token_uri = self.token_uri - result.revoke_uri = self.revoke_uri - result._private_key_pkcs8_pem = self._private_key_pkcs8_pem - result._private_key_pkcs12 = self._private_key_pkcs12 - result._private_key_password = self._private_key_password - return result - - def create_delegated(self, sub): - """Create credentials that act as domain-wide delegation of authority. - - Use the ``sub`` parameter as the subject to delegate on behalf of - that user. - - For example:: - - >>> account_sub = 'foo@email.com' - >>> delegate_creds = creds.create_delegated(account_sub) - - Args: - sub: string, An email address that this service account will - act on behalf of (via domain-wide delegation). - - Returns: - ServiceAccountCredentials, a copy of the current service account - updated to act on behalf of ``sub``. - """ - return self.create_with_claims({'sub': sub}) - - -def _datetime_to_secs(utc_time): - # TODO(issue 298): use time_delta.total_seconds() - # time_delta.total_seconds() not supported in Python 2.6 - epoch = datetime.datetime(1970, 1, 1) - time_delta = utc_time - epoch - return time_delta.days * 86400 + time_delta.seconds - - -class _JWTAccessCredentials(ServiceAccountCredentials): - """Self signed JWT credentials. - - Makes an assertion to server using a self signed JWT from service account - credentials. These credentials do NOT use OAuth 2.0 and instead - authenticate directly. - """ - _MAX_TOKEN_LIFETIME_SECS = 3600 - """Max lifetime of the token (one hour, in seconds).""" - - def __init__(self, - service_account_email, - signer, - scopes=None, - private_key_id=None, - client_id=None, - user_agent=None, - token_uri=oauth2client.GOOGLE_TOKEN_URI, - revoke_uri=oauth2client.GOOGLE_REVOKE_URI, - additional_claims=None): - if additional_claims is None: - additional_claims = {} - super(_JWTAccessCredentials, self).__init__( - service_account_email, - signer, - private_key_id=private_key_id, - client_id=client_id, - user_agent=user_agent, - token_uri=token_uri, - revoke_uri=revoke_uri, - **additional_claims) - - def authorize(self, http): - """Authorize an httplib2.Http instance with a JWT assertion. - - Unless specified, the 'aud' of the assertion will be the base - uri of the request. - - 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) - """ - transport.wrap_http_for_jwt_access(self, http) - return http - - def get_access_token(self, http=None, additional_claims=None): - """Create a signed jwt. - - Args: - http: unused - additional_claims: dict, additional claims to add to - the payload of the JWT. - Returns: - An AccessTokenInfo with the signed jwt - """ - if additional_claims is None: - if self.access_token is None or self.access_token_expired: - self.refresh(None) - return client.AccessTokenInfo( - access_token=self.access_token, expires_in=self._expires_in()) - else: - # Create a 1 time token - token, unused_expiry = self._create_token(additional_claims) - return client.AccessTokenInfo( - access_token=token, expires_in=self._MAX_TOKEN_LIFETIME_SECS) - - def revoke(self, http): - """Cannot revoke JWTAccessCredentials tokens.""" - pass - - def create_scoped_required(self): - # JWTAccessCredentials are unscoped by definition - return True - - def create_scoped(self, scopes, token_uri=oauth2client.GOOGLE_TOKEN_URI, - revoke_uri=oauth2client.GOOGLE_REVOKE_URI): - # Returns an OAuth2 credentials with the given scope - result = ServiceAccountCredentials(self._service_account_email, - self._signer, - scopes=scopes, - private_key_id=self._private_key_id, - client_id=self.client_id, - user_agent=self._user_agent, - token_uri=token_uri, - revoke_uri=revoke_uri, - **self._kwargs) - if self._private_key_pkcs8_pem is not None: - result._private_key_pkcs8_pem = self._private_key_pkcs8_pem - if self._private_key_pkcs12 is not None: - result._private_key_pkcs12 = self._private_key_pkcs12 - if self._private_key_password is not None: - result._private_key_password = self._private_key_password - return result - - def refresh(self, http): - """Refreshes the access_token. - - The HTTP object is unused since no request needs to be made to - get a new token, it can just be generated locally. - - Args: - http: unused HTTP object - """ - self._refresh(None) - - def _refresh(self, http): - """Refreshes the access_token. - - Args: - http: unused HTTP object - """ - self.access_token, self.token_expiry = self._create_token() - - def _create_token(self, additional_claims=None): - now = client._UTCNOW() - lifetime = datetime.timedelta(seconds=self._MAX_TOKEN_LIFETIME_SECS) - expiry = now + lifetime - payload = { - 'iat': _datetime_to_secs(now), - 'exp': _datetime_to_secs(expiry), - 'iss': self._service_account_email, - 'sub': self._service_account_email - } - payload.update(self._kwargs) - if additional_claims is not None: - payload.update(additional_claims) - jwt = crypt.make_signed_jwt(self._signer, payload, - key_id=self._private_key_id) - return jwt.decode('ascii'), expiry diff --git a/src/oauth2client/oauth2client/tools.py b/src/oauth2client/oauth2client/tools.py deleted file mode 100644 index 51669934..00000000 --- a/src/oauth2client/oauth2client/tools.py +++ /dev/null @@ -1,256 +0,0 @@ -# Copyright 2014 Google Inc. All rights reserved. -# -# 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. -""" - -from __future__ import print_function - -import logging -import socket -import sys - -from six.moves import BaseHTTPServer -from six.moves import http_client -from six.moves import input -from six.moves import urllib - -from oauth2client import _helpers -from oauth2client import client - - -__all__ = ['argparser', 'run_flow', 'message_if_missing'] - -_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: - - {file_path} - -with information from the APIs Console . - -""" - -_FAILED_START_MESSAGE = """ -Failed to start a local webserver listening on either port 8080 -or port 8090. Please check your firewall settings and locally -running programs that may be blocking or using those ports. - -Falling back to --noauth_local_webserver and continuing with -authorization. -""" - -_BROWSER_OPENED_MESSAGE = """ -Your browser has been opened to visit: - - {address} - -If your browser is on a different machine then exit and re-run this -application with the command-line parameter - - --noauth_local_webserver -""" - -_GO_TO_LINK_MESSAGE = """ -Go to the following link in your browser: - - {address} -""" - - -def _CreateArgumentParser(): - try: - import argparse - except ImportError: # pragma: NO COVER - return None - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument('--auth_host_name', default='localhost', - help='Hostname when running a local web server.') - parser.add_argument('--noauth_local_webserver', action='store_true', - default=False, help='Do not run a local web server.') - parser.add_argument('--auth_host_port', default=[8080, 8090], type=int, - nargs='*', help='Port web server should listen on.') - parser.add_argument( - '--logging_level', default='ERROR', - choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], - help='Set the logging level of detail.') - return parser - - -# argparser 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 = _CreateArgumentParser() - - -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(self): - """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. - """ - self.send_response(http_client.OK) - self.send_header('Content-type', 'text/html') - self.end_headers() - parts = urllib.parse.urlparse(self.path) - query = _helpers.parse_unique_urlencoded(parts.query) - self.server.query_params = query - self.wfile.write( - b'Authentication Status') - self.wfile.write( - b'

The authentication flow has completed.

') - self.wfile.write(b'') - - def log_message(self, format, *args): - """Do not log messages to stdout while running as cmd. line program.""" - - -@_helpers.positional(3) -def run_flow(flow, storage, flags=None, 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`` (string, default: ``localhost``) - Host name to use when running a local web server to handle - redirects during OAuth authorization. - - ``--auth_host_port`` (integer, default: ``[8080, 8090]``) - Port to use when running a local web server to handle redirects - during OAuth authorization. Repeat this option to specify a list - of values. - - ``--[no]auth_local_webserver`` (boolean, default: ``True``) - Run a local web server to handle redirects during OAuth - authorization. - - 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.argparser]) - 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.Namespace``, (Optional) The command-line flags. This - is the object returned from calling ``parse_args()`` on - ``argparse.ArgumentParser`` as described above. Defaults - to ``argparser.parse_args()``. - http: An instance of ``httplib2.Http.request`` or something that - acts like it. - - Returns: - Credentials, the obtained credential. - """ - if flags is None: - flags = argparser.parse_args() - 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: - pass - else: - success = True - break - flags.noauth_local_webserver = not success - if not success: - print(_FAILED_START_MESSAGE) - - if not flags.noauth_local_webserver: - oauth_callback = 'http://{host}:{port}/'.format( - host=flags.auth_host_name, port=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: - import webbrowser - webbrowser.open(authorize_url, new=1, autoraise=True) - print(_BROWSER_OPENED_MESSAGE.format(address=authorize_url)) - else: - print(_GO_TO_LINK_MESSAGE.format(address=authorize_url)) - - 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 = input('Enter verification code: ').strip() - - try: - credential = flow.step2_exchange(code, http=http) - except client.FlowExchangeError as e: - sys.exit('Authentication has failed: {0}'.format(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.format(file_path=filename) diff --git a/src/oauth2client/oauth2client/transport.py b/src/oauth2client/oauth2client/transport.py deleted file mode 100644 index 79a61f1c..00000000 --- a/src/oauth2client/oauth2client/transport.py +++ /dev/null @@ -1,285 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# 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 logging - -import httplib2 -import six -from six.moves import http_client - -from oauth2client import _helpers - - -_LOGGER = logging.getLogger(__name__) -# Properties present in file-like streams / buffers. -_STREAM_PROPERTIES = ('read', 'seek', 'tell') - -# Google Data client libraries may need to set this to [401, 403]. -REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,) - - -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) - - -def get_cached_http(): - """Return an HTTP object which caches results returned. - - This is intended to be used in methods like - oauth2client.client.verify_id_token(), which calls to the same URI - to retrieve certs. - - Returns: - httplib2.Http, an HTTP object with a MemoryCache - """ - return _CACHED_HTTP - - -def get_http_object(*args, **kwargs): - """Return a new HTTP object. - - Args: - *args: tuple, The positional arguments to be passed when - contructing a new HTTP object. - **kwargs: dict, The keyword arguments to be passed when - contructing a new HTTP object. - - Returns: - httplib2.Http, an HTTP object. - """ - return httplib2.Http(*args, **kwargs) - - -def _initialize_headers(headers): - """Creates a copy of the headers. - - Args: - headers: dict, request headers to copy. - - Returns: - dict, the copied headers or a new dictionary if the headers - were None. - """ - return {} if headers is None else dict(headers) - - -def _apply_user_agent(headers, user_agent): - """Adds a user-agent to the headers. - - Args: - headers: dict, request headers to add / modify user - agent within. - user_agent: str, the user agent to add. - - Returns: - dict, the original headers passed in, but modified if the - user agent is not None. - """ - if user_agent is not None: - if 'user-agent' in headers: - headers['user-agent'] = (user_agent + ' ' + headers['user-agent']) - else: - headers['user-agent'] = user_agent - - return headers - - -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 six.iteritems(headers): - if not isinstance(k, six.binary_type): - k = str(k) - if not isinstance(v, six.binary_type): - v = str(v) - clean[_helpers._to_bytes(k)] = _helpers._to_bytes(v) - except UnicodeEncodeError: - from oauth2client.client import NonAsciiHeaderError - raise NonAsciiHeaderError(k, ': ', v) - return clean - - -def wrap_http_for_auth(credentials, http): - """Prepares an HTTP object's request method for auth. - - Wraps HTTP requests with logic to catch auth failures (typically - identified via a 401 status code). In the event of failure, tries - to refresh the token used and then retry the original request. - - Args: - credentials: Credentials, the credentials used to identify - the authenticated user. - http: httplib2.Http, an http object to be used to make - auth requests. - """ - orig_request_method = http.request - - # The closure that will replace 'httplib2.Http.request'. - def new_request(uri, method='GET', body=None, headers=None, - redirections=httplib2.DEFAULT_MAX_REDIRECTS, - connection_type=None): - if not credentials.access_token: - _LOGGER.info('Attempting refresh to obtain ' - 'initial access_token') - credentials._refresh(orig_request_method) - - # Clone and modify the request headers to add the appropriate - # Authorization header. - headers = _initialize_headers(headers) - credentials.apply(headers) - _apply_user_agent(headers, credentials.user_agent) - - body_stream_position = None - # Check if the body is a file-like stream. - if all(getattr(body, stream_prop, None) for stream_prop in - _STREAM_PROPERTIES): - body_stream_position = body.tell() - - resp, content = request(orig_request_method, uri, method, body, - clean_headers(headers), - redirections, connection_type) - - # A stored token may expire between the time it is retrieved and - # the time the request is made, so we may need to try twice. - max_refresh_attempts = 2 - for refresh_attempt in range(max_refresh_attempts): - if resp.status not in REFRESH_STATUS_CODES: - break - _LOGGER.info('Refreshing due to a %s (attempt %s/%s)', - resp.status, refresh_attempt + 1, - max_refresh_attempts) - credentials._refresh(orig_request_method) - credentials.apply(headers) - if body_stream_position is not None: - body.seek(body_stream_position) - - resp, content = request(orig_request_method, uri, method, body, - clean_headers(headers), - redirections, connection_type) - - return resp, content - - # Replace the request method with our own closure. - http.request = new_request - - # Set credentials as a property of the request method. - http.request.credentials = credentials - - -def wrap_http_for_jwt_access(credentials, http): - """Prepares an HTTP object's request method for JWT access. - - Wraps HTTP requests with logic to catch auth failures (typically - identified via a 401 status code). In the event of failure, tries - to refresh the token used and then retry the original request. - - Args: - credentials: _JWTAccessCredentials, the credentials used to identify - a service account that uses JWT access tokens. - http: httplib2.Http, an http object to be used to make - auth requests. - """ - orig_request_method = http.request - wrap_http_for_auth(credentials, http) - # The new value of ``http.request`` set by ``wrap_http_for_auth``. - authenticated_request_method = http.request - - # The closure that will replace 'httplib2.Http.request'. - def new_request(uri, method='GET', body=None, headers=None, - redirections=httplib2.DEFAULT_MAX_REDIRECTS, - connection_type=None): - if 'aud' in credentials._kwargs: - # Preemptively refresh token, this is not done for OAuth2 - if (credentials.access_token is None or - credentials.access_token_expired): - credentials.refresh(None) - return request(authenticated_request_method, uri, - method, body, headers, redirections, - connection_type) - else: - # If we don't have an 'aud' (audience) claim, - # create a 1-time token with the uri root as the audience - headers = _initialize_headers(headers) - _apply_user_agent(headers, credentials.user_agent) - uri_root = uri.split('?', 1)[0] - token, unused_expiry = credentials._create_token({'aud': uri_root}) - - headers['Authorization'] = 'Bearer ' + token - return request(orig_request_method, uri, method, body, - clean_headers(headers), - redirections, connection_type) - - # Replace the request method with our own closure. - http.request = new_request - - # Set credentials as a property of the request method. - http.request.credentials = credentials - - -def request(http, uri, method='GET', body=None, headers=None, - redirections=httplib2.DEFAULT_MAX_REDIRECTS, - connection_type=None): - """Make an HTTP request with an HTTP object and arguments. - - Args: - http: httplib2.Http, an http object to be used to make requests. - uri: string, The URI to be requested. - method: string, The HTTP method to use for the request. Defaults - to 'GET'. - body: string, The payload / body in HTTP request. By default - there is no payload. - headers: dict, Key-value pairs of request headers. By default - there are no headers. - redirections: int, The number of allowed 203 redirects for - the request. Defaults to 5. - connection_type: httplib.HTTPConnection, a subclass to be used for - establishing connection. If not set, the type - will be determined from the ``uri``. - - Returns: - tuple, a pair of a httplib2.Response with the status code and other - headers and the bytes of the content returned. - """ - # NOTE: Allowing http or http.request is temporary (See Issue 601). - http_callable = getattr(http, 'request', http) - return http_callable(uri, method=method, body=body, headers=headers, - redirections=redirections, - connection_type=connection_type) - - -_CACHED_HTTP = httplib2.Http(MemoryCache())