diff options
author | alexv-smirnov <alex@ydb.tech> | 2023-12-01 12:02:50 +0300 |
---|---|---|
committer | alexv-smirnov <alex@ydb.tech> | 2023-12-01 13:28:10 +0300 |
commit | 0e578a4c44d4abd539d9838347b9ebafaca41dfb (patch) | |
tree | a0c1969c37f818c830ebeff9c077eacf30be6ef8 /contrib/python/oauth2client | |
parent | 84f2d3d4cc985e63217cff149bd2e6d67ae6fe22 (diff) | |
download | ydb-0e578a4c44d4abd539d9838347b9ebafaca41dfb.tar.gz |
Change "ya.make"
Diffstat (limited to 'contrib/python/oauth2client')
77 files changed, 18144 insertions, 0 deletions
diff --git a/contrib/python/oauth2client/py2/.dist-info/METADATA b/contrib/python/oauth2client/py2/.dist-info/METADATA new file mode 100644 index 0000000000..b4b28000b1 --- /dev/null +++ b/contrib/python/oauth2client/py2/.dist-info/METADATA @@ -0,0 +1,34 @@ +Metadata-Version: 2.1 +Name: oauth2client +Version: 4.1.3 +Summary: OAuth 2.0 client library +Home-page: http://github.com/google/oauth2client/ +Author: Google Inc. +Author-email: jonwayne+oauth2client@google.com +License: Apache 2.0 +Keywords: google oauth 2.0 http client +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Development Status :: 7 - Inactive +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: POSIX +Classifier: Topic :: Internet :: WWW/HTTP +Requires-Dist: httplib2 (>=0.9.1) +Requires-Dist: pyasn1 (>=0.1.7) +Requires-Dist: pyasn1-modules (>=0.0.5) +Requires-Dist: rsa (>=3.1.4) +Requires-Dist: six (>=1.6.1) + +oauth2client is a client library for OAuth 2.0. + +Note: oauth2client is now deprecated. No more features will be added to the + libraries and the core team is turning down support. We recommend you use + `google-auth <https://google-auth.readthedocs.io>`__ and + `oauthlib <http://oauthlib.readthedocs.io/>`__. + + diff --git a/contrib/python/oauth2client/py2/.dist-info/top_level.txt b/contrib/python/oauth2client/py2/.dist-info/top_level.txt new file mode 100644 index 0000000000..c636bd5953 --- /dev/null +++ b/contrib/python/oauth2client/py2/.dist-info/top_level.txt @@ -0,0 +1 @@ +oauth2client diff --git a/contrib/python/oauth2client/py2/LICENSE b/contrib/python/oauth2client/py2/LICENSE new file mode 100644 index 0000000000..c8d76dfc54 --- /dev/null +++ b/contrib/python/oauth2client/py2/LICENSE @@ -0,0 +1,210 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Dependent Modules +================= + +This code has the following dependencies +above and beyond the Python standard library: + +httplib2 - MIT License diff --git a/contrib/python/oauth2client/py2/README.md b/contrib/python/oauth2client/py2/README.md new file mode 100644 index 0000000000..5e7aade714 --- /dev/null +++ b/contrib/python/oauth2client/py2/README.md @@ -0,0 +1,33 @@ +[![Build Status](https://travis-ci.org/google/oauth2client.svg?branch=master)](https://travis-ci.org/google/oauth2client) +[![Coverage Status](https://coveralls.io/repos/google/oauth2client/badge.svg?branch=master&service=github)](https://coveralls.io/github/google/oauth2client?branch=master) +[![Documentation Status](https://readthedocs.org/projects/oauth2client/badge/?version=latest)](https://oauth2client.readthedocs.io/) + +This is a client library for accessing resources protected by OAuth 2.0. + +**Note**: oauth2client is now deprecated. No more features will be added to the +libraries and the core team is turning down support. We recommend you use +[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). For more details on the deprecation, see [oauth2client deprecation](https://google-auth.readthedocs.io/en/latest/oauth2client-deprecation.html). + +Installation +============ + +To install, simply run the following command in your terminal: + +```bash +$ pip install --upgrade oauth2client +``` + +Contributing +============ + +Please see the [CONTRIBUTING page][1] for more information. In particular, we +love pull requests -- but please make sure to sign the contributor license +agreement. + +Supported Python Versions +========================= + +We support Python 2.7 and 3.4+. More information [in the docs][2]. + +[1]: https://github.com/google/oauth2client/blob/master/CONTRIBUTING.md +[2]: https://oauth2client.readthedocs.io/#supported-python-versions diff --git a/contrib/python/oauth2client/py2/oauth2client/__init__.py b/contrib/python/oauth2client/py2/oauth2client/__init__.py new file mode 100644 index 0000000000..92bc191d43 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/__init__.py @@ -0,0 +1,24 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/_helpers.py b/contrib/python/oauth2client/py2/oauth2client/_helpers.py new file mode 100644 index 0000000000..e9123971bc --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/_helpers.py @@ -0,0 +1,341 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/_openssl_crypt.py b/contrib/python/oauth2client/py2/oauth2client/_openssl_crypt.py new file mode 100644 index 0000000000..77fac74354 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/_openssl_crypt.py @@ -0,0 +1,136 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/_pkce.py b/contrib/python/oauth2client/py2/oauth2client/_pkce.py new file mode 100644 index 0000000000..e4952d8c2f --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/_pkce.py @@ -0,0 +1,67 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/_pure_python_crypt.py b/contrib/python/oauth2client/py2/oauth2client/_pure_python_crypt.py new file mode 100644 index 0000000000..2c5d43aae9 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/_pure_python_crypt.py @@ -0,0 +1,184 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/_pycrypto_crypt.py b/contrib/python/oauth2client/py2/oauth2client/_pycrypto_crypt.py new file mode 100644 index 0000000000..fd2ce0cd72 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/_pycrypto_crypt.py @@ -0,0 +1,124 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/client.py b/contrib/python/oauth2client/py2/oauth2client/client.py new file mode 100644 index 0000000000..7618960e44 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/client.py @@ -0,0 +1,2170 @@ +# 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('<an access token>', + '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/contrib/python/oauth2client/py2/oauth2client/clientsecrets.py b/contrib/python/oauth2client/py2/oauth2client/clientsecrets.py new file mode 100644 index 0000000000..1598142e87 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/clientsecrets.py @@ -0,0 +1,173 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/__init__.py b/contrib/python/oauth2client/py2/oauth2client/contrib/__init__.py new file mode 100644 index 0000000000..ecfd06c968 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/__init__.py @@ -0,0 +1,6 @@ +"""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/contrib/python/oauth2client/py2/oauth2client/contrib/_appengine_ndb.py b/contrib/python/oauth2client/py2/oauth2client/contrib/_appengine_ndb.py new file mode 100644 index 0000000000..c863e8f4e7 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/_appengine_ndb.py @@ -0,0 +1,163 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/_metadata.py b/contrib/python/oauth2client/py2/oauth2client/contrib/_metadata.py new file mode 100644 index 0000000000..564cd398da --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/_metadata.py @@ -0,0 +1,118 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/appengine.py b/contrib/python/oauth2client/py2/oauth2client/contrib/appengine.py new file mode 100644 index 0000000000..c1326eeb57 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/appengine.py @@ -0,0 +1,910 @@ +# 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('<html><body>') + request_handler.response.out.write(_safe_html(self._message)) + request_handler.response.out.write('</body></html>') + + 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/contrib/python/oauth2client/py2/oauth2client/contrib/devshell.py b/contrib/python/oauth2client/py2/oauth2client/contrib/devshell.py new file mode 100644 index 0000000000..691765f097 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/devshell.py @@ -0,0 +1,152 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/dictionary_storage.py b/contrib/python/oauth2client/py2/oauth2client/contrib/dictionary_storage.py new file mode 100644 index 0000000000..6ee333fa7c --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/dictionary_storage.py @@ -0,0 +1,65 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/__init__.py b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/__init__.py new file mode 100644 index 0000000000..644a8f9fb7 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/__init__.py @@ -0,0 +1,489 @@ +# 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 <https://console.developers.google.com/project/_/apiui/credential>`. + +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: <a href="{0}">Authorize' + '</a>'.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: <a href="{0}">Authorize' + '</a>'.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/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/apps.py b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/apps.py new file mode 100644 index 0000000000..86676b91a8 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/apps.py @@ -0,0 +1,32 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/decorators.py b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/decorators.py new file mode 100644 index 0000000000..e62e171071 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/decorators.py @@ -0,0 +1,145 @@ +# 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: + <a href="{0}">Authorize</a>'.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/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/models.py b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/models.py new file mode 100644 index 0000000000..37cc697054 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/models.py @@ -0,0 +1,82 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/signals.py b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/signals.py new file mode 100644 index 0000000000..e9356b4dcb --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/signals.py @@ -0,0 +1,28 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/site.py b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/site.py new file mode 100644 index 0000000000..631f79bef4 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/site.py @@ -0,0 +1,26 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/storage.py b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/storage.py new file mode 100644 index 0000000000..5682919bc0 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/storage.py @@ -0,0 +1,81 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/views.py b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/views.py new file mode 100644 index 0000000000..1835208a96 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/django_util/views.py @@ -0,0 +1,193 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/flask_util.py b/contrib/python/oauth2client/py2/oauth2client/contrib/flask_util.py new file mode 100644 index 0000000000..fabd613b46 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/flask_util.py @@ -0,0 +1,557 @@ +# 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 <https://console.developers.google.com/project/_/\ +apiui/credential>`__. + +.. 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 +<https://developers.google.com/identity/protocols/OpenIDConnect?hl=en>`__, 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 <https://developers.google.com\ +/identity/protocols/OAuth2WebServer?hl=en#incrementalAuth>`__. 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/contrib/python/oauth2client/py2/oauth2client/contrib/gce.py b/contrib/python/oauth2client/py2/oauth2client/contrib/gce.py new file mode 100644 index 0000000000..aaab15ffce --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/gce.py @@ -0,0 +1,156 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/keyring_storage.py b/contrib/python/oauth2client/py2/oauth2client/contrib/keyring_storage.py new file mode 100644 index 0000000000..4af944881a --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/keyring_storage.py @@ -0,0 +1,95 @@ +# 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 + <http://pypi.python.org/pypi/keyring/>. 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 <http://pypi.python.org/pypi/keyring/> 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/contrib/python/oauth2client/py2/oauth2client/contrib/multiprocess_file_storage.py b/contrib/python/oauth2client/py2/oauth2client/contrib/multiprocess_file_storage.py new file mode 100644 index 0000000000..e9e8c8cd1d --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/multiprocess_file_storage.py @@ -0,0 +1,355 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/sqlalchemy.py b/contrib/python/oauth2client/py2/oauth2client/contrib/sqlalchemy.py new file mode 100644 index 0000000000..7d9fd4b23f --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/sqlalchemy.py @@ -0,0 +1,173 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/contrib/xsrfutil.py b/contrib/python/oauth2client/py2/oauth2client/contrib/xsrfutil.py new file mode 100644 index 0000000000..7c3ec0353a --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/contrib/xsrfutil.py @@ -0,0 +1,101 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/crypt.py b/contrib/python/oauth2client/py2/oauth2client/crypt.py new file mode 100644 index 0000000000..13260982a6 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/crypt.py @@ -0,0 +1,250 @@ +# -*- 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/contrib/python/oauth2client/py2/oauth2client/file.py b/contrib/python/oauth2client/py2/oauth2client/file.py new file mode 100644 index 0000000000..3551c80d47 --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/file.py @@ -0,0 +1,95 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/service_account.py b/contrib/python/oauth2client/py2/oauth2client/service_account.py new file mode 100644 index 0000000000..540bfaaa1b --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/service_account.py @@ -0,0 +1,685 @@ +# 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/contrib/python/oauth2client/py2/oauth2client/tools.py b/contrib/python/oauth2client/py2/oauth2client/tools.py new file mode 100644 index 0000000000..51669934df --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/tools.py @@ -0,0 +1,256 @@ +# 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 <https://code.google.com/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'<html><head><title>Authentication Status</title></head>') + self.wfile.write( + b'<body><p>The authentication flow has completed.</p>') + self.wfile.write(b'</body></html>') + + 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/contrib/python/oauth2client/py2/oauth2client/transport.py b/contrib/python/oauth2client/py2/oauth2client/transport.py new file mode 100644 index 0000000000..79a61f1c1b --- /dev/null +++ b/contrib/python/oauth2client/py2/oauth2client/transport.py @@ -0,0 +1,285 @@ +# 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()) diff --git a/contrib/python/oauth2client/py2/ya.make b/contrib/python/oauth2client/py2/ya.make new file mode 100644 index 0000000000..73c0e3882b --- /dev/null +++ b/contrib/python/oauth2client/py2/ya.make @@ -0,0 +1,68 @@ +# Generated by devtools/yamaker (pypi). + +PY2_LIBRARY() + +VERSION(4.1.3) + +LICENSE(Apache-2.0) + +PEERDIR( + contrib/python/httplib2 + contrib/python/pyasn1 + contrib/python/pyasn1-modules + contrib/python/rsa + contrib/python/six +) + +NO_LINT() + +NO_CHECK_IMPORTS( + oauth2client._openssl_crypt + oauth2client._pycrypto_crypt + oauth2client.contrib.* +) + +PY_SRCS( + TOP_LEVEL + oauth2client/__init__.py + oauth2client/_helpers.py + oauth2client/_openssl_crypt.py + oauth2client/_pkce.py + oauth2client/_pure_python_crypt.py + oauth2client/_pycrypto_crypt.py + oauth2client/client.py + oauth2client/clientsecrets.py + oauth2client/contrib/__init__.py + oauth2client/contrib/_appengine_ndb.py + oauth2client/contrib/_metadata.py + oauth2client/contrib/appengine.py + oauth2client/contrib/devshell.py + oauth2client/contrib/dictionary_storage.py + oauth2client/contrib/django_util/__init__.py + oauth2client/contrib/django_util/apps.py + oauth2client/contrib/django_util/decorators.py + oauth2client/contrib/django_util/models.py + oauth2client/contrib/django_util/signals.py + oauth2client/contrib/django_util/site.py + oauth2client/contrib/django_util/storage.py + oauth2client/contrib/django_util/views.py + oauth2client/contrib/flask_util.py + oauth2client/contrib/gce.py + oauth2client/contrib/keyring_storage.py + oauth2client/contrib/multiprocess_file_storage.py + oauth2client/contrib/sqlalchemy.py + oauth2client/contrib/xsrfutil.py + oauth2client/crypt.py + oauth2client/file.py + oauth2client/service_account.py + oauth2client/tools.py + oauth2client/transport.py +) + +RESOURCE_FILES( + PREFIX contrib/python/oauth2client/py2/ + .dist-info/METADATA + .dist-info/top_level.txt +) + +END() diff --git a/contrib/python/oauth2client/py3/.dist-info/METADATA b/contrib/python/oauth2client/py3/.dist-info/METADATA new file mode 100644 index 0000000000..b4b28000b1 --- /dev/null +++ b/contrib/python/oauth2client/py3/.dist-info/METADATA @@ -0,0 +1,34 @@ +Metadata-Version: 2.1 +Name: oauth2client +Version: 4.1.3 +Summary: OAuth 2.0 client library +Home-page: http://github.com/google/oauth2client/ +Author: Google Inc. +Author-email: jonwayne+oauth2client@google.com +License: Apache 2.0 +Keywords: google oauth 2.0 http client +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Development Status :: 7 - Inactive +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: POSIX +Classifier: Topic :: Internet :: WWW/HTTP +Requires-Dist: httplib2 (>=0.9.1) +Requires-Dist: pyasn1 (>=0.1.7) +Requires-Dist: pyasn1-modules (>=0.0.5) +Requires-Dist: rsa (>=3.1.4) +Requires-Dist: six (>=1.6.1) + +oauth2client is a client library for OAuth 2.0. + +Note: oauth2client is now deprecated. No more features will be added to the + libraries and the core team is turning down support. We recommend you use + `google-auth <https://google-auth.readthedocs.io>`__ and + `oauthlib <http://oauthlib.readthedocs.io/>`__. + + diff --git a/contrib/python/oauth2client/py3/.dist-info/top_level.txt b/contrib/python/oauth2client/py3/.dist-info/top_level.txt new file mode 100644 index 0000000000..c636bd5953 --- /dev/null +++ b/contrib/python/oauth2client/py3/.dist-info/top_level.txt @@ -0,0 +1 @@ +oauth2client diff --git a/contrib/python/oauth2client/py3/LICENSE b/contrib/python/oauth2client/py3/LICENSE new file mode 100644 index 0000000000..c8d76dfc54 --- /dev/null +++ b/contrib/python/oauth2client/py3/LICENSE @@ -0,0 +1,210 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Dependent Modules +================= + +This code has the following dependencies +above and beyond the Python standard library: + +httplib2 - MIT License diff --git a/contrib/python/oauth2client/py3/README.md b/contrib/python/oauth2client/py3/README.md new file mode 100644 index 0000000000..5e7aade714 --- /dev/null +++ b/contrib/python/oauth2client/py3/README.md @@ -0,0 +1,33 @@ +[![Build Status](https://travis-ci.org/google/oauth2client.svg?branch=master)](https://travis-ci.org/google/oauth2client) +[![Coverage Status](https://coveralls.io/repos/google/oauth2client/badge.svg?branch=master&service=github)](https://coveralls.io/github/google/oauth2client?branch=master) +[![Documentation Status](https://readthedocs.org/projects/oauth2client/badge/?version=latest)](https://oauth2client.readthedocs.io/) + +This is a client library for accessing resources protected by OAuth 2.0. + +**Note**: oauth2client is now deprecated. No more features will be added to the +libraries and the core team is turning down support. We recommend you use +[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). For more details on the deprecation, see [oauth2client deprecation](https://google-auth.readthedocs.io/en/latest/oauth2client-deprecation.html). + +Installation +============ + +To install, simply run the following command in your terminal: + +```bash +$ pip install --upgrade oauth2client +``` + +Contributing +============ + +Please see the [CONTRIBUTING page][1] for more information. In particular, we +love pull requests -- but please make sure to sign the contributor license +agreement. + +Supported Python Versions +========================= + +We support Python 2.7 and 3.4+. More information [in the docs][2]. + +[1]: https://github.com/google/oauth2client/blob/master/CONTRIBUTING.md +[2]: https://oauth2client.readthedocs.io/#supported-python-versions diff --git a/contrib/python/oauth2client/py3/oauth2client/__init__.py b/contrib/python/oauth2client/py3/oauth2client/__init__.py new file mode 100644 index 0000000000..92bc191d43 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/__init__.py @@ -0,0 +1,24 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/_helpers.py b/contrib/python/oauth2client/py3/oauth2client/_helpers.py new file mode 100644 index 0000000000..e9123971bc --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/_helpers.py @@ -0,0 +1,341 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/_openssl_crypt.py b/contrib/python/oauth2client/py3/oauth2client/_openssl_crypt.py new file mode 100644 index 0000000000..77fac74354 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/_openssl_crypt.py @@ -0,0 +1,136 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/_pkce.py b/contrib/python/oauth2client/py3/oauth2client/_pkce.py new file mode 100644 index 0000000000..e4952d8c2f --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/_pkce.py @@ -0,0 +1,67 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/_pure_python_crypt.py b/contrib/python/oauth2client/py3/oauth2client/_pure_python_crypt.py new file mode 100644 index 0000000000..2c5d43aae9 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/_pure_python_crypt.py @@ -0,0 +1,184 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/_pycrypto_crypt.py b/contrib/python/oauth2client/py3/oauth2client/_pycrypto_crypt.py new file mode 100644 index 0000000000..fd2ce0cd72 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/_pycrypto_crypt.py @@ -0,0 +1,124 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/client.py b/contrib/python/oauth2client/py3/oauth2client/client.py new file mode 100644 index 0000000000..7618960e44 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/client.py @@ -0,0 +1,2170 @@ +# 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('<an access token>', + '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/contrib/python/oauth2client/py3/oauth2client/clientsecrets.py b/contrib/python/oauth2client/py3/oauth2client/clientsecrets.py new file mode 100644 index 0000000000..1598142e87 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/clientsecrets.py @@ -0,0 +1,173 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/__init__.py b/contrib/python/oauth2client/py3/oauth2client/contrib/__init__.py new file mode 100644 index 0000000000..ecfd06c968 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/__init__.py @@ -0,0 +1,6 @@ +"""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/contrib/python/oauth2client/py3/oauth2client/contrib/_appengine_ndb.py b/contrib/python/oauth2client/py3/oauth2client/contrib/_appengine_ndb.py new file mode 100644 index 0000000000..c863e8f4e7 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/_appengine_ndb.py @@ -0,0 +1,163 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/_metadata.py b/contrib/python/oauth2client/py3/oauth2client/contrib/_metadata.py new file mode 100644 index 0000000000..564cd398da --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/_metadata.py @@ -0,0 +1,118 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/appengine.py b/contrib/python/oauth2client/py3/oauth2client/contrib/appengine.py new file mode 100644 index 0000000000..c1326eeb57 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/appengine.py @@ -0,0 +1,910 @@ +# 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('<html><body>') + request_handler.response.out.write(_safe_html(self._message)) + request_handler.response.out.write('</body></html>') + + 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/contrib/python/oauth2client/py3/oauth2client/contrib/devshell.py b/contrib/python/oauth2client/py3/oauth2client/contrib/devshell.py new file mode 100644 index 0000000000..691765f097 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/devshell.py @@ -0,0 +1,152 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/dictionary_storage.py b/contrib/python/oauth2client/py3/oauth2client/contrib/dictionary_storage.py new file mode 100644 index 0000000000..6ee333fa7c --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/dictionary_storage.py @@ -0,0 +1,65 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/__init__.py b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/__init__.py new file mode 100644 index 0000000000..644a8f9fb7 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/__init__.py @@ -0,0 +1,489 @@ +# 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 <https://console.developers.google.com/project/_/apiui/credential>`. + +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: <a href="{0}">Authorize' + '</a>'.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: <a href="{0}">Authorize' + '</a>'.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/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/apps.py b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/apps.py new file mode 100644 index 0000000000..86676b91a8 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/apps.py @@ -0,0 +1,32 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/decorators.py b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/decorators.py new file mode 100644 index 0000000000..e62e171071 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/decorators.py @@ -0,0 +1,145 @@ +# 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: + <a href="{0}">Authorize</a>'.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/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/models.py b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/models.py new file mode 100644 index 0000000000..37cc697054 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/models.py @@ -0,0 +1,82 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/signals.py b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/signals.py new file mode 100644 index 0000000000..e9356b4dcb --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/signals.py @@ -0,0 +1,28 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/site.py b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/site.py new file mode 100644 index 0000000000..631f79bef4 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/site.py @@ -0,0 +1,26 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/storage.py b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/storage.py new file mode 100644 index 0000000000..5682919bc0 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/storage.py @@ -0,0 +1,81 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/views.py b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/views.py new file mode 100644 index 0000000000..1835208a96 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/django_util/views.py @@ -0,0 +1,193 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/flask_util.py b/contrib/python/oauth2client/py3/oauth2client/contrib/flask_util.py new file mode 100644 index 0000000000..fabd613b46 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/flask_util.py @@ -0,0 +1,557 @@ +# 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 <https://console.developers.google.com/project/_/\ +apiui/credential>`__. + +.. 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 +<https://developers.google.com/identity/protocols/OpenIDConnect?hl=en>`__, 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 <https://developers.google.com\ +/identity/protocols/OAuth2WebServer?hl=en#incrementalAuth>`__. 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/contrib/python/oauth2client/py3/oauth2client/contrib/gce.py b/contrib/python/oauth2client/py3/oauth2client/contrib/gce.py new file mode 100644 index 0000000000..aaab15ffce --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/gce.py @@ -0,0 +1,156 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/keyring_storage.py b/contrib/python/oauth2client/py3/oauth2client/contrib/keyring_storage.py new file mode 100644 index 0000000000..4af944881a --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/keyring_storage.py @@ -0,0 +1,95 @@ +# 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 + <http://pypi.python.org/pypi/keyring/>. 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 <http://pypi.python.org/pypi/keyring/> 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/contrib/python/oauth2client/py3/oauth2client/contrib/multiprocess_file_storage.py b/contrib/python/oauth2client/py3/oauth2client/contrib/multiprocess_file_storage.py new file mode 100644 index 0000000000..e9e8c8cd1d --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/multiprocess_file_storage.py @@ -0,0 +1,355 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/sqlalchemy.py b/contrib/python/oauth2client/py3/oauth2client/contrib/sqlalchemy.py new file mode 100644 index 0000000000..7d9fd4b23f --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/sqlalchemy.py @@ -0,0 +1,173 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/contrib/xsrfutil.py b/contrib/python/oauth2client/py3/oauth2client/contrib/xsrfutil.py new file mode 100644 index 0000000000..7c3ec0353a --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/contrib/xsrfutil.py @@ -0,0 +1,101 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/crypt.py b/contrib/python/oauth2client/py3/oauth2client/crypt.py new file mode 100644 index 0000000000..13260982a6 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/crypt.py @@ -0,0 +1,250 @@ +# -*- 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/contrib/python/oauth2client/py3/oauth2client/file.py b/contrib/python/oauth2client/py3/oauth2client/file.py new file mode 100644 index 0000000000..3551c80d47 --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/file.py @@ -0,0 +1,95 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/service_account.py b/contrib/python/oauth2client/py3/oauth2client/service_account.py new file mode 100644 index 0000000000..540bfaaa1b --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/service_account.py @@ -0,0 +1,685 @@ +# 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/contrib/python/oauth2client/py3/oauth2client/tools.py b/contrib/python/oauth2client/py3/oauth2client/tools.py new file mode 100644 index 0000000000..51669934df --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/tools.py @@ -0,0 +1,256 @@ +# 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 <https://code.google.com/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'<html><head><title>Authentication Status</title></head>') + self.wfile.write( + b'<body><p>The authentication flow has completed.</p>') + self.wfile.write(b'</body></html>') + + 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/contrib/python/oauth2client/py3/oauth2client/transport.py b/contrib/python/oauth2client/py3/oauth2client/transport.py new file mode 100644 index 0000000000..79a61f1c1b --- /dev/null +++ b/contrib/python/oauth2client/py3/oauth2client/transport.py @@ -0,0 +1,285 @@ +# 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()) diff --git a/contrib/python/oauth2client/py3/ya.make b/contrib/python/oauth2client/py3/ya.make new file mode 100644 index 0000000000..644e4afed7 --- /dev/null +++ b/contrib/python/oauth2client/py3/ya.make @@ -0,0 +1,68 @@ +# Generated by devtools/yamaker (pypi). + +PY3_LIBRARY() + +VERSION(4.1.3) + +LICENSE(Apache-2.0) + +PEERDIR( + contrib/python/httplib2 + contrib/python/pyasn1 + contrib/python/pyasn1-modules + contrib/python/rsa + contrib/python/six +) + +NO_LINT() + +NO_CHECK_IMPORTS( + oauth2client._openssl_crypt + oauth2client._pycrypto_crypt + oauth2client.contrib.* +) + +PY_SRCS( + TOP_LEVEL + oauth2client/__init__.py + oauth2client/_helpers.py + oauth2client/_openssl_crypt.py + oauth2client/_pkce.py + oauth2client/_pure_python_crypt.py + oauth2client/_pycrypto_crypt.py + oauth2client/client.py + oauth2client/clientsecrets.py + oauth2client/contrib/__init__.py + oauth2client/contrib/_appengine_ndb.py + oauth2client/contrib/_metadata.py + oauth2client/contrib/appengine.py + oauth2client/contrib/devshell.py + oauth2client/contrib/dictionary_storage.py + oauth2client/contrib/django_util/__init__.py + oauth2client/contrib/django_util/apps.py + oauth2client/contrib/django_util/decorators.py + oauth2client/contrib/django_util/models.py + oauth2client/contrib/django_util/signals.py + oauth2client/contrib/django_util/site.py + oauth2client/contrib/django_util/storage.py + oauth2client/contrib/django_util/views.py + oauth2client/contrib/flask_util.py + oauth2client/contrib/gce.py + oauth2client/contrib/keyring_storage.py + oauth2client/contrib/multiprocess_file_storage.py + oauth2client/contrib/sqlalchemy.py + oauth2client/contrib/xsrfutil.py + oauth2client/crypt.py + oauth2client/file.py + oauth2client/service_account.py + oauth2client/tools.py + oauth2client/transport.py +) + +RESOURCE_FILES( + PREFIX contrib/python/oauth2client/py3/ + .dist-info/METADATA + .dist-info/top_level.txt +) + +END() diff --git a/contrib/python/oauth2client/ya.make b/contrib/python/oauth2client/ya.make new file mode 100644 index 0000000000..8934e6dd84 --- /dev/null +++ b/contrib/python/oauth2client/ya.make @@ -0,0 +1,18 @@ +PY23_LIBRARY() + +LICENSE(Service-Py23-Proxy) + +IF (PYTHON2) + PEERDIR(contrib/python/oauth2client/py2) +ELSE() + PEERDIR(contrib/python/oauth2client/py3) +ENDIF() + +NO_LINT() + +END() + +RECURSE( + py2 + py3 +) |