aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/google-auth/py3
diff options
context:
space:
mode:
authoralexv-smirnov <alex@ydb.tech>2023-12-01 12:02:50 +0300
committeralexv-smirnov <alex@ydb.tech>2023-12-01 13:28:10 +0300
commit0e578a4c44d4abd539d9838347b9ebafaca41dfb (patch)
treea0c1969c37f818c830ebeff9c077eacf30be6ef8 /contrib/python/google-auth/py3
parent84f2d3d4cc985e63217cff149bd2e6d67ae6fe22 (diff)
downloadydb-0e578a4c44d4abd539d9838347b9ebafaca41dfb.tar.gz
Change "ya.make"
Diffstat (limited to 'contrib/python/google-auth/py3')
-rw-r--r--contrib/python/google-auth/py3/.dist-info/METADATA125
-rw-r--r--contrib/python/google-auth/py3/.dist-info/top_level.txt3
-rw-r--r--contrib/python/google-auth/py3/LICENSE201
-rw-r--r--contrib/python/google-auth/py3/README.rst82
-rw-r--r--contrib/python/google-auth/py3/google/auth/__init__.py33
-rw-r--r--contrib/python/google-auth/py3/google/auth/_cloud_sdk.py153
-rw-r--r--contrib/python/google-auth/py3/google/auth/_credentials_async.py171
-rw-r--r--contrib/python/google-auth/py3/google/auth/_default.py691
-rw-r--r--contrib/python/google-auth/py3/google/auth/_default_async.py282
-rw-r--r--contrib/python/google-auth/py3/google/auth/_exponential_backoff.py109
-rw-r--r--contrib/python/google-auth/py3/google/auth/_helpers.py245
-rw-r--r--contrib/python/google-auth/py3/google/auth/_jwt_async.py164
-rw-r--r--contrib/python/google-auth/py3/google/auth/_oauth2client.py167
-rw-r--r--contrib/python/google-auth/py3/google/auth/_service_account_info.py80
-rw-r--r--contrib/python/google-auth/py3/google/auth/api_key.py76
-rw-r--r--contrib/python/google-auth/py3/google/auth/app_engine.py180
-rw-r--r--contrib/python/google-auth/py3/google/auth/aws.py777
-rw-r--r--contrib/python/google-auth/py3/google/auth/compute_engine/__init__.py21
-rw-r--r--contrib/python/google-auth/py3/google/auth/compute_engine/_metadata.py322
-rw-r--r--contrib/python/google-auth/py3/google/auth/compute_engine/credentials.py445
-rw-r--r--contrib/python/google-auth/py3/google/auth/credentials.py410
-rw-r--r--contrib/python/google-auth/py3/google/auth/crypt/__init__.py98
-rw-r--r--contrib/python/google-auth/py3/google/auth/crypt/_cryptography_rsa.py136
-rw-r--r--contrib/python/google-auth/py3/google/auth/crypt/_helpers.py0
-rw-r--r--contrib/python/google-auth/py3/google/auth/crypt/_python_rsa.py175
-rw-r--r--contrib/python/google-auth/py3/google/auth/crypt/base.py127
-rw-r--r--contrib/python/google-auth/py3/google/auth/crypt/es256.py160
-rw-r--r--contrib/python/google-auth/py3/google/auth/crypt/rsa.py30
-rw-r--r--contrib/python/google-auth/py3/google/auth/downscoped.py504
-rw-r--r--contrib/python/google-auth/py3/google/auth/environment_vars.py84
-rw-r--r--contrib/python/google-auth/py3/google/auth/exceptions.py100
-rw-r--r--contrib/python/google-auth/py3/google/auth/external_account.py523
-rw-r--r--contrib/python/google-auth/py3/google/auth/external_account_authorized_user.py350
-rw-r--r--contrib/python/google-auth/py3/google/auth/iam.py99
-rw-r--r--contrib/python/google-auth/py3/google/auth/identity_pool.py261
-rw-r--r--contrib/python/google-auth/py3/google/auth/impersonated_credentials.py462
-rw-r--r--contrib/python/google-auth/py3/google/auth/jwt.py878
-rw-r--r--contrib/python/google-auth/py3/google/auth/metrics.py154
-rw-r--r--contrib/python/google-auth/py3/google/auth/pluggable.py429
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/__init__.py103
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py390
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/_custom_tls_signer.py234
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/_http_client.py113
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/_mtls_helper.py252
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/grpc.py343
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/mtls.py103
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/requests.py604
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/urllib3.py437
-rw-r--r--contrib/python/google-auth/py3/google/auth/version.py15
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/__init__.py15
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/_client.py507
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/_client_async.py292
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/_credentials_async.py112
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/_id_token_async.py285
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/_reauth_async.py328
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/_service_account_async.py132
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/challenges.py203
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/credentials.py545
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/gdch_credentials.py251
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/id_token.py339
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/reauth.py368
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/service_account.py819
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/sts.py176
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/utils.py168
-rw-r--r--contrib/python/google-auth/py3/tests/__init__.py0
-rw-r--r--contrib/python/google-auth/py3/tests/compute_engine/__init__.py0
-rw-r--r--contrib/python/google-auth/py3/tests/compute_engine/data/smbios_product_name1
-rw-r--r--contrib/python/google-auth/py3/tests/compute_engine/data/smbios_product_name_non_google1
-rw-r--r--contrib/python/google-auth/py3/tests/compute_engine/test__metadata.py450
-rw-r--r--contrib/python/google-auth/py3/tests/compute_engine/test_credentials.py875
-rw-r--r--contrib/python/google-auth/py3/tests/conftest.py45
-rw-r--r--contrib/python/google-auth/py3/tests/crypt/__init__.py0
-rw-r--r--contrib/python/google-auth/py3/tests/crypt/test__cryptography_rsa.py162
-rw-r--r--contrib/python/google-auth/py3/tests/crypt/test__python_rsa.py194
-rw-r--r--contrib/python/google-auth/py3/tests/crypt/test_crypt.py59
-rw-r--r--contrib/python/google-auth/py3/tests/crypt/test_es256.py144
-rw-r--r--contrib/python/google-auth/py3/tests/data/authorized_user.json6
-rw-r--r--contrib/python/google-auth/py3/tests/data/authorized_user_cloud_sdk.json6
-rw-r--r--contrib/python/google-auth/py3/tests/data/authorized_user_cloud_sdk_with_quota_project_id.json7
-rw-r--r--contrib/python/google-auth/py3/tests/data/authorized_user_with_rapt_token.json8
-rw-r--r--contrib/python/google-auth/py3/tests/data/client_secrets.json14
-rw-r--r--contrib/python/google-auth/py3/tests/data/context_aware_metadata.json6
-rw-r--r--contrib/python/google-auth/py3/tests/data/enterprise_cert_invalid.json3
-rw-r--r--contrib/python/google-auth/py3/tests/data/enterprise_cert_valid.json6
-rw-r--r--contrib/python/google-auth/py3/tests/data/es256_privatekey.pem5
-rw-r--r--contrib/python/google-auth/py3/tests/data/es256_public_cert.pem8
-rw-r--r--contrib/python/google-auth/py3/tests/data/es256_publickey.pem4
-rw-r--r--contrib/python/google-auth/py3/tests/data/es256_service_account.json10
-rw-r--r--contrib/python/google-auth/py3/tests/data/external_account_authorized_user.json9
-rw-r--r--contrib/python/google-auth/py3/tests/data/external_subject_token.json3
-rw-r--r--contrib/python/google-auth/py3/tests/data/external_subject_token.txt1
-rw-r--r--contrib/python/google-auth/py3/tests/data/gdch_service_account.json11
-rw-r--r--contrib/python/google-auth/py3/tests/data/impersonated_service_account_authorized_user_source.json13
-rw-r--r--contrib/python/google-auth/py3/tests/data/impersonated_service_account_service_account_source.json17
-rw-r--r--contrib/python/google-auth/py3/tests/data/impersonated_service_account_with_quota_project.json14
-rw-r--r--contrib/python/google-auth/py3/tests/data/old_oauth_credentials_py3.picklebin0 -> 283 bytes
-rw-r--r--contrib/python/google-auth/py3/tests/data/other_cert.pem33
-rw-r--r--contrib/python/google-auth/py3/tests/data/pem_from_pkcs12.pem32
-rw-r--r--contrib/python/google-auth/py3/tests/data/privatekey.p12bin0 -> 2452 bytes
-rw-r--r--contrib/python/google-auth/py3/tests/data/privatekey.pem27
-rw-r--r--contrib/python/google-auth/py3/tests/data/privatekey.pub8
-rw-r--r--contrib/python/google-auth/py3/tests/data/public_cert.pem19
-rw-r--r--contrib/python/google-auth/py3/tests/data/service_account.json10
-rw-r--r--contrib/python/google-auth/py3/tests/data/service_account_non_gdu.json15
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/__init__.py0
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test__client.py622
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_challenges.py198
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_credentials.py997
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_gdch_credentials.py175
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_id_token.py312
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_reauth.py388
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_service_account.py789
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_sts.py480
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_utils.py264
-rw-r--r--contrib/python/google-auth/py3/tests/test__cloud_sdk.py182
-rw-r--r--contrib/python/google-auth/py3/tests/test__default.py1352
-rw-r--r--contrib/python/google-auth/py3/tests/test__exponential_backoff.py41
-rw-r--r--contrib/python/google-auth/py3/tests/test__helpers.py170
-rw-r--r--contrib/python/google-auth/py3/tests/test__oauth2client.py178
-rw-r--r--contrib/python/google-auth/py3/tests/test__service_account_info.py83
-rw-r--r--contrib/python/google-auth/py3/tests/test_api_key.py45
-rw-r--r--contrib/python/google-auth/py3/tests/test_app_engine.py217
-rw-r--r--contrib/python/google-auth/py3/tests/test_aws.py2125
-rw-r--r--contrib/python/google-auth/py3/tests/test_credentials.py224
-rw-r--r--contrib/python/google-auth/py3/tests/test_downscoped.py696
-rw-r--r--contrib/python/google-auth/py3/tests/test_exceptions.py55
-rw-r--r--contrib/python/google-auth/py3/tests/test_external_account.py1900
-rw-r--r--contrib/python/google-auth/py3/tests/test_external_account_authorized_user.py512
-rw-r--r--contrib/python/google-auth/py3/tests/test_iam.py102
-rw-r--r--contrib/python/google-auth/py3/tests/test_identity_pool.py1302
-rw-r--r--contrib/python/google-auth/py3/tests/test_impersonated_credentials.py660
-rw-r--r--contrib/python/google-auth/py3/tests/test_jwt.py671
-rw-r--r--contrib/python/google-auth/py3/tests/test_metrics.py96
-rw-r--r--contrib/python/google-auth/py3/tests/test_packaging.py30
-rw-r--r--contrib/python/google-auth/py3/tests/test_pluggable.py1250
-rw-r--r--contrib/python/google-auth/py3/tests/transport/__init__.py0
-rw-r--r--contrib/python/google-auth/py3/tests/transport/compliance.py108
-rw-r--r--contrib/python/google-auth/py3/tests/transport/test__custom_tls_signer.py234
-rw-r--r--contrib/python/google-auth/py3/tests/transport/test__http_client.py31
-rw-r--r--contrib/python/google-auth/py3/tests/transport/test__mtls_helper.py441
-rw-r--r--contrib/python/google-auth/py3/tests/transport/test_grpc.py503
-rw-r--r--contrib/python/google-auth/py3/tests/transport/test_mtls.py83
-rw-r--r--contrib/python/google-auth/py3/tests/transport/test_requests.py575
-rw-r--r--contrib/python/google-auth/py3/tests/transport/test_urllib3.py322
-rw-r--r--contrib/python/google-auth/py3/tests/ya.make77
-rw-r--r--contrib/python/google-auth/py3/ya.make100
146 files changed, 37257 insertions, 0 deletions
diff --git a/contrib/python/google-auth/py3/.dist-info/METADATA b/contrib/python/google-auth/py3/.dist-info/METADATA
new file mode 100644
index 0000000000..23841a2ee7
--- /dev/null
+++ b/contrib/python/google-auth/py3/.dist-info/METADATA
@@ -0,0 +1,125 @@
+Metadata-Version: 2.1
+Name: google-auth
+Version: 2.23.0
+Summary: Google Authentication Library
+Home-page: https://github.com/googleapis/google-auth-library-python
+Author: Google Cloud Platform
+Author-email: googleapis-packages@google.com
+License: Apache 2.0
+Keywords: google auth oauth client
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: POSIX
+Classifier: Operating System :: Microsoft :: Windows
+Classifier: Operating System :: MacOS :: MacOS X
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Internet :: WWW/HTTP
+Requires-Python: >=3.7
+License-File: LICENSE
+Requires-Dist: cachetools (<6.0,>=2.0.0)
+Requires-Dist: pyasn1-modules (>=0.2.1)
+Requires-Dist: rsa (<5,>=3.1.4)
+Requires-Dist: urllib3 (<2.0)
+Provides-Extra: aiohttp
+Requires-Dist: aiohttp (<4.0.0.dev0,>=3.6.2) ; extra == 'aiohttp'
+Requires-Dist: requests (<3.0.0.dev0,>=2.20.0) ; extra == 'aiohttp'
+Provides-Extra: enterprise_cert
+Requires-Dist: cryptography (==36.0.2) ; extra == 'enterprise_cert'
+Requires-Dist: pyopenssl (==22.0.0) ; extra == 'enterprise_cert'
+Provides-Extra: pyopenssl
+Requires-Dist: pyopenssl (>=20.0.0) ; extra == 'pyopenssl'
+Requires-Dist: cryptography (>=38.0.3) ; extra == 'pyopenssl'
+Provides-Extra: reauth
+Requires-Dist: pyu2f (>=0.1.5) ; extra == 'reauth'
+Provides-Extra: requests
+Requires-Dist: requests (<3.0.0.dev0,>=2.20.0) ; extra == 'requests'
+
+Google Auth Python Library
+==========================
+
+|pypi|
+
+This library simplifies using Google's various server-to-server authentication
+mechanisms to access Google APIs.
+
+.. |pypi| image:: https://img.shields.io/pypi/v/google-auth.svg
+ :target: https://pypi.python.org/pypi/google-auth
+
+Installing
+----------
+
+You can install using `pip`_::
+
+ $ pip install google-auth
+
+.. _pip: https://pip.pypa.io/en/stable/
+
+For more information on setting up your Python development environment, please refer to `Python Development Environment Setup Guide`_ for Google Cloud Platform.
+
+.. _`Python Development Environment Setup Guide`: https://cloud.google.com/python/setup
+
+Extras
+------
+
+google-auth has few extras that you can install. For example::
+
+ $ pip install google-auth[pyopenssl]
+
+Note that the extras pyopenssl and enterprise_cert should not be used together because they use conflicting versions of `cryptography`_.
+
+.. _`cryptography`: https://cryptography.io/en/latest/
+
+Supported Python Versions
+^^^^^^^^^^^^^^^^^^^^^^^^^
+Python >= 3.7
+
+Unsupported Python Versions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+- Python == 2.7: The last version of this library with support for Python 2.7
+ was `google.auth == 1.34.0`.
+
+- Python 3.5: The last version of this library with support for Python 3.5
+ was `google.auth == 1.23.0`.
+
+- Python 3.6: The last version of this library with support for Python 3.6
+ was `google.auth == 2.22.0`.
+
+Documentation
+-------------
+
+Google Auth Python Library has usage and reference documentation at https://googleapis.dev/python/google-auth/latest/index.html.
+
+Current Maintainers
+-------------------
+- googleapis-auth@google.com
+
+Authors
+-------
+
+- `@theacodes <https://github.com/theacodes>`_ (Thea Flowers)
+- `@dhermes <https://github.com/dhermes>`_ (Danny Hermes)
+- `@lukesneeringer <https://github.com/lukesneeringer>`_ (Luke Sneeringer)
+- `@busunkim96 <https://github.com/busunkim96>`_ (Bu Sun Kim)
+
+Contributing
+------------
+
+Contributions to this library are always welcome and highly encouraged.
+
+See `CONTRIBUTING.rst`_ for more information on how to get started.
+
+.. _CONTRIBUTING.rst: https://github.com/googleapis/google-auth-library-python/blob/main/CONTRIBUTING.rst
+
+License
+-------
+
+Apache 2.0 - See `the LICENSE`_ for more information.
+
+.. _the LICENSE: https://github.com/googleapis/google-auth-library-python/blob/main/LICENSE
diff --git a/contrib/python/google-auth/py3/.dist-info/top_level.txt b/contrib/python/google-auth/py3/.dist-info/top_level.txt
new file mode 100644
index 0000000000..64f26a32e6
--- /dev/null
+++ b/contrib/python/google-auth/py3/.dist-info/top_level.txt
@@ -0,0 +1,3 @@
+google
+scripts
+testing
diff --git a/contrib/python/google-auth/py3/LICENSE b/contrib/python/google-auth/py3/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/contrib/python/google-auth/py3/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/contrib/python/google-auth/py3/README.rst b/contrib/python/google-auth/py3/README.rst
new file mode 100644
index 0000000000..cdd19bed50
--- /dev/null
+++ b/contrib/python/google-auth/py3/README.rst
@@ -0,0 +1,82 @@
+Google Auth Python Library
+==========================
+
+|pypi|
+
+This library simplifies using Google's various server-to-server authentication
+mechanisms to access Google APIs.
+
+.. |pypi| image:: https://img.shields.io/pypi/v/google-auth.svg
+ :target: https://pypi.python.org/pypi/google-auth
+
+Installing
+----------
+
+You can install using `pip`_::
+
+ $ pip install google-auth
+
+.. _pip: https://pip.pypa.io/en/stable/
+
+For more information on setting up your Python development environment, please refer to `Python Development Environment Setup Guide`_ for Google Cloud Platform.
+
+.. _`Python Development Environment Setup Guide`: https://cloud.google.com/python/setup
+
+Extras
+------
+
+google-auth has few extras that you can install. For example::
+
+ $ pip install google-auth[pyopenssl]
+
+Note that the extras pyopenssl and enterprise_cert should not be used together because they use conflicting versions of `cryptography`_.
+
+.. _`cryptography`: https://cryptography.io/en/latest/
+
+Supported Python Versions
+^^^^^^^^^^^^^^^^^^^^^^^^^
+Python >= 3.7
+
+Unsupported Python Versions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+- Python == 2.7: The last version of this library with support for Python 2.7
+ was `google.auth == 1.34.0`.
+
+- Python 3.5: The last version of this library with support for Python 3.5
+ was `google.auth == 1.23.0`.
+
+- Python 3.6: The last version of this library with support for Python 3.6
+ was `google.auth == 2.22.0`.
+
+Documentation
+-------------
+
+Google Auth Python Library has usage and reference documentation at https://googleapis.dev/python/google-auth/latest/index.html.
+
+Current Maintainers
+-------------------
+- googleapis-auth@google.com
+
+Authors
+-------
+
+- `@theacodes <https://github.com/theacodes>`_ (Thea Flowers)
+- `@dhermes <https://github.com/dhermes>`_ (Danny Hermes)
+- `@lukesneeringer <https://github.com/lukesneeringer>`_ (Luke Sneeringer)
+- `@busunkim96 <https://github.com/busunkim96>`_ (Bu Sun Kim)
+
+Contributing
+------------
+
+Contributions to this library are always welcome and highly encouraged.
+
+See `CONTRIBUTING.rst`_ for more information on how to get started.
+
+.. _CONTRIBUTING.rst: https://github.com/googleapis/google-auth-library-python/blob/main/CONTRIBUTING.rst
+
+License
+-------
+
+Apache 2.0 - See `the LICENSE`_ for more information.
+
+.. _the LICENSE: https://github.com/googleapis/google-auth-library-python/blob/main/LICENSE
diff --git a/contrib/python/google-auth/py3/google/auth/__init__.py b/contrib/python/google-auth/py3/google/auth/__init__.py
new file mode 100644
index 0000000000..2875772b37
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/__init__.py
@@ -0,0 +1,33 @@
+# Copyright 2016 Google LLC
+#
+# 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 Auth Library for Python."""
+
+import logging
+
+from google.auth import version as google_auth_version
+from google.auth._default import (
+ default,
+ load_credentials_from_dict,
+ load_credentials_from_file,
+)
+
+
+__version__ = google_auth_version.__version__
+
+
+__all__ = ["default", "load_credentials_from_file", "load_credentials_from_dict"]
+
+# Set default logging handler to avoid "No handler found" warnings.
+logging.getLogger(__name__).addHandler(logging.NullHandler())
diff --git a/contrib/python/google-auth/py3/google/auth/_cloud_sdk.py b/contrib/python/google-auth/py3/google/auth/_cloud_sdk.py
new file mode 100644
index 0000000000..a94411949b
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/_cloud_sdk.py
@@ -0,0 +1,153 @@
+# Copyright 2015 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helpers for reading the Google Cloud SDK's configuration."""
+
+import os
+import subprocess
+
+from google.auth import _helpers
+from google.auth import environment_vars
+from google.auth import exceptions
+
+
+# The ~/.config subdirectory containing gcloud credentials.
+_CONFIG_DIRECTORY = "gcloud"
+# Windows systems store config at %APPDATA%\gcloud
+_WINDOWS_CONFIG_ROOT_ENV_VAR = "APPDATA"
+# The name of the file in the Cloud SDK config that contains default
+# credentials.
+_CREDENTIALS_FILENAME = "application_default_credentials.json"
+# The name of the Cloud SDK shell script
+_CLOUD_SDK_POSIX_COMMAND = "gcloud"
+_CLOUD_SDK_WINDOWS_COMMAND = "gcloud.cmd"
+# The command to get the Cloud SDK configuration
+_CLOUD_SDK_CONFIG_GET_PROJECT_COMMAND = ("config", "get", "project")
+# The command to get google user access token
+_CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND = ("auth", "print-access-token")
+# Cloud SDK's application-default client ID
+CLOUD_SDK_CLIENT_ID = (
+ "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com"
+)
+
+
+def get_config_path():
+ """Returns the absolute path the the Cloud SDK's configuration directory.
+
+ Returns:
+ str: The Cloud SDK config path.
+ """
+ # If the path is explicitly set, return that.
+ try:
+ return os.environ[environment_vars.CLOUD_SDK_CONFIG_DIR]
+ except KeyError:
+ pass
+
+ # Non-windows systems store this at ~/.config/gcloud
+ if os.name != "nt":
+ return os.path.join(os.path.expanduser("~"), ".config", _CONFIG_DIRECTORY)
+ # Windows systems store config at %APPDATA%\gcloud
+ else:
+ try:
+ return os.path.join(
+ os.environ[_WINDOWS_CONFIG_ROOT_ENV_VAR], _CONFIG_DIRECTORY
+ )
+ except KeyError:
+ # This should never happen unless someone is really
+ # messing with things, but we'll cover the case anyway.
+ drive = os.environ.get("SystemDrive", "C:")
+ return os.path.join(drive, "\\", _CONFIG_DIRECTORY)
+
+
+def get_application_default_credentials_path():
+ """Gets the path to the application default credentials file.
+
+ The path may or may not exist.
+
+ Returns:
+ str: The full path to application default credentials.
+ """
+ config_path = get_config_path()
+ return os.path.join(config_path, _CREDENTIALS_FILENAME)
+
+
+def _run_subprocess_ignore_stderr(command):
+ """ Return subprocess.check_output with the given command and ignores stderr."""
+ with open(os.devnull, "w") as devnull:
+ output = subprocess.check_output(command, stderr=devnull)
+ return output
+
+
+def get_project_id():
+ """Gets the project ID from the Cloud SDK.
+
+ Returns:
+ Optional[str]: The project ID.
+ """
+ if os.name == "nt":
+ command = _CLOUD_SDK_WINDOWS_COMMAND
+ else:
+ command = _CLOUD_SDK_POSIX_COMMAND
+
+ try:
+ # Ignore the stderr coming from gcloud, so it won't be mixed into the output.
+ # https://github.com/googleapis/google-auth-library-python/issues/673
+ project = _run_subprocess_ignore_stderr(
+ (command,) + _CLOUD_SDK_CONFIG_GET_PROJECT_COMMAND
+ )
+
+ # Turn bytes into a string and remove "\n"
+ project = _helpers.from_bytes(project).strip()
+ return project if project else None
+ except (subprocess.CalledProcessError, OSError, IOError):
+ return None
+
+
+def get_auth_access_token(account=None):
+ """Load user access token with the ``gcloud auth print-access-token`` command.
+
+ Args:
+ account (Optional[str]): Account to get the access token for. If not
+ specified, the current active account will be used.
+
+ Returns:
+ str: The user access token.
+
+ Raises:
+ google.auth.exceptions.UserAccessTokenError: if failed to get access
+ token from gcloud.
+ """
+ if os.name == "nt":
+ command = _CLOUD_SDK_WINDOWS_COMMAND
+ else:
+ command = _CLOUD_SDK_POSIX_COMMAND
+
+ try:
+ if account:
+ command = (
+ (command,)
+ + _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
+ + ("--account=" + account,)
+ )
+ else:
+ command = (command,) + _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
+
+ access_token = subprocess.check_output(command, stderr=subprocess.STDOUT)
+ # remove the trailing "\n"
+ return access_token.decode("utf-8").strip()
+ except (subprocess.CalledProcessError, OSError, IOError) as caught_exc:
+ new_exc = exceptions.UserAccessTokenError(
+ "Failed to obtain access token", caught_exc
+ )
+ raise new_exc from caught_exc
diff --git a/contrib/python/google-auth/py3/google/auth/_credentials_async.py b/contrib/python/google-auth/py3/google/auth/_credentials_async.py
new file mode 100644
index 0000000000..760758d851
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/_credentials_async.py
@@ -0,0 +1,171 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+
+
+"""Interfaces for credentials."""
+
+import abc
+import inspect
+
+from google.auth import credentials
+
+
+class Credentials(credentials.Credentials, metaclass=abc.ABCMeta):
+ """Async inherited credentials class from google.auth.credentials.
+ The added functionality is the before_request call which requires
+ async/await syntax.
+ All credentials have a :attr:`token` that is used for authentication and
+ may also optionally set an :attr:`expiry` to indicate when the token will
+ no longer be valid.
+
+ Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
+ Credentials can do this automatically before the first HTTP request in
+ :meth:`before_request`.
+
+ Although the token and expiration will change as the credentials are
+ :meth:`refreshed <refresh>` and used, credentials should be considered
+ immutable. Various credentials will accept configuration such as private
+ keys, scopes, and other options. These options are not changeable after
+ construction. Some classes will provide mechanisms to copy the credentials
+ with modifications such as :meth:`ScopedCredentials.with_scopes`.
+ """
+
+ async def before_request(self, request, method, url, headers):
+ """Performs credential-specific before request logic.
+
+ Refreshes the credentials if necessary, then calls :meth:`apply` to
+ apply the token to the authentication header.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ method (str): The request's HTTP method or the RPC method being
+ invoked.
+ url (str): The request's URI or the RPC service's URI.
+ headers (Mapping): The request's headers.
+ """
+ # pylint: disable=unused-argument
+ # (Subclasses may use these arguments to ascertain information about
+ # the http request.)
+
+ if not self.valid:
+ if inspect.iscoroutinefunction(self.refresh):
+ await self.refresh(request)
+ else:
+ self.refresh(request)
+ self.apply(headers)
+
+
+class CredentialsWithQuotaProject(credentials.CredentialsWithQuotaProject):
+ """Abstract base for credentials supporting ``with_quota_project`` factory"""
+
+
+class AnonymousCredentials(credentials.AnonymousCredentials, Credentials):
+ """Credentials that do not provide any authentication information.
+
+ These are useful in the case of services that support anonymous access or
+ local service emulators that do not use credentials. This class inherits
+ from the sync anonymous credentials file, but is kept if async credentials
+ is initialized and we would like anonymous credentials.
+ """
+
+
+class ReadOnlyScoped(credentials.ReadOnlyScoped, metaclass=abc.ABCMeta):
+ """Interface for credentials whose scopes can be queried.
+
+ OAuth 2.0-based credentials allow limiting access using scopes as described
+ in `RFC6749 Section 3.3`_.
+ If a credential class implements this interface then the credentials either
+ use scopes in their implementation.
+
+ Some credentials require scopes in order to obtain a token. You can check
+ if scoping is necessary with :attr:`requires_scopes`::
+
+ if credentials.requires_scopes:
+ # Scoping is required.
+ credentials = _credentials_async.with_scopes(scopes=['one', 'two'])
+
+ Credentials that require scopes must either be constructed with scopes::
+
+ credentials = SomeScopedCredentials(scopes=['one', 'two'])
+
+ Or must copy an existing instance using :meth:`with_scopes`::
+
+ scoped_credentials = _credentials_async.with_scopes(scopes=['one', 'two'])
+
+ Some credentials have scopes but do not allow or require scopes to be set,
+ these credentials can be used as-is.
+
+ .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
+ """
+
+
+class Scoped(credentials.Scoped):
+ """Interface for credentials whose scopes can be replaced while copying.
+
+ OAuth 2.0-based credentials allow limiting access using scopes as described
+ in `RFC6749 Section 3.3`_.
+ If a credential class implements this interface then the credentials either
+ use scopes in their implementation.
+
+ Some credentials require scopes in order to obtain a token. You can check
+ if scoping is necessary with :attr:`requires_scopes`::
+
+ if credentials.requires_scopes:
+ # Scoping is required.
+ credentials = _credentials_async.create_scoped(['one', 'two'])
+
+ Credentials that require scopes must either be constructed with scopes::
+
+ credentials = SomeScopedCredentials(scopes=['one', 'two'])
+
+ Or must copy an existing instance using :meth:`with_scopes`::
+
+ scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
+
+ Some credentials have scopes but do not allow or require scopes to be set,
+ these credentials can be used as-is.
+
+ .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
+ """
+
+
+def with_scopes_if_required(credentials, scopes):
+ """Creates a copy of the credentials with scopes if scoping is required.
+
+ This helper function is useful when you do not know (or care to know) the
+ specific type of credentials you are using (such as when you use
+ :func:`google.auth.default`). This function will call
+ :meth:`Scoped.with_scopes` if the credentials are scoped credentials and if
+ the credentials require scoping. Otherwise, it will return the credentials
+ as-is.
+
+ Args:
+ credentials (google.auth.credentials.Credentials): The credentials to
+ scope if necessary.
+ scopes (Sequence[str]): The list of scopes to use.
+
+ Returns:
+ google.auth._credentials_async.Credentials: Either a new set of scoped
+ credentials, or the passed in credentials instance if no scoping
+ was required.
+ """
+ if isinstance(credentials, Scoped) and credentials.requires_scopes:
+ return credentials.with_scopes(scopes)
+ else:
+ return credentials
+
+
+class Signing(credentials.Signing, metaclass=abc.ABCMeta):
+ """Interface for credentials that can cryptographically sign messages."""
diff --git a/contrib/python/google-auth/py3/google/auth/_default.py b/contrib/python/google-auth/py3/google/auth/_default.py
new file mode 100644
index 0000000000..63009dfb86
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/_default.py
@@ -0,0 +1,691 @@
+# Copyright 2015 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Application default credentials.
+
+Implements application default credentials and project ID detection.
+"""
+
+import io
+import json
+import logging
+import os
+import warnings
+
+from google.auth import environment_vars
+from google.auth import exceptions
+import google.auth.transport._http_client
+
+_LOGGER = logging.getLogger(__name__)
+
+# Valid types accepted for file-based credentials.
+_AUTHORIZED_USER_TYPE = "authorized_user"
+_SERVICE_ACCOUNT_TYPE = "service_account"
+_EXTERNAL_ACCOUNT_TYPE = "external_account"
+_EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE = "external_account_authorized_user"
+_IMPERSONATED_SERVICE_ACCOUNT_TYPE = "impersonated_service_account"
+_GDCH_SERVICE_ACCOUNT_TYPE = "gdch_service_account"
+_VALID_TYPES = (
+ _AUTHORIZED_USER_TYPE,
+ _SERVICE_ACCOUNT_TYPE,
+ _EXTERNAL_ACCOUNT_TYPE,
+ _EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE,
+ _IMPERSONATED_SERVICE_ACCOUNT_TYPE,
+ _GDCH_SERVICE_ACCOUNT_TYPE,
+)
+
+# Help message when no credentials can be found.
+_CLOUD_SDK_MISSING_CREDENTIALS = """\
+Your default credentials were not found. To set up Application Default Credentials, \
+see https://cloud.google.com/docs/authentication/external/set-up-adc for more information.\
+"""
+
+# Warning when using Cloud SDK user credentials
+_CLOUD_SDK_CREDENTIALS_WARNING = """\
+Your application has authenticated using end user credentials from Google \
+Cloud SDK without a quota project. You might receive a "quota exceeded" \
+or "API not enabled" error. See the following page for troubleshooting: \
+https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \
+"""
+
+# The subject token type used for AWS external_account credentials.
+_AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request"
+
+
+def _warn_about_problematic_credentials(credentials):
+ """Determines if the credentials are problematic.
+
+ Credentials from the Cloud SDK that are associated with Cloud SDK's project
+ are problematic because they may not have APIs enabled and have limited
+ quota. If this is the case, warn about it.
+ """
+ from google.auth import _cloud_sdk
+
+ if credentials.client_id == _cloud_sdk.CLOUD_SDK_CLIENT_ID:
+ warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)
+
+
+def load_credentials_from_file(
+ filename, scopes=None, default_scopes=None, quota_project_id=None, request=None
+):
+ """Loads Google credentials from a file.
+
+ The credentials file must be a service account key, stored authorized
+ user credentials, external account credentials, or impersonated service
+ account credentials.
+
+ Args:
+ filename (str): The full path to the credentials file.
+ scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
+ specified, the credentials will automatically be scoped if
+ necessary
+ default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+ Google client library. Use 'scopes' for user-defined scopes.
+ quota_project_id (Optional[str]): The project ID used for
+ quota and billing.
+ request (Optional[google.auth.transport.Request]): An object used to make
+ HTTP requests. This is used to determine the associated project ID
+ for a workload identity pool resource (external account credentials).
+ If not specified, then it will use a
+ google.auth.transport.requests.Request client to make requests.
+
+ Returns:
+ Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
+ credentials and the project ID. Authorized user credentials do not
+ have the project ID information. External account credentials project
+ IDs may not always be determined.
+
+ Raises:
+ google.auth.exceptions.DefaultCredentialsError: if the file is in the
+ wrong format or is missing.
+ """
+ if not os.path.exists(filename):
+ raise exceptions.DefaultCredentialsError(
+ "File {} was not found.".format(filename)
+ )
+
+ with io.open(filename, "r") as file_obj:
+ try:
+ info = json.load(file_obj)
+ except ValueError as caught_exc:
+ new_exc = exceptions.DefaultCredentialsError(
+ "File {} is not a valid json file.".format(filename), caught_exc
+ )
+ raise new_exc from caught_exc
+ return _load_credentials_from_info(
+ filename, info, scopes, default_scopes, quota_project_id, request
+ )
+
+
+def load_credentials_from_dict(
+ info, scopes=None, default_scopes=None, quota_project_id=None, request=None
+):
+ """Loads Google credentials from a dict.
+
+ The credentials file must be a service account key, stored authorized
+ user credentials, external account credentials, or impersonated service
+ account credentials.
+
+ Args:
+ info (Dict[str, Any]): A dict object containing the credentials
+ scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
+ specified, the credentials will automatically be scoped if
+ necessary
+ default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+ Google client library. Use 'scopes' for user-defined scopes.
+ quota_project_id (Optional[str]): The project ID used for
+ quota and billing.
+ request (Optional[google.auth.transport.Request]): An object used to make
+ HTTP requests. This is used to determine the associated project ID
+ for a workload identity pool resource (external account credentials).
+ If not specified, then it will use a
+ google.auth.transport.requests.Request client to make requests.
+
+ Returns:
+ Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
+ credentials and the project ID. Authorized user credentials do not
+ have the project ID information. External account credentials project
+ IDs may not always be determined.
+
+ Raises:
+ google.auth.exceptions.DefaultCredentialsError: if the file is in the
+ wrong format or is missing.
+ """
+ if not isinstance(info, dict):
+ raise exceptions.DefaultCredentialsError(
+ "info object was of type {} but dict type was expected.".format(type(info))
+ )
+
+ return _load_credentials_from_info(
+ "dict object", info, scopes, default_scopes, quota_project_id, request
+ )
+
+
+def _load_credentials_from_info(
+ filename, info, scopes, default_scopes, quota_project_id, request
+):
+ from google.auth.credentials import CredentialsWithQuotaProject
+
+ credential_type = info.get("type")
+
+ if credential_type == _AUTHORIZED_USER_TYPE:
+ credentials, project_id = _get_authorized_user_credentials(
+ filename, info, scopes
+ )
+
+ elif credential_type == _SERVICE_ACCOUNT_TYPE:
+ credentials, project_id = _get_service_account_credentials(
+ filename, info, scopes, default_scopes
+ )
+
+ elif credential_type == _EXTERNAL_ACCOUNT_TYPE:
+ credentials, project_id = _get_external_account_credentials(
+ info,
+ filename,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ request=request,
+ )
+
+ elif credential_type == _EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE:
+ credentials, project_id = _get_external_account_authorized_user_credentials(
+ filename, info, request
+ )
+
+ elif credential_type == _IMPERSONATED_SERVICE_ACCOUNT_TYPE:
+ credentials, project_id = _get_impersonated_service_account_credentials(
+ filename, info, scopes
+ )
+ elif credential_type == _GDCH_SERVICE_ACCOUNT_TYPE:
+ credentials, project_id = _get_gdch_service_account_credentials(filename, info)
+ else:
+ raise exceptions.DefaultCredentialsError(
+ "The file {file} does not have a valid type. "
+ "Type is {type}, expected one of {valid_types}.".format(
+ file=filename, type=credential_type, valid_types=_VALID_TYPES
+ )
+ )
+ if isinstance(credentials, CredentialsWithQuotaProject):
+ credentials = _apply_quota_project_id(credentials, quota_project_id)
+ return credentials, project_id
+
+
+def _get_gcloud_sdk_credentials(quota_project_id=None):
+ """Gets the credentials and project ID from the Cloud SDK."""
+ from google.auth import _cloud_sdk
+
+ _LOGGER.debug("Checking Cloud SDK credentials as part of auth process...")
+
+ # Check if application default credentials exist.
+ credentials_filename = _cloud_sdk.get_application_default_credentials_path()
+
+ if not os.path.isfile(credentials_filename):
+ _LOGGER.debug("Cloud SDK credentials not found on disk; not using them")
+ return None, None
+
+ credentials, project_id = load_credentials_from_file(
+ credentials_filename, quota_project_id=quota_project_id
+ )
+
+ if not project_id:
+ project_id = _cloud_sdk.get_project_id()
+
+ return credentials, project_id
+
+
+def _get_explicit_environ_credentials(quota_project_id=None):
+ """Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
+ variable."""
+ from google.auth import _cloud_sdk
+
+ cloud_sdk_adc_path = _cloud_sdk.get_application_default_credentials_path()
+ explicit_file = os.environ.get(environment_vars.CREDENTIALS)
+
+ _LOGGER.debug(
+ "Checking %s for explicit credentials as part of auth process...", explicit_file
+ )
+
+ if explicit_file is not None and explicit_file == cloud_sdk_adc_path:
+ # Cloud sdk flow calls gcloud to fetch project id, so if the explicit
+ # file path is cloud sdk credentials path, then we should fall back
+ # to cloud sdk flow, otherwise project id cannot be obtained.
+ _LOGGER.debug(
+ "Explicit credentials path %s is the same as Cloud SDK credentials path, fall back to Cloud SDK credentials flow...",
+ explicit_file,
+ )
+ return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id)
+
+ if explicit_file is not None:
+ credentials, project_id = load_credentials_from_file(
+ os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id
+ )
+
+ return credentials, project_id
+
+ else:
+ return None, None
+
+
+def _get_gae_credentials():
+ """Gets Google App Engine App Identity credentials and project ID."""
+ # If not GAE gen1, prefer the metadata service even if the GAE APIs are
+ # available as per https://google.aip.dev/auth/4115.
+ if os.environ.get(environment_vars.LEGACY_APPENGINE_RUNTIME) != "python27":
+ return None, None
+
+ # While this library is normally bundled with app_engine, there are
+ # some cases where it's not available, so we tolerate ImportError.
+ try:
+ _LOGGER.debug("Checking for App Engine runtime as part of auth process...")
+ import google.auth.app_engine as app_engine
+ except ImportError:
+ _LOGGER.warning("Import of App Engine auth library failed.")
+ return None, None
+
+ try:
+ credentials = app_engine.Credentials()
+ project_id = app_engine.get_project_id()
+ return credentials, project_id
+ except EnvironmentError:
+ _LOGGER.debug(
+ "No App Engine library was found so cannot authentication via App Engine Identity Credentials."
+ )
+ return None, None
+
+
+def _get_gce_credentials(request=None, quota_project_id=None):
+ """Gets credentials and project ID from the GCE Metadata Service."""
+ # Ping requires a transport, but we want application default credentials
+ # to require no arguments. So, we'll use the _http_client transport which
+ # uses http.client. This is only acceptable because the metadata server
+ # doesn't do SSL and never requires proxies.
+
+ # While this library is normally bundled with compute_engine, there are
+ # some cases where it's not available, so we tolerate ImportError.
+ try:
+ from google.auth import compute_engine
+ from google.auth.compute_engine import _metadata
+ except ImportError:
+ _LOGGER.warning("Import of Compute Engine auth library failed.")
+ return None, None
+
+ if request is None:
+ request = google.auth.transport._http_client.Request()
+
+ if _metadata.is_on_gce(request=request):
+ # Get the project ID.
+ try:
+ project_id = _metadata.get_project_id(request=request)
+ except exceptions.TransportError:
+ project_id = None
+
+ cred = compute_engine.Credentials()
+ cred = _apply_quota_project_id(cred, quota_project_id)
+
+ return cred, project_id
+ else:
+ _LOGGER.warning(
+ "Authentication failed using Compute Engine authentication due to unavailable metadata server."
+ )
+ return None, None
+
+
+def _get_external_account_credentials(
+ info, filename, scopes=None, default_scopes=None, request=None
+):
+ """Loads external account Credentials from the parsed external account info.
+
+ The credentials information must correspond to a supported external account
+ credentials.
+
+ Args:
+ info (Mapping[str, str]): The external account info in Google format.
+ filename (str): The full path to the credentials file.
+ scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
+ specified, the credentials will automatically be scoped if
+ necessary.
+ default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+ Google client library. Use 'scopes' for user-defined scopes.
+ request (Optional[google.auth.transport.Request]): An object used to make
+ HTTP requests. This is used to determine the associated project ID
+ for a workload identity pool resource (external account credentials).
+ If not specified, then it will use a
+ google.auth.transport.requests.Request client to make requests.
+
+ Returns:
+ Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
+ credentials and the project ID. External account credentials project
+ IDs may not always be determined.
+
+ Raises:
+ google.auth.exceptions.DefaultCredentialsError: if the info dictionary
+ is in the wrong format or is missing required information.
+ """
+ # There are currently 3 types of external_account credentials.
+ if info.get("subject_token_type") == _AWS_SUBJECT_TOKEN_TYPE:
+ # Check if configuration corresponds to an AWS credentials.
+ from google.auth import aws
+
+ credentials = aws.Credentials.from_info(
+ info, scopes=scopes, default_scopes=default_scopes
+ )
+ elif (
+ info.get("credential_source") is not None
+ and info.get("credential_source").get("executable") is not None
+ ):
+ from google.auth import pluggable
+
+ credentials = pluggable.Credentials.from_info(
+ info, scopes=scopes, default_scopes=default_scopes
+ )
+ else:
+ try:
+ # Check if configuration corresponds to an Identity Pool credentials.
+ from google.auth import identity_pool
+
+ credentials = identity_pool.Credentials.from_info(
+ info, scopes=scopes, default_scopes=default_scopes
+ )
+ except ValueError:
+ # If the configuration is invalid or does not correspond to any
+ # supported external_account credentials, raise an error.
+ raise exceptions.DefaultCredentialsError(
+ "Failed to load external account credentials from {}".format(filename)
+ )
+ if request is None:
+ import google.auth.transport.requests
+
+ request = google.auth.transport.requests.Request()
+
+ return credentials, credentials.get_project_id(request=request)
+
+
+def _get_external_account_authorized_user_credentials(
+ filename, info, scopes=None, default_scopes=None, request=None
+):
+ try:
+ from google.auth import external_account_authorized_user
+
+ credentials = external_account_authorized_user.Credentials.from_info(info)
+ except ValueError:
+ raise exceptions.DefaultCredentialsError(
+ "Failed to load external account authorized user credentials from {}".format(
+ filename
+ )
+ )
+
+ return credentials, None
+
+
+def _get_authorized_user_credentials(filename, info, scopes=None):
+ from google.oauth2 import credentials
+
+ try:
+ credentials = credentials.Credentials.from_authorized_user_info(
+ info, scopes=scopes
+ )
+ except ValueError as caught_exc:
+ msg = "Failed to load authorized user credentials from {}".format(filename)
+ new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
+ raise new_exc from caught_exc
+ return credentials, None
+
+
+def _get_service_account_credentials(filename, info, scopes=None, default_scopes=None):
+ from google.oauth2 import service_account
+
+ try:
+ credentials = service_account.Credentials.from_service_account_info(
+ info, scopes=scopes, default_scopes=default_scopes
+ )
+ except ValueError as caught_exc:
+ msg = "Failed to load service account credentials from {}".format(filename)
+ new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
+ raise new_exc from caught_exc
+ return credentials, info.get("project_id")
+
+
+def _get_impersonated_service_account_credentials(filename, info, scopes):
+ from google.auth import impersonated_credentials
+
+ try:
+ source_credentials_info = info.get("source_credentials")
+ source_credentials_type = source_credentials_info.get("type")
+ if source_credentials_type == _AUTHORIZED_USER_TYPE:
+ source_credentials, _ = _get_authorized_user_credentials(
+ filename, source_credentials_info
+ )
+ elif source_credentials_type == _SERVICE_ACCOUNT_TYPE:
+ source_credentials, _ = _get_service_account_credentials(
+ filename, source_credentials_info
+ )
+ else:
+ raise exceptions.InvalidType(
+ "source credential of type {} is not supported.".format(
+ source_credentials_type
+ )
+ )
+ impersonation_url = info.get("service_account_impersonation_url")
+ start_index = impersonation_url.rfind("/")
+ end_index = impersonation_url.find(":generateAccessToken")
+ if start_index == -1 or end_index == -1 or start_index > end_index:
+ raise exceptions.InvalidValue(
+ "Cannot extract target principal from {}".format(impersonation_url)
+ )
+ target_principal = impersonation_url[start_index + 1 : end_index]
+ delegates = info.get("delegates")
+ quota_project_id = info.get("quota_project_id")
+ credentials = impersonated_credentials.Credentials(
+ source_credentials,
+ target_principal,
+ scopes,
+ delegates,
+ quota_project_id=quota_project_id,
+ )
+ except ValueError as caught_exc:
+ msg = "Failed to load impersonated service account credentials from {}".format(
+ filename
+ )
+ new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
+ raise new_exc from caught_exc
+ return credentials, None
+
+
+def _get_gdch_service_account_credentials(filename, info):
+ from google.oauth2 import gdch_credentials
+
+ try:
+ credentials = gdch_credentials.ServiceAccountCredentials.from_service_account_info(
+ info
+ )
+ except ValueError as caught_exc:
+ msg = "Failed to load GDCH service account credentials from {}".format(filename)
+ new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
+ raise new_exc from caught_exc
+ return credentials, info.get("project")
+
+
+def get_api_key_credentials(key):
+ """Return credentials with the given API key."""
+ from google.auth import api_key
+
+ return api_key.Credentials(key)
+
+
+def _apply_quota_project_id(credentials, quota_project_id):
+ if quota_project_id:
+ credentials = credentials.with_quota_project(quota_project_id)
+ else:
+ credentials = credentials.with_quota_project_from_environment()
+
+ from google.oauth2 import credentials as authorized_user_credentials
+
+ if isinstance(credentials, authorized_user_credentials.Credentials) and (
+ not credentials.quota_project_id
+ ):
+ _warn_about_problematic_credentials(credentials)
+ return credentials
+
+
+def default(scopes=None, request=None, quota_project_id=None, default_scopes=None):
+ """Gets the default credentials for the current environment.
+
+ `Application Default Credentials`_ provides an easy way to obtain
+ credentials to call Google APIs for server-to-server or local applications.
+ This function acquires credentials from the environment in the following
+ order:
+
+ 1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+ to the path of a valid service account JSON private key file, then it is
+ loaded and returned. The project ID returned is the project ID defined
+ in the service account file if available (some older files do not
+ contain project ID information).
+
+ If the environment variable is set to the path of a valid external
+ account JSON configuration file (workload identity federation), then the
+ configuration file is used to determine and retrieve the external
+ credentials from the current environment (AWS, Azure, etc).
+ These will then be exchanged for Google access tokens via the Google STS
+ endpoint.
+ The project ID returned in this case is the one corresponding to the
+ underlying workload identity pool resource if determinable.
+
+ If the environment variable is set to the path of a valid GDCH service
+ account JSON file (`Google Distributed Cloud Hosted`_), then a GDCH
+ credential will be returned. The project ID returned is the project
+ specified in the JSON file.
+ 2. If the `Google Cloud SDK`_ is installed and has application default
+ credentials set they are loaded and returned.
+
+ To enable application default credentials with the Cloud SDK run::
+
+ gcloud auth application-default login
+
+ If the Cloud SDK has an active project, the project ID is returned. The
+ active project can be set using::
+
+ gcloud config set project
+
+ 3. If the application is running in the `App Engine standard environment`_
+ (first generation) then the credentials and project ID from the
+ `App Identity Service`_ are used.
+ 4. If the application is running in `Compute Engine`_ or `Cloud Run`_ or
+ the `App Engine flexible environment`_ or the `App Engine standard
+ environment`_ (second generation) then the credentials and project ID
+ are obtained from the `Metadata Service`_.
+ 5. If no credentials are found,
+ :class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
+
+ .. _Application Default Credentials: https://developers.google.com\
+ /identity/protocols/application-default-credentials
+ .. _Google Cloud SDK: https://cloud.google.com/sdk
+ .. _App Engine standard environment: https://cloud.google.com/appengine
+ .. _App Identity Service: https://cloud.google.com/appengine/docs/python\
+ /appidentity/
+ .. _Compute Engine: https://cloud.google.com/compute
+ .. _App Engine flexible environment: https://cloud.google.com\
+ /appengine/flexible
+ .. _Metadata Service: https://cloud.google.com/compute/docs\
+ /storing-retrieving-metadata
+ .. _Cloud Run: https://cloud.google.com/run
+ .. _Google Distributed Cloud Hosted: https://cloud.google.com/blog/topics\
+ /hybrid-cloud/announcing-google-distributed-cloud-edge-and-hosted
+
+ Example::
+
+ import google.auth
+
+ credentials, project_id = google.auth.default()
+
+ Args:
+ scopes (Sequence[str]): The list of scopes for the credentials. If
+ specified, the credentials will automatically be scoped if
+ necessary.
+ request (Optional[google.auth.transport.Request]): An object used to make
+ HTTP requests. This is used to either detect whether the application
+ is running on Compute Engine or to determine the associated project
+ ID for a workload identity pool resource (external account
+ credentials). If not specified, then it will either use the standard
+ library http client to make requests for Compute Engine credentials
+ or a google.auth.transport.requests.Request client for external
+ account credentials.
+ quota_project_id (Optional[str]): The project ID used for
+ quota and billing.
+ default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+ Google client library. Use 'scopes' for user-defined scopes.
+ Returns:
+ Tuple[~google.auth.credentials.Credentials, Optional[str]]:
+ the current environment's credentials and project ID. Project ID
+ may be None, which indicates that the Project ID could not be
+ ascertained from the environment.
+
+ Raises:
+ ~google.auth.exceptions.DefaultCredentialsError:
+ If no credentials were found, or if the credentials found were
+ invalid.
+ """
+ from google.auth.credentials import with_scopes_if_required
+ from google.auth.credentials import CredentialsWithQuotaProject
+
+ explicit_project_id = os.environ.get(
+ environment_vars.PROJECT, os.environ.get(environment_vars.LEGACY_PROJECT)
+ )
+
+ checkers = (
+ # Avoid passing scopes here to prevent passing scopes to user credentials.
+ # with_scopes_if_required() below will ensure scopes/default scopes are
+ # safely set on the returned credentials since requires_scopes will
+ # guard against setting scopes on user credentials.
+ lambda: _get_explicit_environ_credentials(quota_project_id=quota_project_id),
+ lambda: _get_gcloud_sdk_credentials(quota_project_id=quota_project_id),
+ _get_gae_credentials,
+ lambda: _get_gce_credentials(request, quota_project_id=quota_project_id),
+ )
+
+ for checker in checkers:
+ credentials, project_id = checker()
+ if credentials is not None:
+ credentials = with_scopes_if_required(
+ credentials, scopes, default_scopes=default_scopes
+ )
+
+ effective_project_id = explicit_project_id or project_id
+
+ # For external account credentials, scopes are required to determine
+ # the project ID. Try to get the project ID again if not yet
+ # determined.
+ if not effective_project_id and callable(
+ getattr(credentials, "get_project_id", None)
+ ):
+ if request is None:
+ import google.auth.transport.requests
+
+ request = google.auth.transport.requests.Request()
+ effective_project_id = credentials.get_project_id(request=request)
+
+ if quota_project_id and isinstance(
+ credentials, CredentialsWithQuotaProject
+ ):
+ credentials = credentials.with_quota_project(quota_project_id)
+
+ if not effective_project_id:
+ _LOGGER.warning(
+ "No project ID could be determined. Consider running "
+ "`gcloud config set project` or setting the %s "
+ "environment variable",
+ environment_vars.PROJECT,
+ )
+ return credentials, effective_project_id
+
+ raise exceptions.DefaultCredentialsError(_CLOUD_SDK_MISSING_CREDENTIALS)
diff --git a/contrib/python/google-auth/py3/google/auth/_default_async.py b/contrib/python/google-auth/py3/google/auth/_default_async.py
new file mode 100644
index 0000000000..2e53e20887
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/_default_async.py
@@ -0,0 +1,282 @@
+# Copyright 2020 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Application default credentials.
+
+Implements application default credentials and project ID detection.
+"""
+
+import io
+import json
+import os
+
+from google.auth import _default
+from google.auth import environment_vars
+from google.auth import exceptions
+
+
+def load_credentials_from_file(filename, scopes=None, quota_project_id=None):
+ """Loads Google credentials from a file.
+
+ The credentials file must be a service account key or stored authorized
+ user credentials.
+
+ Args:
+ filename (str): The full path to the credentials file.
+ scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
+ specified, the credentials will automatically be scoped if
+ necessary
+ quota_project_id (Optional[str]): The project ID used for
+ quota and billing.
+
+ Returns:
+ Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
+ credentials and the project ID. Authorized user credentials do not
+ have the project ID information.
+
+ Raises:
+ google.auth.exceptions.DefaultCredentialsError: if the file is in the
+ wrong format or is missing.
+ """
+ if not os.path.exists(filename):
+ raise exceptions.DefaultCredentialsError(
+ "File {} was not found.".format(filename)
+ )
+
+ with io.open(filename, "r") as file_obj:
+ try:
+ info = json.load(file_obj)
+ except ValueError as caught_exc:
+ new_exc = exceptions.DefaultCredentialsError(
+ "File {} is not a valid json file.".format(filename), caught_exc
+ )
+ raise new_exc from caught_exc
+
+ # The type key should indicate that the file is either a service account
+ # credentials file or an authorized user credentials file.
+ credential_type = info.get("type")
+
+ if credential_type == _default._AUTHORIZED_USER_TYPE:
+ from google.oauth2 import _credentials_async as credentials
+
+ try:
+ credentials = credentials.Credentials.from_authorized_user_info(
+ info, scopes=scopes
+ )
+ except ValueError as caught_exc:
+ msg = "Failed to load authorized user credentials from {}".format(filename)
+ new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
+ raise new_exc from caught_exc
+ if quota_project_id:
+ credentials = credentials.with_quota_project(quota_project_id)
+ if not credentials.quota_project_id:
+ _default._warn_about_problematic_credentials(credentials)
+ return credentials, None
+
+ elif credential_type == _default._SERVICE_ACCOUNT_TYPE:
+ from google.oauth2 import _service_account_async as service_account
+
+ try:
+ credentials = service_account.Credentials.from_service_account_info(
+ info, scopes=scopes
+ ).with_quota_project(quota_project_id)
+ except ValueError as caught_exc:
+ msg = "Failed to load service account credentials from {}".format(filename)
+ new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
+ raise new_exc from caught_exc
+ return credentials, info.get("project_id")
+
+ else:
+ raise exceptions.DefaultCredentialsError(
+ "The file {file} does not have a valid type. "
+ "Type is {type}, expected one of {valid_types}.".format(
+ file=filename, type=credential_type, valid_types=_default._VALID_TYPES
+ )
+ )
+
+
+def _get_gcloud_sdk_credentials(quota_project_id=None):
+ """Gets the credentials and project ID from the Cloud SDK."""
+ from google.auth import _cloud_sdk
+
+ # Check if application default credentials exist.
+ credentials_filename = _cloud_sdk.get_application_default_credentials_path()
+
+ if not os.path.isfile(credentials_filename):
+ return None, None
+
+ credentials, project_id = load_credentials_from_file(
+ credentials_filename, quota_project_id=quota_project_id
+ )
+
+ if not project_id:
+ project_id = _cloud_sdk.get_project_id()
+
+ return credentials, project_id
+
+
+def _get_explicit_environ_credentials(quota_project_id=None):
+ """Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
+ variable."""
+ from google.auth import _cloud_sdk
+
+ cloud_sdk_adc_path = _cloud_sdk.get_application_default_credentials_path()
+ explicit_file = os.environ.get(environment_vars.CREDENTIALS)
+
+ if explicit_file is not None and explicit_file == cloud_sdk_adc_path:
+ # Cloud sdk flow calls gcloud to fetch project id, so if the explicit
+ # file path is cloud sdk credentials path, then we should fall back
+ # to cloud sdk flow, otherwise project id cannot be obtained.
+ return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id)
+
+ if explicit_file is not None:
+ credentials, project_id = load_credentials_from_file(
+ os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id
+ )
+
+ return credentials, project_id
+
+ else:
+ return None, None
+
+
+def _get_gae_credentials():
+ """Gets Google App Engine App Identity credentials and project ID."""
+ # While this library is normally bundled with app_engine, there are
+ # some cases where it's not available, so we tolerate ImportError.
+
+ return _default._get_gae_credentials()
+
+
+def _get_gce_credentials(request=None):
+ """Gets credentials and project ID from the GCE Metadata Service."""
+ # Ping requires a transport, but we want application default credentials
+ # to require no arguments. So, we'll use the _http_client transport which
+ # uses http.client. This is only acceptable because the metadata server
+ # doesn't do SSL and never requires proxies.
+
+ # While this library is normally bundled with compute_engine, there are
+ # some cases where it's not available, so we tolerate ImportError.
+
+ return _default._get_gce_credentials(request)
+
+
+def default_async(scopes=None, request=None, quota_project_id=None):
+ """Gets the default credentials for the current environment.
+
+ `Application Default Credentials`_ provides an easy way to obtain
+ credentials to call Google APIs for server-to-server or local applications.
+ This function acquires credentials from the environment in the following
+ order:
+
+ 1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+ to the path of a valid service account JSON private key file, then it is
+ loaded and returned. The project ID returned is the project ID defined
+ in the service account file if available (some older files do not
+ contain project ID information).
+ 2. If the `Google Cloud SDK`_ is installed and has application default
+ credentials set they are loaded and returned.
+
+ To enable application default credentials with the Cloud SDK run::
+
+ gcloud auth application-default login
+
+ If the Cloud SDK has an active project, the project ID is returned. The
+ active project can be set using::
+
+ gcloud config set project
+
+ 3. If the application is running in the `App Engine standard environment`_
+ (first generation) then the credentials and project ID from the
+ `App Identity Service`_ are used.
+ 4. If the application is running in `Compute Engine`_ or `Cloud Run`_ or
+ the `App Engine flexible environment`_ or the `App Engine standard
+ environment`_ (second generation) then the credentials and project ID
+ are obtained from the `Metadata Service`_.
+ 5. If no credentials are found,
+ :class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
+
+ .. _Application Default Credentials: https://developers.google.com\
+ /identity/protocols/application-default-credentials
+ .. _Google Cloud SDK: https://cloud.google.com/sdk
+ .. _App Engine standard environment: https://cloud.google.com/appengine
+ .. _App Identity Service: https://cloud.google.com/appengine/docs/python\
+ /appidentity/
+ .. _Compute Engine: https://cloud.google.com/compute
+ .. _App Engine flexible environment: https://cloud.google.com\
+ /appengine/flexible
+ .. _Metadata Service: https://cloud.google.com/compute/docs\
+ /storing-retrieving-metadata
+ .. _Cloud Run: https://cloud.google.com/run
+
+ Example::
+
+ import google.auth
+
+ credentials, project_id = google.auth.default()
+
+ Args:
+ scopes (Sequence[str]): The list of scopes for the credentials. If
+ specified, the credentials will automatically be scoped if
+ necessary.
+ request (google.auth.transport.Request): An object used to make
+ HTTP requests. This is used to detect whether the application
+ is running on Compute Engine. If not specified, then it will
+ use the standard library http client to make requests.
+ quota_project_id (Optional[str]): The project ID used for
+ quota and billing.
+ Returns:
+ Tuple[~google.auth.credentials.Credentials, Optional[str]]:
+ the current environment's credentials and project ID. Project ID
+ may be None, which indicates that the Project ID could not be
+ ascertained from the environment.
+
+ Raises:
+ ~google.auth.exceptions.DefaultCredentialsError:
+ If no credentials were found, or if the credentials found were
+ invalid.
+ """
+ from google.auth._credentials_async import with_scopes_if_required
+ from google.auth.credentials import CredentialsWithQuotaProject
+
+ explicit_project_id = os.environ.get(
+ environment_vars.PROJECT, os.environ.get(environment_vars.LEGACY_PROJECT)
+ )
+
+ checkers = (
+ lambda: _get_explicit_environ_credentials(quota_project_id=quota_project_id),
+ lambda: _get_gcloud_sdk_credentials(quota_project_id=quota_project_id),
+ _get_gae_credentials,
+ lambda: _get_gce_credentials(request),
+ )
+
+ for checker in checkers:
+ credentials, project_id = checker()
+ if credentials is not None:
+ credentials = with_scopes_if_required(credentials, scopes)
+ if quota_project_id and isinstance(
+ credentials, CredentialsWithQuotaProject
+ ):
+ credentials = credentials.with_quota_project(quota_project_id)
+ effective_project_id = explicit_project_id or project_id
+ if not effective_project_id:
+ _default._LOGGER.warning(
+ "No project ID could be determined. Consider running "
+ "`gcloud config set project` or setting the %s "
+ "environment variable",
+ environment_vars.PROJECT,
+ )
+ return credentials, effective_project_id
+
+ raise exceptions.DefaultCredentialsError(_default._CLOUD_SDK_MISSING_CREDENTIALS)
diff --git a/contrib/python/google-auth/py3/google/auth/_exponential_backoff.py b/contrib/python/google-auth/py3/google/auth/_exponential_backoff.py
new file mode 100644
index 0000000000..0dd621a949
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/_exponential_backoff.py
@@ -0,0 +1,109 @@
+# Copyright 2022 Google LLC
+#
+# 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 random
+import time
+
+# The default amount of retry attempts
+_DEFAULT_RETRY_TOTAL_ATTEMPTS = 3
+
+# The default initial backoff period (1.0 second).
+_DEFAULT_INITIAL_INTERVAL_SECONDS = 1.0
+
+# The default randomization factor (0.1 which results in a random period ranging
+# between 10% below and 10% above the retry interval).
+_DEFAULT_RANDOMIZATION_FACTOR = 0.1
+
+# The default multiplier value (2 which is 100% increase per back off).
+_DEFAULT_MULTIPLIER = 2.0
+
+"""Exponential Backoff Utility
+
+This is a private module that implements the exponential back off algorithm.
+It can be used as a utility for code that needs to retry on failure, for example
+an HTTP request.
+"""
+
+
+class ExponentialBackoff:
+ """An exponential backoff iterator. This can be used in a for loop to
+ perform requests with exponential backoff.
+
+ Args:
+ total_attempts Optional[int]:
+ The maximum amount of retries that should happen.
+ The default value is 3 attempts.
+ initial_wait_seconds Optional[int]:
+ The amount of time to sleep in the first backoff. This parameter
+ should be in seconds.
+ The default value is 1 second.
+ randomization_factor Optional[float]:
+ The amount of jitter that should be in each backoff. For example,
+ a value of 0.1 will introduce a jitter range of 10% to the
+ current backoff period.
+ The default value is 0.1.
+ multiplier Optional[float]:
+ The backoff multipler. This adjusts how much each backoff will
+ increase. For example a value of 2.0 leads to a 200% backoff
+ on each attempt. If the initial_wait is 1.0 it would look like
+ this sequence [1.0, 2.0, 4.0, 8.0].
+ The default value is 2.0.
+ """
+
+ def __init__(
+ self,
+ total_attempts=_DEFAULT_RETRY_TOTAL_ATTEMPTS,
+ initial_wait_seconds=_DEFAULT_INITIAL_INTERVAL_SECONDS,
+ randomization_factor=_DEFAULT_RANDOMIZATION_FACTOR,
+ multiplier=_DEFAULT_MULTIPLIER,
+ ):
+ self._total_attempts = total_attempts
+ self._initial_wait_seconds = initial_wait_seconds
+
+ self._current_wait_in_seconds = self._initial_wait_seconds
+
+ self._randomization_factor = randomization_factor
+ self._multiplier = multiplier
+ self._backoff_count = 0
+
+ def __iter__(self):
+ self._backoff_count = 0
+ self._current_wait_in_seconds = self._initial_wait_seconds
+ return self
+
+ def __next__(self):
+ if self._backoff_count >= self._total_attempts:
+ raise StopIteration
+ self._backoff_count += 1
+
+ jitter_variance = self._current_wait_in_seconds * self._randomization_factor
+ jitter = random.uniform(
+ self._current_wait_in_seconds - jitter_variance,
+ self._current_wait_in_seconds + jitter_variance,
+ )
+
+ time.sleep(jitter)
+
+ self._current_wait_in_seconds *= self._multiplier
+ return self._backoff_count
+
+ @property
+ def total_attempts(self):
+ """The total amount of backoff attempts that will be made."""
+ return self._total_attempts
+
+ @property
+ def backoff_count(self):
+ """The current amount of backoff attempts that have been made."""
+ return self._backoff_count
diff --git a/contrib/python/google-auth/py3/google/auth/_helpers.py b/contrib/python/google-auth/py3/google/auth/_helpers.py
new file mode 100644
index 0000000000..ad2c095f28
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/_helpers.py
@@ -0,0 +1,245 @@
+# Copyright 2015 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helper functions for commonly used utilities."""
+
+import base64
+import calendar
+import datetime
+import sys
+import urllib
+
+from google.auth import exceptions
+
+# Token server doesn't provide a new a token when doing refresh unless the
+# token is expiring within 30 seconds, so refresh threshold should not be
+# more than 30 seconds. Otherwise auth lib will send tons of refresh requests
+# until 30 seconds before the expiration, and cause a spike of CPU usage.
+REFRESH_THRESHOLD = datetime.timedelta(seconds=20)
+
+
+def copy_docstring(source_class):
+ """Decorator that copies a method's docstring from another class.
+
+ Args:
+ source_class (type): The class that has the documented method.
+
+ Returns:
+ Callable: A decorator that will copy the docstring of the same
+ named method in the source class to the decorated method.
+ """
+
+ def decorator(method):
+ """Decorator implementation.
+
+ Args:
+ method (Callable): The method to copy the docstring to.
+
+ Returns:
+ Callable: the same method passed in with an updated docstring.
+
+ Raises:
+ google.auth.exceptions.InvalidOperation: if the method already has a docstring.
+ """
+ if method.__doc__:
+ raise exceptions.InvalidOperation("Method already has a docstring.")
+
+ source_method = getattr(source_class, method.__name__)
+ method.__doc__ = source_method.__doc__
+
+ return method
+
+ return decorator
+
+
+def utcnow():
+ """Returns the current UTC datetime.
+
+ Returns:
+ datetime: The current time in UTC.
+ """
+ return datetime.datetime.utcnow()
+
+
+def datetime_to_secs(value):
+ """Convert a datetime object to the number of seconds since the UNIX epoch.
+
+ Args:
+ value (datetime): The datetime to convert.
+
+ Returns:
+ int: The number of seconds since the UNIX epoch.
+ """
+ return calendar.timegm(value.utctimetuple())
+
+
+def to_bytes(value, encoding="utf-8"):
+ """Converts a string value to bytes, if necessary.
+
+ Args:
+ value (Union[str, bytes]): The value to be converted.
+ encoding (str): The encoding to use to convert unicode to bytes.
+ Defaults to "utf-8".
+
+ Returns:
+ bytes: The original value converted to bytes (if unicode) or as
+ passed in if it started out as bytes.
+
+ Raises:
+ google.auth.exceptions.InvalidValue: If the value could not be converted to bytes.
+ """
+ result = value.encode(encoding) if isinstance(value, str) else value
+ if isinstance(result, bytes):
+ return result
+ else:
+ raise exceptions.InvalidValue(
+ "{0!r} could not be converted to bytes".format(value)
+ )
+
+
+def from_bytes(value):
+ """Converts bytes to a string value, if necessary.
+
+ Args:
+ value (Union[str, bytes]): The value to be converted.
+
+ Returns:
+ str: The original value converted to unicode (if bytes) or as passed in
+ if it started out as unicode.
+
+ Raises:
+ google.auth.exceptions.InvalidValue: If the value could not be converted to unicode.
+ """
+ result = value.decode("utf-8") if isinstance(value, bytes) else value
+ if isinstance(result, str):
+ return result
+ else:
+ raise exceptions.InvalidValue(
+ "{0!r} could not be converted to unicode".format(value)
+ )
+
+
+def update_query(url, params, remove=None):
+ """Updates a URL's query parameters.
+
+ Replaces any current values if they are already present in the URL.
+
+ Args:
+ url (str): The URL to update.
+ params (Mapping[str, str]): A mapping of query parameter
+ keys to values.
+ remove (Sequence[str]): Parameters to remove from the query string.
+
+ Returns:
+ str: The URL with updated query parameters.
+
+ Examples:
+
+ >>> url = 'http://example.com?a=1'
+ >>> update_query(url, {'a': '2'})
+ http://example.com?a=2
+ >>> update_query(url, {'b': '3'})
+ http://example.com?a=1&b=3
+ >> update_query(url, {'b': '3'}, remove=['a'])
+ http://example.com?b=3
+
+ """
+ if remove is None:
+ remove = []
+
+ # Split the URL into parts.
+ parts = urllib.parse.urlparse(url)
+ # Parse the query string.
+ query_params = urllib.parse.parse_qs(parts.query)
+ # Update the query parameters with the new parameters.
+ query_params.update(params)
+ # Remove any values specified in remove.
+ query_params = {
+ key: value for key, value in query_params.items() if key not in remove
+ }
+ # Re-encoded the query string.
+ new_query = urllib.parse.urlencode(query_params, doseq=True)
+ # Unsplit the url.
+ new_parts = parts._replace(query=new_query)
+ return urllib.parse.urlunparse(new_parts)
+
+
+def scopes_to_string(scopes):
+ """Converts scope value to a string suitable for sending to OAuth 2.0
+ authorization servers.
+
+ Args:
+ scopes (Sequence[str]): The sequence of scopes to convert.
+
+ Returns:
+ str: The scopes formatted as a single string.
+ """
+ return " ".join(scopes)
+
+
+def string_to_scopes(scopes):
+ """Converts stringifed scopes value to a list.
+
+ Args:
+ scopes (Union[Sequence, str]): The string of space-separated scopes
+ to convert.
+ Returns:
+ Sequence(str): The separated scopes.
+ """
+ if not scopes:
+ return []
+
+ return scopes.split(" ")
+
+
+def padded_urlsafe_b64decode(value):
+ """Decodes base64 strings lacking padding characters.
+
+ Google infrastructure tends to omit the base64 padding characters.
+
+ Args:
+ value (Union[str, bytes]): The encoded value.
+
+ Returns:
+ bytes: The decoded value
+ """
+ b64string = to_bytes(value)
+ padded = b64string + b"=" * (-len(b64string) % 4)
+ return base64.urlsafe_b64decode(padded)
+
+
+def unpadded_urlsafe_b64encode(value):
+ """Encodes base64 strings removing any padding characters.
+
+ `rfc 7515`_ defines Base64url to NOT include any padding
+ characters, but the stdlib doesn't do that by default.
+
+ _rfc7515: https://tools.ietf.org/html/rfc7515#page-6
+
+ Args:
+ value (Union[str|bytes]): The bytes-like value to encode
+
+ Returns:
+ Union[str|bytes]: The encoded value
+ """
+ return base64.urlsafe_b64encode(value).rstrip(b"=")
+
+
+def is_python_3():
+ """Check if the Python interpreter is Python 2 or 3.
+
+ Returns:
+ bool: True if the Python interpreter is Python 3 and False otherwise.
+ """
+ return sys.version_info > (3, 0)
diff --git a/contrib/python/google-auth/py3/google/auth/_jwt_async.py b/contrib/python/google-auth/py3/google/auth/_jwt_async.py
new file mode 100644
index 0000000000..3a1abc5b85
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/_jwt_async.py
@@ -0,0 +1,164 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+
+"""JSON Web Tokens
+
+Provides support for creating (encoding) and verifying (decoding) JWTs,
+especially JWTs generated and consumed by Google infrastructure.
+
+See `rfc7519`_ for more details on JWTs.
+
+To encode a JWT use :func:`encode`::
+
+ from google.auth import crypt
+ from google.auth import jwt_async
+
+ signer = crypt.Signer(private_key)
+ payload = {'some': 'payload'}
+ encoded = jwt_async.encode(signer, payload)
+
+To decode a JWT and verify claims use :func:`decode`::
+
+ claims = jwt_async.decode(encoded, certs=public_certs)
+
+You can also skip verification::
+
+ claims = jwt_async.decode(encoded, verify=False)
+
+.. _rfc7519: https://tools.ietf.org/html/rfc7519
+
+
+NOTE: This async support is experimental and marked internal. This surface may
+change in minor releases.
+"""
+
+from google.auth import _credentials_async
+from google.auth import jwt
+
+
+def encode(signer, payload, header=None, key_id=None):
+ """Make a signed JWT.
+
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign the JWT.
+ payload (Mapping[str, str]): The JWT payload.
+ header (Mapping[str, str]): Additional JWT header payload.
+ key_id (str): The key id to add to the JWT header. If the
+ signer has a key id it will be used as the default. If this is
+ specified it will override the signer's key id.
+
+ Returns:
+ bytes: The encoded JWT.
+ """
+ return jwt.encode(signer, payload, header, key_id)
+
+
+def decode(token, certs=None, verify=True, audience=None):
+ """Decode and verify a JWT.
+
+ Args:
+ token (str): The encoded JWT.
+ certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
+ certificate used to validate the JWT signature. If bytes or string,
+ it must the the public key certificate in PEM format. If a mapping,
+ it must be a mapping of key IDs to public key certificates in PEM
+ format. The mapping must contain the same key ID that's specified
+ in the token's header.
+ verify (bool): Whether to perform signature and claim validation.
+ Verification is done by default.
+ audience (str): The audience claim, 'aud', that this JWT should
+ contain. If None then the JWT's 'aud' parameter is not verified.
+
+ Returns:
+ Mapping[str, str]: The deserialized JSON payload in the JWT.
+
+ Raises:
+ ValueError: if any verification checks failed.
+ """
+
+ return jwt.decode(token, certs, verify, audience)
+
+
+class Credentials(
+ jwt.Credentials, _credentials_async.Signing, _credentials_async.Credentials
+):
+ """Credentials that use a JWT as the bearer token.
+
+ These credentials require an "audience" claim. This claim identifies the
+ intended recipient of the bearer token.
+
+ The constructor arguments determine the claims for the JWT that is
+ sent with requests. Usually, you'll construct these credentials with
+ one of the helper constructors as shown in the next section.
+
+ To create JWT credentials using a Google service account private key
+ JSON file::
+
+ audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
+ credentials = jwt_async.Credentials.from_service_account_file(
+ 'service-account.json',
+ audience=audience)
+
+ If you already have the service account file loaded and parsed::
+
+ service_account_info = json.load(open('service_account.json'))
+ credentials = jwt_async.Credentials.from_service_account_info(
+ service_account_info,
+ audience=audience)
+
+ Both helper methods pass on arguments to the constructor, so you can
+ specify the JWT claims::
+
+ credentials = jwt_async.Credentials.from_service_account_file(
+ 'service-account.json',
+ audience=audience,
+ additional_claims={'meta': 'data'})
+
+ You can also construct the credentials directly if you have a
+ :class:`~google.auth.crypt.Signer` instance::
+
+ credentials = jwt_async.Credentials(
+ signer,
+ issuer='your-issuer',
+ subject='your-subject',
+ audience=audience)
+
+ The claims are considered immutable. If you want to modify the claims,
+ you can easily create another instance using :meth:`with_claims`::
+
+ new_audience = (
+ 'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
+ new_credentials = credentials.with_claims(audience=new_audience)
+ """
+
+
+class OnDemandCredentials(
+ jwt.OnDemandCredentials, _credentials_async.Signing, _credentials_async.Credentials
+):
+ """On-demand JWT credentials.
+
+ Like :class:`Credentials`, this class uses a JWT as the bearer token for
+ authentication. However, this class does not require the audience at
+ construction time. Instead, it will generate a new token on-demand for
+ each request using the request URI as the audience. It caches tokens
+ so that multiple requests to the same URI do not incur the overhead
+ of generating a new token every time.
+
+ This behavior is especially useful for `gRPC`_ clients. A gRPC service may
+ have multiple audience and gRPC clients may not know all of the audiences
+ required for accessing a particular service. With these credentials,
+ no knowledge of the audiences is required ahead of time.
+
+ .. _grpc: http://www.grpc.io/
+ """
diff --git a/contrib/python/google-auth/py3/google/auth/_oauth2client.py b/contrib/python/google-auth/py3/google/auth/_oauth2client.py
new file mode 100644
index 0000000000..8b83ff23c1
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/_oauth2client.py
@@ -0,0 +1,167 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Helpers for transitioning from oauth2client to google-auth.
+
+.. warning::
+ This module is private as it is intended to assist first-party downstream
+ clients with the transition from oauth2client to google-auth.
+"""
+
+from __future__ import absolute_import
+
+from google.auth import _helpers
+import google.auth.app_engine
+import google.auth.compute_engine
+import google.oauth2.credentials
+import google.oauth2.service_account
+
+try:
+ import oauth2client.client # type: ignore
+ import oauth2client.contrib.gce # type: ignore
+ import oauth2client.service_account # type: ignore
+except ImportError as caught_exc:
+ raise ImportError("oauth2client is not installed.") from caught_exc
+
+try:
+ import oauth2client.contrib.appengine # type: ignore
+
+ _HAS_APPENGINE = True
+except ImportError:
+ _HAS_APPENGINE = False
+
+
+_CONVERT_ERROR_TMPL = "Unable to convert {} to a google-auth credentials class."
+
+
+def _convert_oauth2_credentials(credentials):
+ """Converts to :class:`google.oauth2.credentials.Credentials`.
+
+ Args:
+ credentials (Union[oauth2client.client.OAuth2Credentials,
+ oauth2client.client.GoogleCredentials]): The credentials to
+ convert.
+
+ Returns:
+ google.oauth2.credentials.Credentials: The converted credentials.
+ """
+ new_credentials = google.oauth2.credentials.Credentials(
+ token=credentials.access_token,
+ refresh_token=credentials.refresh_token,
+ token_uri=credentials.token_uri,
+ client_id=credentials.client_id,
+ client_secret=credentials.client_secret,
+ scopes=credentials.scopes,
+ )
+
+ new_credentials._expires = credentials.token_expiry
+
+ return new_credentials
+
+
+def _convert_service_account_credentials(credentials):
+ """Converts to :class:`google.oauth2.service_account.Credentials`.
+
+ Args:
+ credentials (Union[
+ oauth2client.service_account.ServiceAccountCredentials,
+ oauth2client.service_account._JWTAccessCredentials]): The
+ credentials to convert.
+
+ Returns:
+ google.oauth2.service_account.Credentials: The converted credentials.
+ """
+ info = credentials.serialization_data.copy()
+ info["token_uri"] = credentials.token_uri
+ return google.oauth2.service_account.Credentials.from_service_account_info(info)
+
+
+def _convert_gce_app_assertion_credentials(credentials):
+ """Converts to :class:`google.auth.compute_engine.Credentials`.
+
+ Args:
+ credentials (oauth2client.contrib.gce.AppAssertionCredentials): The
+ credentials to convert.
+
+ Returns:
+ google.oauth2.service_account.Credentials: The converted credentials.
+ """
+ return google.auth.compute_engine.Credentials(
+ service_account_email=credentials.service_account_email
+ )
+
+
+def _convert_appengine_app_assertion_credentials(credentials):
+ """Converts to :class:`google.auth.app_engine.Credentials`.
+
+ Args:
+ credentials (oauth2client.contrib.app_engine.AppAssertionCredentials):
+ The credentials to convert.
+
+ Returns:
+ google.oauth2.service_account.Credentials: The converted credentials.
+ """
+ # pylint: disable=invalid-name
+ return google.auth.app_engine.Credentials(
+ scopes=_helpers.string_to_scopes(credentials.scope),
+ service_account_id=credentials.service_account_id,
+ )
+
+
+_CLASS_CONVERSION_MAP = {
+ oauth2client.client.OAuth2Credentials: _convert_oauth2_credentials,
+ oauth2client.client.GoogleCredentials: _convert_oauth2_credentials,
+ oauth2client.service_account.ServiceAccountCredentials: _convert_service_account_credentials,
+ oauth2client.service_account._JWTAccessCredentials: _convert_service_account_credentials,
+ oauth2client.contrib.gce.AppAssertionCredentials: _convert_gce_app_assertion_credentials,
+}
+
+if _HAS_APPENGINE:
+ _CLASS_CONVERSION_MAP[
+ oauth2client.contrib.appengine.AppAssertionCredentials
+ ] = _convert_appengine_app_assertion_credentials
+
+
+def convert(credentials):
+ """Convert oauth2client credentials to google-auth credentials.
+
+ This class converts:
+
+ - :class:`oauth2client.client.OAuth2Credentials` to
+ :class:`google.oauth2.credentials.Credentials`.
+ - :class:`oauth2client.client.GoogleCredentials` to
+ :class:`google.oauth2.credentials.Credentials`.
+ - :class:`oauth2client.service_account.ServiceAccountCredentials` to
+ :class:`google.oauth2.service_account.Credentials`.
+ - :class:`oauth2client.service_account._JWTAccessCredentials` to
+ :class:`google.oauth2.service_account.Credentials`.
+ - :class:`oauth2client.contrib.gce.AppAssertionCredentials` to
+ :class:`google.auth.compute_engine.Credentials`.
+ - :class:`oauth2client.contrib.appengine.AppAssertionCredentials` to
+ :class:`google.auth.app_engine.Credentials`.
+
+ Returns:
+ google.auth.credentials.Credentials: The converted credentials.
+
+ Raises:
+ ValueError: If the credentials could not be converted.
+ """
+
+ credentials_class = type(credentials)
+
+ try:
+ return _CLASS_CONVERSION_MAP[credentials_class](credentials)
+ except KeyError as caught_exc:
+ new_exc = ValueError(_CONVERT_ERROR_TMPL.format(credentials_class))
+ raise new_exc from caught_exc
diff --git a/contrib/python/google-auth/py3/google/auth/_service_account_info.py b/contrib/python/google-auth/py3/google/auth/_service_account_info.py
new file mode 100644
index 0000000000..6b64adcaeb
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/_service_account_info.py
@@ -0,0 +1,80 @@
+# Copyright 2016 Google LLC
+#
+# 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 loading data from a Google service account file."""
+
+import io
+import json
+
+from google.auth import crypt
+from google.auth import exceptions
+
+
+def from_dict(data, require=None, use_rsa_signer=True):
+ """Validates a dictionary containing Google service account data.
+
+ Creates and returns a :class:`google.auth.crypt.Signer` instance from the
+ private key specified in the data.
+
+ Args:
+ data (Mapping[str, str]): The service account data
+ require (Sequence[str]): List of keys required to be present in the
+ info.
+ use_rsa_signer (Optional[bool]): Whether to use RSA signer or EC signer.
+ We use RSA signer by default.
+
+ Returns:
+ google.auth.crypt.Signer: A signer created from the private key in the
+ service account file.
+
+ Raises:
+ MalformedError: if the data was in the wrong format, or if one of the
+ required keys is missing.
+ """
+ keys_needed = set(require if require is not None else [])
+
+ missing = keys_needed.difference(data.keys())
+
+ if missing:
+ raise exceptions.MalformedError(
+ "Service account info was not in the expected format, missing "
+ "fields {}.".format(", ".join(missing))
+ )
+
+ # Create a signer.
+ if use_rsa_signer:
+ signer = crypt.RSASigner.from_service_account_info(data)
+ else:
+ signer = crypt.ES256Signer.from_service_account_info(data)
+
+ return signer
+
+
+def from_filename(filename, require=None, use_rsa_signer=True):
+ """Reads a Google service account JSON file and returns its parsed info.
+
+ Args:
+ filename (str): The path to the service account .json file.
+ require (Sequence[str]): List of keys required to be present in the
+ info.
+ use_rsa_signer (Optional[bool]): Whether to use RSA signer or EC signer.
+ We use RSA signer by default.
+
+ Returns:
+ Tuple[ Mapping[str, str], google.auth.crypt.Signer ]: The verified
+ info and a signer instance.
+ """
+ with io.open(filename, "r", encoding="utf-8") as json_file:
+ data = json.load(json_file)
+ return data, from_dict(data, require=require, use_rsa_signer=use_rsa_signer)
diff --git a/contrib/python/google-auth/py3/google/auth/api_key.py b/contrib/python/google-auth/py3/google/auth/api_key.py
new file mode 100644
index 0000000000..4fdf7f2769
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/api_key.py
@@ -0,0 +1,76 @@
+# Copyright 2022 Google LLC
+#
+# 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 API key support.
+This module provides authentication using the `API key`_.
+.. _API key:
+ https://cloud.google.com/docs/authentication/api-keys/
+"""
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+
+
+class Credentials(credentials.Credentials):
+ """API key credentials.
+ These credentials use API key to provide authorization to applications.
+ """
+
+ def __init__(self, token):
+ """
+ Args:
+ token (str): API key string
+ Raises:
+ ValueError: If the provided API key is not a non-empty string.
+ """
+ super(Credentials, self).__init__()
+ if not token:
+ raise exceptions.InvalidValue("Token must be a non-empty API key string")
+ self.token = token
+
+ @property
+ def expired(self):
+ return False
+
+ @property
+ def valid(self):
+ return True
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def refresh(self, request):
+ return
+
+ def apply(self, headers, token=None):
+ """Apply the API key token to the x-goog-api-key header.
+ Args:
+ headers (Mapping): The HTTP request headers.
+ token (Optional[str]): If specified, overrides the current access
+ token.
+ """
+ headers["x-goog-api-key"] = token or self.token
+
+ def before_request(self, request, method, url, headers):
+ """Performs credential-specific before request logic.
+ Refreshes the credentials if necessary, then calls :meth:`apply` to
+ apply the token to the x-goog-api-key header.
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ method (str): The request's HTTP method or the RPC method being
+ invoked.
+ url (str): The request's URI or the RPC service's URI.
+ headers (Mapping): The request's headers.
+ """
+ self.apply(headers)
diff --git a/contrib/python/google-auth/py3/google/auth/app_engine.py b/contrib/python/google-auth/py3/google/auth/app_engine.py
new file mode 100644
index 0000000000..7083ee6143
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/app_engine.py
@@ -0,0 +1,180 @@
+# Copyright 2016 Google LLC
+#
+# 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 standard environment support.
+
+This module provides authentication and signing for applications running on App
+Engine in the standard environment using the `App Identity API`_.
+
+
+.. _App Identity API:
+ https://cloud.google.com/appengine/docs/python/appidentity/
+"""
+
+import datetime
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import crypt
+from google.auth import exceptions
+
+# pytype: disable=import-error
+try:
+ from google.appengine.api import app_identity # type: ignore
+except ImportError:
+ app_identity = None # type: ignore
+# pytype: enable=import-error
+
+
+class Signer(crypt.Signer):
+ """Signs messages using the App Engine App Identity service.
+
+ This can be used in place of :class:`google.auth.crypt.Signer` when
+ running in the App Engine standard environment.
+ """
+
+ @property
+ def key_id(self):
+ """Optional[str]: The key ID used to identify this private key.
+
+ .. warning::
+ This is always ``None``. The key ID used by App Engine can not
+ be reliably determined ahead of time.
+ """
+ return None
+
+ @_helpers.copy_docstring(crypt.Signer)
+ def sign(self, message):
+ message = _helpers.to_bytes(message)
+ _, signature = app_identity.sign_blob(message)
+ return signature
+
+
+def get_project_id():
+ """Gets the project ID for the current App Engine application.
+
+ Returns:
+ str: The project ID
+
+ Raises:
+ google.auth.exceptions.OSError: If the App Engine APIs are unavailable.
+ """
+ # pylint: disable=missing-raises-doc
+ # Pylint rightfully thinks google.auth.exceptions.OSError is OSError, but doesn't
+ # realize it's a valid alias.
+ if app_identity is None:
+ raise exceptions.OSError("The App Engine APIs are not available.")
+ return app_identity.get_application_id()
+
+
+class Credentials(
+ credentials.Scoped, credentials.Signing, credentials.CredentialsWithQuotaProject
+):
+ """App Engine standard environment credentials.
+
+ These credentials use the App Engine App Identity API to obtain access
+ tokens.
+ """
+
+ def __init__(
+ self,
+ scopes=None,
+ default_scopes=None,
+ service_account_id=None,
+ quota_project_id=None,
+ ):
+ """
+ Args:
+ scopes (Sequence[str]): Scopes to request from the App Identity
+ API.
+ default_scopes (Sequence[str]): Default scopes passed by a
+ Google client library. Use 'scopes' for user-defined scopes.
+ service_account_id (str): The service account ID passed into
+ :func:`google.appengine.api.app_identity.get_access_token`.
+ If not specified, the default application service account
+ ID will be used.
+ quota_project_id (Optional[str]): The project ID used for quota
+ and billing.
+
+ Raises:
+ google.auth.exceptions.OSError: If the App Engine APIs are unavailable.
+ """
+ # pylint: disable=missing-raises-doc
+ # Pylint rightfully thinks google.auth.exceptions.OSError is OSError, but doesn't
+ # realize it's a valid alias.
+ if app_identity is None:
+ raise exceptions.OSError("The App Engine APIs are not available.")
+
+ super(Credentials, self).__init__()
+ self._scopes = scopes
+ self._default_scopes = default_scopes
+ self._service_account_id = service_account_id
+ self._signer = Signer()
+ self._quota_project_id = quota_project_id
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def refresh(self, request):
+ scopes = self._scopes if self._scopes is not None else self._default_scopes
+ # pylint: disable=unused-argument
+ token, ttl = app_identity.get_access_token(scopes, self._service_account_id)
+ expiry = datetime.datetime.utcfromtimestamp(ttl)
+
+ self.token, self.expiry = token, expiry
+
+ @property
+ def service_account_email(self):
+ """The service account email."""
+ if self._service_account_id is None:
+ self._service_account_id = app_identity.get_service_account_name()
+ return self._service_account_id
+
+ @property
+ def requires_scopes(self):
+ """Checks if the credentials requires scopes.
+
+ Returns:
+ bool: True if there are no scopes set otherwise False.
+ """
+ return not self._scopes and not self._default_scopes
+
+ @_helpers.copy_docstring(credentials.Scoped)
+ def with_scopes(self, scopes, default_scopes=None):
+ return self.__class__(
+ scopes=scopes,
+ default_scopes=default_scopes,
+ service_account_id=self._service_account_id,
+ quota_project_id=self.quota_project_id,
+ )
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ return self.__class__(
+ scopes=self._scopes,
+ service_account_id=self._service_account_id,
+ quota_project_id=quota_project_id,
+ )
+
+ @_helpers.copy_docstring(credentials.Signing)
+ def sign_bytes(self, message):
+ return self._signer.sign(message)
+
+ @property # type: ignore
+ @_helpers.copy_docstring(credentials.Signing)
+ def signer_email(self):
+ return self.service_account_email
+
+ @property # type: ignore
+ @_helpers.copy_docstring(credentials.Signing)
+ def signer(self):
+ return self._signer
diff --git a/contrib/python/google-auth/py3/google/auth/aws.py b/contrib/python/google-auth/py3/google/auth/aws.py
new file mode 100644
index 0000000000..6e0e4e864f
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/aws.py
@@ -0,0 +1,777 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+
+"""AWS Credentials and AWS Signature V4 Request Signer.
+
+This module provides credentials to access Google Cloud resources from Amazon
+Web Services (AWS) workloads. These credentials are recommended over the
+use of service account credentials in AWS as they do not involve the management
+of long-live service account private keys.
+
+AWS Credentials are initialized using external_account arguments which are
+typically loaded from the external credentials JSON file.
+Unlike other Credentials that can be initialized with a list of explicit
+arguments, secrets or credentials, external account clients use the
+environment and hints/guidelines provided by the external_account JSON
+file to retrieve credentials and exchange them for Google access tokens.
+
+This module also provides a basic implementation of the
+`AWS Signature Version 4`_ request signing algorithm.
+
+AWS Credentials use serialized signed requests to the
+`AWS STS GetCallerIdentity`_ API that can be exchanged for Google access tokens
+via the GCP STS endpoint.
+
+.. _AWS Signature Version 4: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
+.. _AWS STS GetCallerIdentity: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
+"""
+
+import hashlib
+import hmac
+import http.client as http_client
+import json
+import os
+import posixpath
+import re
+import urllib
+from urllib.parse import urljoin
+
+from google.auth import _helpers
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import external_account
+
+# AWS Signature Version 4 signing algorithm identifier.
+_AWS_ALGORITHM = "AWS4-HMAC-SHA256"
+# The termination string for the AWS credential scope value as defined in
+# https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
+_AWS_REQUEST_TYPE = "aws4_request"
+# The AWS authorization header name for the security session token if available.
+_AWS_SECURITY_TOKEN_HEADER = "x-amz-security-token"
+# The AWS authorization header name for the auto-generated date.
+_AWS_DATE_HEADER = "x-amz-date"
+
+
+class RequestSigner(object):
+ """Implements an AWS request signer based on the AWS Signature Version 4 signing
+ process.
+ https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
+ """
+
+ def __init__(self, region_name):
+ """Instantiates an AWS request signer used to compute authenticated signed
+ requests to AWS APIs based on the AWS Signature Version 4 signing process.
+
+ Args:
+ region_name (str): The AWS region to use.
+ """
+
+ self._region_name = region_name
+
+ def get_request_options(
+ self,
+ aws_security_credentials,
+ url,
+ method,
+ request_payload="",
+ additional_headers={},
+ ):
+ """Generates the signed request for the provided HTTP request for calling
+ an AWS API. This follows the steps described at:
+ https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
+
+ Args:
+ aws_security_credentials (Mapping[str, str]): A dictionary containing
+ the AWS security credentials.
+ url (str): The AWS service URL containing the canonical URI and
+ query string.
+ method (str): The HTTP method used to call this API.
+ request_payload (Optional[str]): The optional request payload if
+ available.
+ additional_headers (Optional[Mapping[str, str]]): The optional
+ additional headers needed for the requested AWS API.
+
+ Returns:
+ Mapping[str, str]: The AWS signed request dictionary object.
+ """
+ # Get AWS credentials.
+ access_key = aws_security_credentials.get("access_key_id")
+ secret_key = aws_security_credentials.get("secret_access_key")
+ security_token = aws_security_credentials.get("security_token")
+
+ additional_headers = additional_headers or {}
+
+ uri = urllib.parse.urlparse(url)
+ # Normalize the URL path. This is needed for the canonical_uri.
+ # os.path.normpath can't be used since it normalizes "/" paths
+ # to "\\" in Windows OS.
+ normalized_uri = urllib.parse.urlparse(
+ urljoin(url, posixpath.normpath(uri.path))
+ )
+ # Validate provided URL.
+ if not uri.hostname or uri.scheme != "https":
+ raise exceptions.InvalidResource("Invalid AWS service URL")
+
+ header_map = _generate_authentication_header_map(
+ host=uri.hostname,
+ canonical_uri=normalized_uri.path or "/",
+ canonical_querystring=_get_canonical_querystring(uri.query),
+ method=method,
+ region=self._region_name,
+ access_key=access_key,
+ secret_key=secret_key,
+ security_token=security_token,
+ request_payload=request_payload,
+ additional_headers=additional_headers,
+ )
+ headers = {
+ "Authorization": header_map.get("authorization_header"),
+ "host": uri.hostname,
+ }
+ # Add x-amz-date if available.
+ if "amz_date" in header_map:
+ headers[_AWS_DATE_HEADER] = header_map.get("amz_date")
+ # Append additional optional headers, eg. X-Amz-Target, Content-Type, etc.
+ for key in additional_headers:
+ headers[key] = additional_headers[key]
+
+ # Add session token if available.
+ if security_token is not None:
+ headers[_AWS_SECURITY_TOKEN_HEADER] = security_token
+
+ signed_request = {"url": url, "method": method, "headers": headers}
+ if request_payload:
+ signed_request["data"] = request_payload
+ return signed_request
+
+
+def _get_canonical_querystring(query):
+ """Generates the canonical query string given a raw query string.
+ Logic is based on
+ https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
+
+ Args:
+ query (str): The raw query string.
+
+ Returns:
+ str: The canonical query string.
+ """
+ # Parse raw query string.
+ querystring = urllib.parse.parse_qs(query)
+ querystring_encoded_map = {}
+ for key in querystring:
+ quote_key = urllib.parse.quote(key, safe="-_.~")
+ # URI encode key.
+ querystring_encoded_map[quote_key] = []
+ for item in querystring[key]:
+ # For each key, URI encode all values for that key.
+ querystring_encoded_map[quote_key].append(
+ urllib.parse.quote(item, safe="-_.~")
+ )
+ # Sort values for each key.
+ querystring_encoded_map[quote_key].sort()
+ # Sort keys.
+ sorted_keys = list(querystring_encoded_map.keys())
+ sorted_keys.sort()
+ # Reconstruct the query string. Preserve keys with multiple values.
+ querystring_encoded_pairs = []
+ for key in sorted_keys:
+ for item in querystring_encoded_map[key]:
+ querystring_encoded_pairs.append("{}={}".format(key, item))
+ return "&".join(querystring_encoded_pairs)
+
+
+def _sign(key, msg):
+ """Creates the HMAC-SHA256 hash of the provided message using the provided
+ key.
+
+ Args:
+ key (str): The HMAC-SHA256 key to use.
+ msg (str): The message to hash.
+
+ Returns:
+ str: The computed hash bytes.
+ """
+ return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
+
+
+def _get_signing_key(key, date_stamp, region_name, service_name):
+ """Calculates the signing key used to calculate the signature for
+ AWS Signature Version 4 based on:
+ https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
+
+ Args:
+ key (str): The AWS secret access key.
+ date_stamp (str): The '%Y%m%d' date format.
+ region_name (str): The AWS region.
+ service_name (str): The AWS service name, eg. sts.
+
+ Returns:
+ str: The signing key bytes.
+ """
+ k_date = _sign(("AWS4" + key).encode("utf-8"), date_stamp)
+ k_region = _sign(k_date, region_name)
+ k_service = _sign(k_region, service_name)
+ k_signing = _sign(k_service, "aws4_request")
+ return k_signing
+
+
+def _generate_authentication_header_map(
+ host,
+ canonical_uri,
+ canonical_querystring,
+ method,
+ region,
+ access_key,
+ secret_key,
+ security_token,
+ request_payload="",
+ additional_headers={},
+):
+ """Generates the authentication header map needed for generating the AWS
+ Signature Version 4 signed request.
+
+ Args:
+ host (str): The AWS service URL hostname.
+ canonical_uri (str): The AWS service URL path name.
+ canonical_querystring (str): The AWS service URL query string.
+ method (str): The HTTP method used to call this API.
+ region (str): The AWS region.
+ access_key (str): The AWS access key ID.
+ secret_key (str): The AWS secret access key.
+ security_token (Optional[str]): The AWS security session token. This is
+ available for temporary sessions.
+ request_payload (Optional[str]): The optional request payload if
+ available.
+ additional_headers (Optional[Mapping[str, str]]): The optional
+ additional headers needed for the requested AWS API.
+
+ Returns:
+ Mapping[str, str]: The AWS authentication header dictionary object.
+ This contains the x-amz-date and authorization header information.
+ """
+ # iam.amazonaws.com host => iam service.
+ # sts.us-east-2.amazonaws.com host => sts service.
+ service_name = host.split(".")[0]
+
+ current_time = _helpers.utcnow()
+ amz_date = current_time.strftime("%Y%m%dT%H%M%SZ")
+ date_stamp = current_time.strftime("%Y%m%d")
+
+ # Change all additional headers to be lower case.
+ full_headers = {}
+ for key in additional_headers:
+ full_headers[key.lower()] = additional_headers[key]
+ # Add AWS session token if available.
+ if security_token is not None:
+ full_headers[_AWS_SECURITY_TOKEN_HEADER] = security_token
+
+ # Required headers
+ full_headers["host"] = host
+ # Do not use generated x-amz-date if the date header is provided.
+ # Previously the date was not fixed with x-amz- and could be provided
+ # manually.
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
+ if "date" not in full_headers:
+ full_headers[_AWS_DATE_HEADER] = amz_date
+
+ # Header keys need to be sorted alphabetically.
+ canonical_headers = ""
+ header_keys = list(full_headers.keys())
+ header_keys.sort()
+ for key in header_keys:
+ canonical_headers = "{}{}:{}\n".format(
+ canonical_headers, key, full_headers[key]
+ )
+ signed_headers = ";".join(header_keys)
+
+ payload_hash = hashlib.sha256((request_payload or "").encode("utf-8")).hexdigest()
+
+ # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
+ canonical_request = "{}\n{}\n{}\n{}\n{}\n{}".format(
+ method,
+ canonical_uri,
+ canonical_querystring,
+ canonical_headers,
+ signed_headers,
+ payload_hash,
+ )
+
+ credential_scope = "{}/{}/{}/{}".format(
+ date_stamp, region, service_name, _AWS_REQUEST_TYPE
+ )
+
+ # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
+ string_to_sign = "{}\n{}\n{}\n{}".format(
+ _AWS_ALGORITHM,
+ amz_date,
+ credential_scope,
+ hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
+ )
+
+ # https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
+ signing_key = _get_signing_key(secret_key, date_stamp, region, service_name)
+ signature = hmac.new(
+ signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
+ ).hexdigest()
+
+ # https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
+ authorization_header = "{} Credential={}/{}, SignedHeaders={}, Signature={}".format(
+ _AWS_ALGORITHM, access_key, credential_scope, signed_headers, signature
+ )
+
+ authentication_header = {"authorization_header": authorization_header}
+ # Do not use generated x-amz-date if the date header is provided.
+ if "date" not in full_headers:
+ authentication_header["amz_date"] = amz_date
+ return authentication_header
+
+
+class Credentials(external_account.Credentials):
+ """AWS external account credentials.
+ This is used to exchange serialized AWS signature v4 signed requests to
+ AWS STS GetCallerIdentity service for Google access tokens.
+ """
+
+ def __init__(
+ self,
+ audience,
+ subject_token_type,
+ token_url,
+ credential_source=None,
+ *args,
+ **kwargs
+ ):
+ """Instantiates an AWS workload external account credentials object.
+
+ Args:
+ audience (str): The STS audience field.
+ subject_token_type (str): The subject token type.
+ token_url (str): The STS endpoint URL.
+ credential_source (Mapping): The credential source dictionary used
+ to provide instructions on how to retrieve external credential
+ to be exchanged for Google access tokens.
+ args (List): Optional positional arguments passed into the underlying :meth:`~external_account.Credentials.__init__` method.
+ kwargs (Mapping): Optional keyword arguments passed into the underlying :meth:`~external_account.Credentials.__init__` method.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If an error is encountered during
+ access token retrieval logic.
+ ValueError: For invalid parameters.
+
+ .. note:: Typically one of the helper constructors
+ :meth:`from_file` or
+ :meth:`from_info` are used instead of calling the constructor directly.
+ """
+ super(Credentials, self).__init__(
+ audience=audience,
+ subject_token_type=subject_token_type,
+ token_url=token_url,
+ credential_source=credential_source,
+ *args,
+ **kwargs
+ )
+ credential_source = credential_source or {}
+ self._environment_id = credential_source.get("environment_id") or ""
+ self._region_url = credential_source.get("region_url")
+ self._security_credentials_url = credential_source.get("url")
+ self._cred_verification_url = credential_source.get(
+ "regional_cred_verification_url"
+ )
+ self._imdsv2_session_token_url = credential_source.get(
+ "imdsv2_session_token_url"
+ )
+ self._region = None
+ self._request_signer = None
+ self._target_resource = audience
+
+ # Get the environment ID. Currently, only one version supported (v1).
+ matches = re.match(r"^(aws)([\d]+)$", self._environment_id)
+ if matches:
+ env_id, env_version = matches.groups()
+ else:
+ env_id, env_version = (None, None)
+
+ if env_id != "aws" or self._cred_verification_url is None:
+ raise exceptions.InvalidResource(
+ "No valid AWS 'credential_source' provided"
+ )
+ elif int(env_version or "") != 1:
+ raise exceptions.InvalidValue(
+ "aws version '{}' is not supported in the current build.".format(
+ env_version
+ )
+ )
+
+ def retrieve_subject_token(self, request):
+ """Retrieves the subject token using the credential_source object.
+ The subject token is a serialized `AWS GetCallerIdentity signed request`_.
+
+ The logic is summarized as:
+
+ Retrieve the AWS region from the AWS_REGION or AWS_DEFAULT_REGION
+ environment variable or from the AWS metadata server availability-zone
+ if not found in the environment variable.
+
+ Check AWS credentials in environment variables. If not found, retrieve
+ from the AWS metadata server security-credentials endpoint.
+
+ When retrieving AWS credentials from the metadata server
+ security-credentials endpoint, the AWS role needs to be determined by
+ calling the security-credentials endpoint without any argument. Then the
+ credentials can be retrieved via: security-credentials/role_name
+
+ Generate the signed request to AWS STS GetCallerIdentity action.
+
+ Inject x-goog-cloud-target-resource into header and serialize the
+ signed request. This will be the subject-token to pass to GCP STS.
+
+ .. _AWS GetCallerIdentity signed request:
+ https://cloud.google.com/iam/docs/access-resources-aws#exchange-token
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ Returns:
+ str: The retrieved subject token.
+ """
+ # Fetch the session token required to make meta data endpoint calls to aws.
+ if (
+ request is not None
+ and self._imdsv2_session_token_url is not None
+ and self._should_use_metadata_server()
+ ):
+ headers = {"X-aws-ec2-metadata-token-ttl-seconds": "300"}
+
+ imdsv2_session_token_response = request(
+ url=self._imdsv2_session_token_url, method="PUT", headers=headers
+ )
+
+ if imdsv2_session_token_response.status != 200:
+ raise exceptions.RefreshError(
+ "Unable to retrieve AWS Session Token",
+ imdsv2_session_token_response.data,
+ )
+
+ imdsv2_session_token = imdsv2_session_token_response.data
+ else:
+ imdsv2_session_token = None
+
+ # Initialize the request signer if not yet initialized after determining
+ # the current AWS region.
+ if self._request_signer is None:
+ self._region = self._get_region(
+ request, self._region_url, imdsv2_session_token
+ )
+ self._request_signer = RequestSigner(self._region)
+
+ # Retrieve the AWS security credentials needed to generate the signed
+ # request.
+ aws_security_credentials = self._get_security_credentials(
+ request, imdsv2_session_token
+ )
+ # Generate the signed request to AWS STS GetCallerIdentity API.
+ # Use the required regional endpoint. Otherwise, the request will fail.
+ request_options = self._request_signer.get_request_options(
+ aws_security_credentials,
+ self._cred_verification_url.replace("{region}", self._region),
+ "POST",
+ )
+ # The GCP STS endpoint expects the headers to be formatted as:
+ # [
+ # {key: 'x-amz-date', value: '...'},
+ # {key: 'Authorization', value: '...'},
+ # ...
+ # ]
+ # And then serialized as:
+ # quote(json.dumps({
+ # url: '...',
+ # method: 'POST',
+ # headers: [{key: 'x-amz-date', value: '...'}, ...]
+ # }))
+ request_headers = request_options.get("headers")
+ # The full, canonical resource name of the workload identity pool
+ # provider, with or without the HTTPS prefix.
+ # Including this header as part of the signature is recommended to
+ # ensure data integrity.
+ request_headers["x-goog-cloud-target-resource"] = self._target_resource
+
+ # Serialize AWS signed request.
+ # Keeping inner keys in sorted order makes testing easier for Python
+ # versions <=3.5 as the stringified JSON string would have a predictable
+ # key order.
+ aws_signed_req = {}
+ aws_signed_req["url"] = request_options.get("url")
+ aws_signed_req["method"] = request_options.get("method")
+ aws_signed_req["headers"] = []
+ # Reformat header to GCP STS expected format.
+ for key in sorted(request_headers.keys()):
+ aws_signed_req["headers"].append(
+ {"key": key, "value": request_headers[key]}
+ )
+
+ return urllib.parse.quote(
+ json.dumps(aws_signed_req, separators=(",", ":"), sort_keys=True)
+ )
+
+ def _get_region(self, request, url, imdsv2_session_token):
+ """Retrieves the current AWS region from either the AWS_REGION or
+ AWS_DEFAULT_REGION environment variable or from the AWS metadata server.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ url (str): The AWS metadata server region URL.
+ imdsv2_session_token (str): The AWS IMDSv2 session token to be added as a
+ header in the requests to AWS metadata endpoint.
+
+ Returns:
+ str: The current AWS region.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If an error occurs while
+ retrieving the AWS region.
+ """
+ # The AWS metadata server is not available in some AWS environments
+ # such as AWS lambda. Instead, it is available via environment
+ # variable.
+ env_aws_region = os.environ.get(environment_vars.AWS_REGION)
+ if env_aws_region is not None:
+ return env_aws_region
+
+ env_aws_region = os.environ.get(environment_vars.AWS_DEFAULT_REGION)
+ if env_aws_region is not None:
+ return env_aws_region
+
+ if not self._region_url:
+ raise exceptions.RefreshError("Unable to determine AWS region")
+
+ headers = None
+ if imdsv2_session_token is not None:
+ headers = {"X-aws-ec2-metadata-token": imdsv2_session_token}
+
+ response = request(url=self._region_url, method="GET", headers=headers)
+
+ # Support both string and bytes type response.data.
+ response_body = (
+ response.data.decode("utf-8")
+ if hasattr(response.data, "decode")
+ else response.data
+ )
+
+ if response.status != 200:
+ raise exceptions.RefreshError(
+ "Unable to retrieve AWS region", response_body
+ )
+
+ # This endpoint will return the region in format: us-east-2b.
+ # Only the us-east-2 part should be used.
+ return response_body[:-1]
+
+ def _get_security_credentials(self, request, imdsv2_session_token):
+ """Retrieves the AWS security credentials required for signing AWS
+ requests from either the AWS security credentials environment variables
+ or from the AWS metadata server.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ imdsv2_session_token (str): The AWS IMDSv2 session token to be added as a
+ header in the requests to AWS metadata endpoint.
+
+ Returns:
+ Mapping[str, str]: The AWS security credentials dictionary object.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If an error occurs while
+ retrieving the AWS security credentials.
+ """
+
+ # Check environment variables for permanent credentials first.
+ # https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+ env_aws_access_key_id = os.environ.get(environment_vars.AWS_ACCESS_KEY_ID)
+ env_aws_secret_access_key = os.environ.get(
+ environment_vars.AWS_SECRET_ACCESS_KEY
+ )
+ # This is normally not available for permanent credentials.
+ env_aws_session_token = os.environ.get(environment_vars.AWS_SESSION_TOKEN)
+ if env_aws_access_key_id and env_aws_secret_access_key:
+ return {
+ "access_key_id": env_aws_access_key_id,
+ "secret_access_key": env_aws_secret_access_key,
+ "security_token": env_aws_session_token,
+ }
+
+ # Get role name.
+ role_name = self._get_metadata_role_name(request, imdsv2_session_token)
+
+ # Get security credentials.
+ credentials = self._get_metadata_security_credentials(
+ request, role_name, imdsv2_session_token
+ )
+
+ return {
+ "access_key_id": credentials.get("AccessKeyId"),
+ "secret_access_key": credentials.get("SecretAccessKey"),
+ "security_token": credentials.get("Token"),
+ }
+
+ def _get_metadata_security_credentials(
+ self, request, role_name, imdsv2_session_token
+ ):
+ """Retrieves the AWS security credentials required for signing AWS
+ requests from the AWS metadata server.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ role_name (str): The AWS role name required by the AWS metadata
+ server security_credentials endpoint in order to return the
+ credentials.
+ imdsv2_session_token (str): The AWS IMDSv2 session token to be added as a
+ header in the requests to AWS metadata endpoint.
+
+ Returns:
+ Mapping[str, str]: The AWS metadata server security credentials
+ response.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If an error occurs while
+ retrieving the AWS security credentials.
+ """
+ headers = {"Content-Type": "application/json"}
+ if imdsv2_session_token is not None:
+ headers["X-aws-ec2-metadata-token"] = imdsv2_session_token
+
+ response = request(
+ url="{}/{}".format(self._security_credentials_url, role_name),
+ method="GET",
+ headers=headers,
+ )
+
+ # support both string and bytes type response.data
+ response_body = (
+ response.data.decode("utf-8")
+ if hasattr(response.data, "decode")
+ else response.data
+ )
+
+ if response.status != http_client.OK:
+ raise exceptions.RefreshError(
+ "Unable to retrieve AWS security credentials", response_body
+ )
+
+ credentials_response = json.loads(response_body)
+
+ return credentials_response
+
+ def _get_metadata_role_name(self, request, imdsv2_session_token):
+ """Retrieves the AWS role currently attached to the current AWS
+ workload by querying the AWS metadata server. This is needed for the
+ AWS metadata server security credentials endpoint in order to retrieve
+ the AWS security credentials needed to sign requests to AWS APIs.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ imdsv2_session_token (str): The AWS IMDSv2 session token to be added as a
+ header in the requests to AWS metadata endpoint.
+
+ Returns:
+ str: The AWS role name.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If an error occurs while
+ retrieving the AWS role name.
+ """
+ if self._security_credentials_url is None:
+ raise exceptions.RefreshError(
+ "Unable to determine the AWS metadata server security credentials endpoint"
+ )
+
+ headers = None
+ if imdsv2_session_token is not None:
+ headers = {"X-aws-ec2-metadata-token": imdsv2_session_token}
+
+ response = request(
+ url=self._security_credentials_url, method="GET", headers=headers
+ )
+
+ # support both string and bytes type response.data
+ response_body = (
+ response.data.decode("utf-8")
+ if hasattr(response.data, "decode")
+ else response.data
+ )
+
+ if response.status != http_client.OK:
+ raise exceptions.RefreshError(
+ "Unable to retrieve AWS role name", response_body
+ )
+
+ return response_body
+
+ def _should_use_metadata_server(self):
+ # The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION.
+ # The metadata server should be used if it cannot be retrieved from one of
+ # these environment variables.
+ if not os.environ.get(environment_vars.AWS_REGION) and not os.environ.get(
+ environment_vars.AWS_DEFAULT_REGION
+ ):
+ return True
+
+ # AWS security credentials can be retrieved from the AWS_ACCESS_KEY_ID
+ # and AWS_SECRET_ACCESS_KEY environment variables. The metadata server
+ # should be used if either of these are not available.
+ if not os.environ.get(environment_vars.AWS_ACCESS_KEY_ID) or not os.environ.get(
+ environment_vars.AWS_SECRET_ACCESS_KEY
+ ):
+ return True
+
+ return False
+
+ def _create_default_metrics_options(self):
+ metrics_options = super(Credentials, self)._create_default_metrics_options()
+ metrics_options["source"] = "aws"
+ return metrics_options
+
+ @classmethod
+ def from_info(cls, info, **kwargs):
+ """Creates an AWS Credentials instance from parsed external account info.
+
+ Args:
+ info (Mapping[str, str]): The AWS external account info in Google
+ format.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.aws.Credentials: The constructed credentials.
+
+ Raises:
+ ValueError: For invalid parameters.
+ """
+ return super(Credentials, cls).from_info(info, **kwargs)
+
+ @classmethod
+ def from_file(cls, filename, **kwargs):
+ """Creates an AWS Credentials instance from an external account json file.
+
+ Args:
+ filename (str): The path to the AWS external account json file.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.aws.Credentials: The constructed credentials.
+ """
+ return super(Credentials, cls).from_file(filename, **kwargs)
diff --git a/contrib/python/google-auth/py3/google/auth/compute_engine/__init__.py b/contrib/python/google-auth/py3/google/auth/compute_engine/__init__.py
new file mode 100644
index 0000000000..5c84234e93
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/compute_engine/__init__.py
@@ -0,0 +1,21 @@
+# Copyright 2016 Google LLC
+#
+# 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 Compute Engine authentication."""
+
+from google.auth.compute_engine.credentials import Credentials
+from google.auth.compute_engine.credentials import IDTokenCredentials
+
+
+__all__ = ["Credentials", "IDTokenCredentials"]
diff --git a/contrib/python/google-auth/py3/google/auth/compute_engine/_metadata.py b/contrib/python/google-auth/py3/google/auth/compute_engine/_metadata.py
new file mode 100644
index 0000000000..04abe178f5
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/compute_engine/_metadata.py
@@ -0,0 +1,322 @@
+# Copyright 2016 Google LLC
+#
+# 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 for more details.
+"""
+
+import datetime
+import http.client as http_client
+import json
+import logging
+import os
+from urllib.parse import urljoin
+
+from google.auth import _helpers
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import metrics
+
+_LOGGER = logging.getLogger(__name__)
+
+# Environment variable GCE_METADATA_HOST is originally named
+# GCE_METADATA_ROOT. For compatiblity reasons, here it checks
+# the new variable first; if not set, the system falls back
+# to the old variable.
+_GCE_METADATA_HOST = os.getenv(environment_vars.GCE_METADATA_HOST, None)
+if not _GCE_METADATA_HOST:
+ _GCE_METADATA_HOST = os.getenv(
+ environment_vars.GCE_METADATA_ROOT, "metadata.google.internal"
+ )
+_METADATA_ROOT = "http://{}/computeMetadata/v1/".format(_GCE_METADATA_HOST)
+
+# This is used to ping the metadata server, it avoids the cost of a DNS
+# lookup.
+_METADATA_IP_ROOT = "http://{}".format(
+ os.getenv(environment_vars.GCE_METADATA_IP, "169.254.169.254")
+)
+_METADATA_FLAVOR_HEADER = "metadata-flavor"
+_METADATA_FLAVOR_VALUE = "Google"
+_METADATA_HEADERS = {_METADATA_FLAVOR_HEADER: _METADATA_FLAVOR_VALUE}
+
+# Timeout in seconds to wait for the GCE metadata server when detecting the
+# GCE environment.
+try:
+ _METADATA_DEFAULT_TIMEOUT = int(os.getenv("GCE_METADATA_TIMEOUT", 3))
+except ValueError: # pragma: NO COVER
+ _METADATA_DEFAULT_TIMEOUT = 3
+
+# Detect GCE Residency
+_GOOGLE = "Google"
+_GCE_PRODUCT_NAME_FILE = "/sys/class/dmi/id/product_name"
+
+
+def is_on_gce(request):
+ """Checks to see if the code runs on Google Compute Engine
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+
+ Returns:
+ bool: True if the code runs on Google Compute Engine, False otherwise.
+ """
+ if ping(request):
+ return True
+
+ if os.name == "nt":
+ # TODO: implement GCE residency detection on Windows
+ return False
+
+ # Detect GCE residency on Linux
+ return detect_gce_residency_linux()
+
+
+def detect_gce_residency_linux():
+ """Detect Google Compute Engine residency by smbios check on Linux
+
+ Returns:
+ bool: True if the GCE product name file is detected, False otherwise.
+ """
+ try:
+ with open(_GCE_PRODUCT_NAME_FILE, "r") as file_obj:
+ content = file_obj.read().strip()
+
+ except Exception:
+ return False
+
+ return content.startswith(_GOOGLE)
+
+
+def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
+ """Checks to see if the metadata server is available.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ timeout (int): How long to wait for the metadata server to respond.
+ retry_count (int): How many times to attempt connecting to metadata
+ server using above timeout.
+
+ Returns:
+ bool: True if the metadata server is reachable, False otherwise.
+ """
+ # 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".
+ retries = 0
+ headers = _METADATA_HEADERS.copy()
+ headers[metrics.API_CLIENT_HEADER] = metrics.mds_ping()
+
+ while retries < retry_count:
+ try:
+ response = request(
+ url=_METADATA_IP_ROOT, method="GET", headers=headers, timeout=timeout
+ )
+
+ metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER)
+ return (
+ response.status == http_client.OK
+ and metadata_flavor == _METADATA_FLAVOR_VALUE
+ )
+
+ except exceptions.TransportError as e:
+ _LOGGER.warning(
+ "Compute Engine Metadata server unavailable on "
+ "attempt %s of %s. Reason: %s",
+ retries + 1,
+ retry_count,
+ e,
+ )
+ retries += 1
+
+ return False
+
+
+def get(
+ request,
+ path,
+ root=_METADATA_ROOT,
+ params=None,
+ recursive=False,
+ retry_count=5,
+ headers=None,
+):
+ """Fetch a resource from the metadata server.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ path (str): The resource to retrieve. For example,
+ ``'instance/service-accounts/default'``.
+ root (str): The full path to the metadata server root.
+ params (Optional[Mapping[str, str]]): A mapping of query parameter
+ keys to values.
+ recursive (bool): Whether to do a recursive query of metadata. See
+ https://cloud.google.com/compute/docs/metadata#aggcontents for more
+ details.
+ retry_count (int): How many times to attempt connecting to metadata
+ server using above timeout.
+ headers (Optional[Mapping[str, str]]): Headers for the request.
+
+ Returns:
+ Union[Mapping, str]: If the metadata server returns JSON, a mapping of
+ the decoded JSON is return. Otherwise, the response content is
+ returned as a string.
+
+ Raises:
+ google.auth.exceptions.TransportError: if an error occurred while
+ retrieving metadata.
+ """
+ base_url = urljoin(root, path)
+ query_params = {} if params is None else params
+
+ headers_to_use = _METADATA_HEADERS.copy()
+ if headers:
+ headers_to_use.update(headers)
+
+ if recursive:
+ query_params["recursive"] = "true"
+
+ url = _helpers.update_query(base_url, query_params)
+
+ retries = 0
+ while retries < retry_count:
+ try:
+ response = request(url=url, method="GET", headers=headers_to_use)
+ break
+
+ except exceptions.TransportError as e:
+ _LOGGER.warning(
+ "Compute Engine Metadata server unavailable on "
+ "attempt %s of %s. Reason: %s",
+ retries + 1,
+ retry_count,
+ e,
+ )
+ retries += 1
+ else:
+ raise exceptions.TransportError(
+ "Failed to retrieve {} from the Google Compute Engine "
+ "metadata service. Compute Engine Metadata server unavailable".format(url)
+ )
+
+ if response.status == http_client.OK:
+ content = _helpers.from_bytes(response.data)
+ if response.headers["content-type"] == "application/json":
+ try:
+ return json.loads(content)
+ except ValueError as caught_exc:
+ new_exc = exceptions.TransportError(
+ "Received invalid JSON from the Google Compute Engine "
+ "metadata service: {:.20}".format(content)
+ )
+ raise new_exc from caught_exc
+ else:
+ return content
+ else:
+ raise exceptions.TransportError(
+ "Failed to retrieve {} from the Google Compute Engine "
+ "metadata service. Status: {} Response:\n{}".format(
+ url, response.status, response.data
+ ),
+ response,
+ )
+
+
+def get_project_id(request):
+ """Get the Google Cloud Project ID from the metadata server.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+
+ Returns:
+ str: The project ID
+
+ Raises:
+ google.auth.exceptions.TransportError: if an error occurred while
+ retrieving metadata.
+ """
+ return get(request, "project/project-id")
+
+
+def get_service_account_info(request, service_account="default"):
+ """Get information about a service account from the metadata server.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ service_account (str): The string 'default' or a service account email
+ address. The determines which service account for which to acquire
+ information.
+
+ Returns:
+ Mapping: The service account's information, for example::
+
+ {
+ 'email': '...',
+ 'scopes': ['scope', ...],
+ 'aliases': ['default', '...']
+ }
+
+ Raises:
+ google.auth.exceptions.TransportError: if an error occurred while
+ retrieving metadata.
+ """
+ path = "instance/service-accounts/{0}/".format(service_account)
+ # See https://cloud.google.com/compute/docs/metadata#aggcontents
+ # for more on the use of 'recursive'.
+ return get(request, path, params={"recursive": "true"})
+
+
+def get_service_account_token(request, service_account="default", scopes=None):
+ """Get the OAuth 2.0 access token for a service account.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ service_account (str): The string 'default' or a service account email
+ address. The determines which service account for which to acquire
+ an access token.
+ scopes (Optional[Union[str, List[str]]]): Optional string or list of
+ strings with auth scopes.
+ Returns:
+ Tuple[str, datetime]: The access token and its expiration.
+
+ Raises:
+ google.auth.exceptions.TransportError: if an error occurred while
+ retrieving metadata.
+ """
+ if scopes:
+ if not isinstance(scopes, str):
+ scopes = ",".join(scopes)
+ params = {"scopes": scopes}
+ else:
+ params = None
+
+ metrics_header = {
+ metrics.API_CLIENT_HEADER: metrics.token_request_access_token_mds()
+ }
+
+ path = "instance/service-accounts/{0}/token".format(service_account)
+ token_json = get(request, path, params=params, headers=metrics_header)
+ token_expiry = _helpers.utcnow() + datetime.timedelta(
+ seconds=token_json["expires_in"]
+ )
+ return token_json["access_token"], token_expiry
diff --git a/contrib/python/google-auth/py3/google/auth/compute_engine/credentials.py b/contrib/python/google-auth/py3/google/auth/compute_engine/credentials.py
new file mode 100644
index 0000000000..7ae673880f
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/compute_engine/credentials.py
@@ -0,0 +1,445 @@
+# Copyright 2016 Google LLC
+#
+# 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 Compute Engine credentials.
+
+This module provides authentication for an application running on Google
+Compute Engine using the Compute Engine metadata server.
+
+"""
+
+import datetime
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.auth import iam
+from google.auth import jwt
+from google.auth import metrics
+from google.auth.compute_engine import _metadata
+from google.oauth2 import _client
+
+
+class Credentials(credentials.Scoped, credentials.CredentialsWithQuotaProject):
+ """Compute Engine Credentials.
+
+ These credentials use the Google Compute Engine metadata server to obtain
+ OAuth 2.0 access tokens associated with the instance's service account,
+ and are also used for Cloud Run, Flex and App Engine (except for the Python
+ 2.7 runtime, which is supported only on older versions of this library).
+
+ For more information about Compute Engine authentication, including how
+ to configure scopes, see the `Compute Engine authentication
+ documentation`_.
+
+ .. note:: On Compute Engine the metadata server ignores requested scopes.
+ On Cloud Run, Flex and App Engine the server honours requested scopes.
+
+ .. _Compute Engine authentication documentation:
+ https://cloud.google.com/compute/docs/authentication#using
+ """
+
+ def __init__(
+ self,
+ service_account_email="default",
+ quota_project_id=None,
+ scopes=None,
+ default_scopes=None,
+ ):
+ """
+ Args:
+ service_account_email (str): The service account email to use, or
+ 'default'. A Compute Engine instance may have multiple service
+ accounts.
+ quota_project_id (Optional[str]): The project ID used for quota and
+ billing.
+ scopes (Optional[Sequence[str]]): The list of scopes for the credentials.
+ default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+ Google client library. Use 'scopes' for user-defined scopes.
+ """
+ super(Credentials, self).__init__()
+ self._service_account_email = service_account_email
+ self._quota_project_id = quota_project_id
+ self._scopes = scopes
+ self._default_scopes = default_scopes
+
+ def _retrieve_info(self, request):
+ """Retrieve information about the service account.
+
+ Updates the scopes and retrieves the full service account email.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ """
+ info = _metadata.get_service_account_info(
+ request, service_account=self._service_account_email
+ )
+
+ self._service_account_email = info["email"]
+
+ # Don't override scopes requested by the user.
+ if self._scopes is None:
+ self._scopes = info["scopes"]
+
+ def _metric_header_for_usage(self):
+ return metrics.CRED_TYPE_SA_MDS
+
+ def refresh(self, request):
+ """Refresh the access token and scopes.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the Compute Engine metadata
+ service can't be reached if if the instance has not
+ credentials.
+ """
+ scopes = self._scopes if self._scopes is not None else self._default_scopes
+ try:
+ self._retrieve_info(request)
+ self.token, self.expiry = _metadata.get_service_account_token(
+ request, service_account=self._service_account_email, scopes=scopes
+ )
+ except exceptions.TransportError as caught_exc:
+ new_exc = exceptions.RefreshError(caught_exc)
+ raise new_exc from caught_exc
+
+ @property
+ def service_account_email(self):
+ """The service account email.
+
+ .. note:: This is not guaranteed to be set until :meth:`refresh` has been
+ called.
+ """
+ return self._service_account_email
+
+ @property
+ def requires_scopes(self):
+ return not self._scopes
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ return self.__class__(
+ service_account_email=self._service_account_email,
+ quota_project_id=quota_project_id,
+ scopes=self._scopes,
+ )
+
+ @_helpers.copy_docstring(credentials.Scoped)
+ def with_scopes(self, scopes, default_scopes=None):
+ # Compute Engine credentials can not be scoped (the metadata service
+ # ignores the scopes parameter). App Engine, Cloud Run and Flex support
+ # requesting scopes.
+ return self.__class__(
+ scopes=scopes,
+ default_scopes=default_scopes,
+ service_account_email=self._service_account_email,
+ quota_project_id=self._quota_project_id,
+ )
+
+
+_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
+_DEFAULT_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token"
+
+
+class IDTokenCredentials(
+ credentials.CredentialsWithQuotaProject,
+ credentials.Signing,
+ credentials.CredentialsWithTokenUri,
+):
+ """Open ID Connect ID Token-based service account credentials.
+
+ These credentials relies on the default service account of a GCE instance.
+
+ ID token can be requested from `GCE metadata server identity endpoint`_, IAM
+ token endpoint or other token endpoints you specify. If metadata server
+ identity endpoint is not used, the GCE instance must have been started with
+ a service account that has access to the IAM Cloud API.
+
+ .. _GCE metadata server identity endpoint:
+ https://cloud.google.com/compute/docs/instances/verifying-instance-identity
+ """
+
+ def __init__(
+ self,
+ request,
+ target_audience,
+ token_uri=None,
+ additional_claims=None,
+ service_account_email=None,
+ signer=None,
+ use_metadata_identity_endpoint=False,
+ quota_project_id=None,
+ ):
+ """
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ target_audience (str): The intended audience for these credentials,
+ used when requesting the ID Token. The ID Token's ``aud`` claim
+ will be set to this string.
+ token_uri (str): The OAuth 2.0 Token URI.
+ additional_claims (Mapping[str, str]): Any additional claims for
+ the JWT assertion used in the authorization grant.
+ service_account_email (str): Optional explicit service account to
+ use to sign JWT tokens.
+ By default, this is the default GCE service account.
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ In case the signer is specified, the request argument will be
+ ignored.
+ use_metadata_identity_endpoint (bool): Whether to use GCE metadata
+ identity endpoint. For backward compatibility the default value
+ is False. If set to True, ``token_uri``, ``additional_claims``,
+ ``service_account_email``, ``signer`` argument should not be set;
+ otherwise ValueError will be raised.
+ quota_project_id (Optional[str]): The project ID used for quota and
+ billing.
+
+ Raises:
+ ValueError:
+ If ``use_metadata_identity_endpoint`` is set to True, and one of
+ ``token_uri``, ``additional_claims``, ``service_account_email``,
+ ``signer`` arguments is set.
+ """
+ super(IDTokenCredentials, self).__init__()
+
+ self._quota_project_id = quota_project_id
+ self._use_metadata_identity_endpoint = use_metadata_identity_endpoint
+ self._target_audience = target_audience
+
+ if use_metadata_identity_endpoint:
+ if token_uri or additional_claims or service_account_email or signer:
+ raise exceptions.MalformedError(
+ "If use_metadata_identity_endpoint is set, token_uri, "
+ "additional_claims, service_account_email, signer arguments"
+ " must not be set"
+ )
+ self._token_uri = None
+ self._additional_claims = None
+ self._signer = None
+
+ if service_account_email is None:
+ sa_info = _metadata.get_service_account_info(request)
+ self._service_account_email = sa_info["email"]
+ else:
+ self._service_account_email = service_account_email
+
+ if not use_metadata_identity_endpoint:
+ if signer is None:
+ signer = iam.Signer(
+ request=request,
+ credentials=Credentials(),
+ service_account_email=self._service_account_email,
+ )
+ self._signer = signer
+ self._token_uri = token_uri or _DEFAULT_TOKEN_URI
+
+ if additional_claims is not None:
+ self._additional_claims = additional_claims
+ else:
+ self._additional_claims = {}
+
+ def with_target_audience(self, target_audience):
+ """Create a copy of these credentials with the specified target
+ audience.
+ Args:
+ target_audience (str): The intended audience for these credentials,
+ used when requesting the ID Token.
+ Returns:
+ google.auth.service_account.IDTokenCredentials: A new credentials
+ instance.
+ """
+ # since the signer is already instantiated,
+ # the request is not needed
+ if self._use_metadata_identity_endpoint:
+ return self.__class__(
+ None,
+ target_audience=target_audience,
+ use_metadata_identity_endpoint=True,
+ quota_project_id=self._quota_project_id,
+ )
+ else:
+ return self.__class__(
+ None,
+ service_account_email=self._service_account_email,
+ token_uri=self._token_uri,
+ target_audience=target_audience,
+ additional_claims=self._additional_claims.copy(),
+ signer=self.signer,
+ use_metadata_identity_endpoint=False,
+ quota_project_id=self._quota_project_id,
+ )
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+
+ # since the signer is already instantiated,
+ # the request is not needed
+ if self._use_metadata_identity_endpoint:
+ return self.__class__(
+ None,
+ target_audience=self._target_audience,
+ use_metadata_identity_endpoint=True,
+ quota_project_id=quota_project_id,
+ )
+ else:
+ return self.__class__(
+ None,
+ service_account_email=self._service_account_email,
+ token_uri=self._token_uri,
+ target_audience=self._target_audience,
+ additional_claims=self._additional_claims.copy(),
+ signer=self.signer,
+ use_metadata_identity_endpoint=False,
+ quota_project_id=quota_project_id,
+ )
+
+ @_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
+ def with_token_uri(self, token_uri):
+
+ # since the signer is already instantiated,
+ # the request is not needed
+ if self._use_metadata_identity_endpoint:
+ raise exceptions.MalformedError(
+ "If use_metadata_identity_endpoint is set, token_uri" " must not be set"
+ )
+ else:
+ return self.__class__(
+ None,
+ service_account_email=self._service_account_email,
+ token_uri=token_uri,
+ target_audience=self._target_audience,
+ additional_claims=self._additional_claims.copy(),
+ signer=self.signer,
+ use_metadata_identity_endpoint=False,
+ quota_project_id=self.quota_project_id,
+ )
+
+ def _make_authorization_grant_assertion(self):
+ """Create the OAuth 2.0 assertion.
+ This assertion is used during the OAuth 2.0 grant to acquire an
+ ID token.
+ Returns:
+ bytes: The authorization grant assertion.
+ """
+ now = _helpers.utcnow()
+ lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
+ expiry = now + lifetime
+
+ payload = {
+ "iat": _helpers.datetime_to_secs(now),
+ "exp": _helpers.datetime_to_secs(expiry),
+ # The issuer must be the service account email.
+ "iss": self.service_account_email,
+ # The audience must be the auth token endpoint's URI
+ "aud": self._token_uri,
+ # The target audience specifies which service the ID token is
+ # intended for.
+ "target_audience": self._target_audience,
+ }
+
+ payload.update(self._additional_claims)
+
+ token = jwt.encode(self._signer, payload)
+
+ return token
+
+ def _call_metadata_identity_endpoint(self, request):
+ """Request ID token from metadata identity endpoint.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+
+ Returns:
+ Tuple[str, datetime.datetime]: The ID token and the expiry of the ID token.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the Compute Engine metadata
+ service can't be reached or if the instance has no credentials.
+ ValueError: If extracting expiry from the obtained ID token fails.
+ """
+ try:
+ path = "instance/service-accounts/default/identity"
+ params = {"audience": self._target_audience, "format": "full"}
+ metrics_header = {
+ metrics.API_CLIENT_HEADER: metrics.token_request_id_token_mds()
+ }
+ id_token = _metadata.get(
+ request, path, params=params, headers=metrics_header
+ )
+ except exceptions.TransportError as caught_exc:
+ new_exc = exceptions.RefreshError(caught_exc)
+ raise new_exc from caught_exc
+
+ _, payload, _, _ = jwt._unverified_decode(id_token)
+ return id_token, datetime.datetime.utcfromtimestamp(payload["exp"])
+
+ def refresh(self, request):
+ """Refreshes the ID token.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the credentials could
+ not be refreshed.
+ ValueError: If extracting expiry from the obtained ID token fails.
+ """
+ if self._use_metadata_identity_endpoint:
+ self.token, self.expiry = self._call_metadata_identity_endpoint(request)
+ else:
+ assertion = self._make_authorization_grant_assertion()
+ access_token, expiry, _ = _client.id_token_jwt_grant(
+ request, self._token_uri, assertion
+ )
+ self.token = access_token
+ self.expiry = expiry
+
+ @property # type: ignore
+ @_helpers.copy_docstring(credentials.Signing)
+ def signer(self):
+ return self._signer
+
+ def sign_bytes(self, message):
+ """Signs the given message.
+
+ Args:
+ message (bytes): The message to sign.
+
+ Returns:
+ bytes: The message's cryptographic signature.
+
+ Raises:
+ ValueError:
+ Signer is not available if metadata identity endpoint is used.
+ """
+ if self._use_metadata_identity_endpoint:
+ raise exceptions.InvalidOperation(
+ "Signer is not available if metadata identity endpoint is used"
+ )
+ return self._signer.sign(message)
+
+ @property
+ def service_account_email(self):
+ """The service account email."""
+ return self._service_account_email
+
+ @property
+ def signer_email(self):
+ return self._service_account_email
diff --git a/contrib/python/google-auth/py3/google/auth/credentials.py b/contrib/python/google-auth/py3/google/auth/credentials.py
new file mode 100644
index 0000000000..80a2a5c0b4
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/credentials.py
@@ -0,0 +1,410 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+
+"""Interfaces for credentials."""
+
+import abc
+import os
+
+from google.auth import _helpers, environment_vars
+from google.auth import exceptions
+from google.auth import metrics
+
+
+class Credentials(metaclass=abc.ABCMeta):
+ """Base class for all credentials.
+
+ All credentials have a :attr:`token` that is used for authentication and
+ may also optionally set an :attr:`expiry` to indicate when the token will
+ no longer be valid.
+
+ Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
+ Credentials can do this automatically before the first HTTP request in
+ :meth:`before_request`.
+
+ Although the token and expiration will change as the credentials are
+ :meth:`refreshed <refresh>` and used, credentials should be considered
+ immutable. Various credentials will accept configuration such as private
+ keys, scopes, and other options. These options are not changeable after
+ construction. Some classes will provide mechanisms to copy the credentials
+ with modifications such as :meth:`ScopedCredentials.with_scopes`.
+ """
+
+ def __init__(self):
+ self.token = None
+ """str: The bearer token that can be used in HTTP headers to make
+ authenticated requests."""
+ self.expiry = None
+ """Optional[datetime]: When the token expires and is no longer valid.
+ If this is None, the token is assumed to never expire."""
+ self._quota_project_id = None
+ """Optional[str]: Project to use for quota and billing purposes."""
+ self._trust_boundary = None
+ """Optional[str]: Encoded string representation of credentials trust
+ boundary."""
+ self._universe_domain = "googleapis.com"
+ """Optional[str]: The universe domain value, default is googleapis.com
+ """
+
+ @property
+ def expired(self):
+ """Checks if the credentials are expired.
+
+ Note that credentials can be invalid but not expired because
+ Credentials with :attr:`expiry` set to None is considered to never
+ expire.
+ """
+ if not self.expiry:
+ return False
+
+ # Remove some threshold from expiry to err on the side of reporting
+ # expiration early so that we avoid the 401-refresh-retry loop.
+ skewed_expiry = self.expiry - _helpers.REFRESH_THRESHOLD
+ return _helpers.utcnow() >= skewed_expiry
+
+ @property
+ def valid(self):
+ """Checks the validity of the credentials.
+
+ This is True if the credentials have a :attr:`token` and the token
+ is not :attr:`expired`.
+ """
+ return self.token is not None and not self.expired
+
+ @property
+ def quota_project_id(self):
+ """Project to use for quota and billing purposes."""
+ return self._quota_project_id
+
+ @property
+ def universe_domain(self):
+ """The universe domain value."""
+ return self._universe_domain
+
+ @abc.abstractmethod
+ def refresh(self, request):
+ """Refreshes the access token.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the credentials could
+ not be refreshed.
+ """
+ # pylint: disable=missing-raises-doc
+ # (pylint doesn't recognize that this is abstract)
+ raise NotImplementedError("Refresh must be implemented")
+
+ def _metric_header_for_usage(self):
+ """The x-goog-api-client header for token usage metric.
+
+ This header will be added to the API service requests in before_request
+ method. For example, "cred-type/sa-jwt" means service account self
+ signed jwt access token is used in the API service request
+ authorization header. Children credentials classes need to override
+ this method to provide the header value, if the token usage metric is
+ needed.
+
+ Returns:
+ str: The x-goog-api-client header value.
+ """
+ return None
+
+ def apply(self, headers, token=None):
+ """Apply the token to the authentication header.
+
+ Args:
+ headers (Mapping): The HTTP request headers.
+ token (Optional[str]): If specified, overrides the current access
+ token.
+ """
+ headers["authorization"] = "Bearer {}".format(
+ _helpers.from_bytes(token or self.token)
+ )
+ if self._trust_boundary is not None:
+ headers["x-identity-trust-boundary"] = self._trust_boundary
+ if self.quota_project_id:
+ headers["x-goog-user-project"] = self.quota_project_id
+
+ def before_request(self, request, method, url, headers):
+ """Performs credential-specific before request logic.
+
+ Refreshes the credentials if necessary, then calls :meth:`apply` to
+ apply the token to the authentication header.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ method (str): The request's HTTP method or the RPC method being
+ invoked.
+ url (str): The request's URI or the RPC service's URI.
+ headers (Mapping): The request's headers.
+ """
+ # pylint: disable=unused-argument
+ # (Subclasses may use these arguments to ascertain information about
+ # the http request.)
+ if not self.valid:
+ self.refresh(request)
+ metrics.add_metric_header(headers, self._metric_header_for_usage())
+ self.apply(headers)
+
+
+class CredentialsWithQuotaProject(Credentials):
+ """Abstract base for credentials supporting ``with_quota_project`` factory"""
+
+ def with_quota_project(self, quota_project_id):
+ """Returns a copy of these credentials with a modified quota project.
+
+ Args:
+ quota_project_id (str): The project to use for quota and
+ billing purposes
+
+ Returns:
+ google.oauth2.credentials.Credentials: A new credentials instance.
+ """
+ raise NotImplementedError("This credential does not support quota project.")
+
+ def with_quota_project_from_environment(self):
+ quota_from_env = os.environ.get(environment_vars.GOOGLE_CLOUD_QUOTA_PROJECT)
+ if quota_from_env:
+ return self.with_quota_project(quota_from_env)
+ return self
+
+
+class CredentialsWithTokenUri(Credentials):
+ """Abstract base for credentials supporting ``with_token_uri`` factory"""
+
+ def with_token_uri(self, token_uri):
+ """Returns a copy of these credentials with a modified token uri.
+
+ Args:
+ token_uri (str): The uri to use for fetching/exchanging tokens
+
+ Returns:
+ google.oauth2.credentials.Credentials: A new credentials instance.
+ """
+ raise NotImplementedError("This credential does not use token uri.")
+
+
+class AnonymousCredentials(Credentials):
+ """Credentials that do not provide any authentication information.
+
+ These are useful in the case of services that support anonymous access or
+ local service emulators that do not use credentials.
+ """
+
+ @property
+ def expired(self):
+ """Returns `False`, anonymous credentials never expire."""
+ return False
+
+ @property
+ def valid(self):
+ """Returns `True`, anonymous credentials are always valid."""
+ return True
+
+ def refresh(self, request):
+ """Raises :class:``InvalidOperation``, anonymous credentials cannot be
+ refreshed."""
+ raise exceptions.InvalidOperation("Anonymous credentials cannot be refreshed.")
+
+ def apply(self, headers, token=None):
+ """Anonymous credentials do nothing to the request.
+
+ The optional ``token`` argument is not supported.
+
+ Raises:
+ google.auth.exceptions.InvalidValue: If a token was specified.
+ """
+ if token is not None:
+ raise exceptions.InvalidValue("Anonymous credentials don't support tokens.")
+
+ def before_request(self, request, method, url, headers):
+ """Anonymous credentials do nothing to the request."""
+
+
+class ReadOnlyScoped(metaclass=abc.ABCMeta):
+ """Interface for credentials whose scopes can be queried.
+
+ OAuth 2.0-based credentials allow limiting access using scopes as described
+ in `RFC6749 Section 3.3`_.
+ If a credential class implements this interface then the credentials either
+ use scopes in their implementation.
+
+ Some credentials require scopes in order to obtain a token. You can check
+ if scoping is necessary with :attr:`requires_scopes`::
+
+ if credentials.requires_scopes:
+ # Scoping is required.
+ credentials = credentials.with_scopes(scopes=['one', 'two'])
+
+ Credentials that require scopes must either be constructed with scopes::
+
+ credentials = SomeScopedCredentials(scopes=['one', 'two'])
+
+ Or must copy an existing instance using :meth:`with_scopes`::
+
+ scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
+
+ Some credentials have scopes but do not allow or require scopes to be set,
+ these credentials can be used as-is.
+
+ .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
+ """
+
+ def __init__(self):
+ super(ReadOnlyScoped, self).__init__()
+ self._scopes = None
+ self._default_scopes = None
+
+ @property
+ def scopes(self):
+ """Sequence[str]: the credentials' current set of scopes."""
+ return self._scopes
+
+ @property
+ def default_scopes(self):
+ """Sequence[str]: the credentials' current set of default scopes."""
+ return self._default_scopes
+
+ @abc.abstractproperty
+ def requires_scopes(self):
+ """True if these credentials require scopes to obtain an access token.
+ """
+ return False
+
+ def has_scopes(self, scopes):
+ """Checks if the credentials have the given scopes.
+
+ .. warning: This method is not guaranteed to be accurate if the
+ credentials are :attr:`~Credentials.invalid`.
+
+ Args:
+ scopes (Sequence[str]): The list of scopes to check.
+
+ Returns:
+ bool: True if the credentials have the given scopes.
+ """
+ credential_scopes = (
+ self._scopes if self._scopes is not None else self._default_scopes
+ )
+ return set(scopes).issubset(set(credential_scopes or []))
+
+
+class Scoped(ReadOnlyScoped):
+ """Interface for credentials whose scopes can be replaced while copying.
+
+ OAuth 2.0-based credentials allow limiting access using scopes as described
+ in `RFC6749 Section 3.3`_.
+ If a credential class implements this interface then the credentials either
+ use scopes in their implementation.
+
+ Some credentials require scopes in order to obtain a token. You can check
+ if scoping is necessary with :attr:`requires_scopes`::
+
+ if credentials.requires_scopes:
+ # Scoping is required.
+ credentials = credentials.create_scoped(['one', 'two'])
+
+ Credentials that require scopes must either be constructed with scopes::
+
+ credentials = SomeScopedCredentials(scopes=['one', 'two'])
+
+ Or must copy an existing instance using :meth:`with_scopes`::
+
+ scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
+
+ Some credentials have scopes but do not allow or require scopes to be set,
+ these credentials can be used as-is.
+
+ .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
+ """
+
+ @abc.abstractmethod
+ def with_scopes(self, scopes, default_scopes=None):
+ """Create a copy of these credentials with the specified scopes.
+
+ Args:
+ scopes (Sequence[str]): The list of scopes to attach to the
+ current credentials.
+
+ Raises:
+ NotImplementedError: If the credentials' scopes can not be changed.
+ This can be avoided by checking :attr:`requires_scopes` before
+ calling this method.
+ """
+ raise NotImplementedError("This class does not require scoping.")
+
+
+def with_scopes_if_required(credentials, scopes, default_scopes=None):
+ """Creates a copy of the credentials with scopes if scoping is required.
+
+ This helper function is useful when you do not know (or care to know) the
+ specific type of credentials you are using (such as when you use
+ :func:`google.auth.default`). This function will call
+ :meth:`Scoped.with_scopes` if the credentials are scoped credentials and if
+ the credentials require scoping. Otherwise, it will return the credentials
+ as-is.
+
+ Args:
+ credentials (google.auth.credentials.Credentials): The credentials to
+ scope if necessary.
+ scopes (Sequence[str]): The list of scopes to use.
+ default_scopes (Sequence[str]): Default scopes passed by a
+ Google client library. Use 'scopes' for user-defined scopes.
+
+ Returns:
+ google.auth.credentials.Credentials: Either a new set of scoped
+ credentials, or the passed in credentials instance if no scoping
+ was required.
+ """
+ if isinstance(credentials, Scoped) and credentials.requires_scopes:
+ return credentials.with_scopes(scopes, default_scopes=default_scopes)
+ else:
+ return credentials
+
+
+class Signing(metaclass=abc.ABCMeta):
+ """Interface for credentials that can cryptographically sign messages."""
+
+ @abc.abstractmethod
+ def sign_bytes(self, message):
+ """Signs the given message.
+
+ Args:
+ message (bytes): The message to sign.
+
+ Returns:
+ bytes: The message's cryptographic signature.
+ """
+ # pylint: disable=missing-raises-doc,redundant-returns-doc
+ # (pylint doesn't recognize that this is abstract)
+ raise NotImplementedError("Sign bytes must be implemented.")
+
+ @abc.abstractproperty
+ def signer_email(self):
+ """Optional[str]: An email address that identifies the signer."""
+ # pylint: disable=missing-raises-doc
+ # (pylint doesn't recognize that this is abstract)
+ raise NotImplementedError("Signer email must be implemented.")
+
+ @abc.abstractproperty
+ def signer(self):
+ """google.auth.crypt.Signer: The signer used to sign bytes."""
+ # pylint: disable=missing-raises-doc
+ # (pylint doesn't recognize that this is abstract)
+ raise NotImplementedError("Signer must be implemented.")
diff --git a/contrib/python/google-auth/py3/google/auth/crypt/__init__.py b/contrib/python/google-auth/py3/google/auth/crypt/__init__.py
new file mode 100644
index 0000000000..6d147e7061
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/crypt/__init__.py
@@ -0,0 +1,98 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Cryptography helpers for verifying and signing messages.
+
+The simplest way to verify signatures is using :func:`verify_signature`::
+
+ cert = open('certs.pem').read()
+ valid = crypt.verify_signature(message, signature, cert)
+
+If you're going to verify many messages with the same certificate, you can use
+:class:`RSAVerifier`::
+
+ cert = open('certs.pem').read()
+ verifier = crypt.RSAVerifier.from_string(cert)
+ valid = verifier.verify(message, signature)
+
+To sign messages use :class:`RSASigner` with a private key::
+
+ private_key = open('private_key.pem').read()
+ signer = crypt.RSASigner.from_string(private_key)
+ signature = signer.sign(message)
+
+The code above also works for :class:`ES256Signer` and :class:`ES256Verifier`.
+Note that these two classes are only available if your `cryptography` dependency
+version is at least 1.4.0.
+"""
+
+from google.auth.crypt import base
+from google.auth.crypt import rsa
+
+try:
+ from google.auth.crypt import es256
+except ImportError: # pragma: NO COVER
+ es256 = None # type: ignore
+
+if es256 is not None: # pragma: NO COVER
+ __all__ = [
+ "ES256Signer",
+ "ES256Verifier",
+ "RSASigner",
+ "RSAVerifier",
+ "Signer",
+ "Verifier",
+ ]
+else: # pragma: NO COVER
+ __all__ = ["RSASigner", "RSAVerifier", "Signer", "Verifier"]
+
+
+# Aliases to maintain the v1.0.0 interface, as the crypt module was split
+# into submodules.
+Signer = base.Signer
+Verifier = base.Verifier
+RSASigner = rsa.RSASigner
+RSAVerifier = rsa.RSAVerifier
+
+if es256 is not None: # pragma: NO COVER
+ ES256Signer = es256.ES256Signer
+ ES256Verifier = es256.ES256Verifier
+
+
+def verify_signature(message, signature, certs, verifier_cls=rsa.RSAVerifier):
+ """Verify an RSA or ECDSA cryptographic signature.
+
+ Checks that the provided ``signature`` was generated from ``bytes`` using
+ the private key associated with the ``cert``.
+
+ Args:
+ message (Union[str, bytes]): The plaintext message.
+ signature (Union[str, bytes]): The cryptographic signature to check.
+ certs (Union[Sequence, str, bytes]): The certificate or certificates
+ to use to check the signature.
+ verifier_cls (Optional[~google.auth.crypt.base.Signer]): Which verifier
+ class to use for verification. This can be used to select different
+ algorithms, such as RSA or ECDSA. Default value is :class:`RSAVerifier`.
+
+ Returns:
+ bool: True if the signature is valid, otherwise False.
+ """
+ if isinstance(certs, (str, bytes)):
+ certs = [certs]
+
+ for cert in certs:
+ verifier = verifier_cls.from_string(cert)
+ if verifier.verify(message, signature):
+ return True
+ return False
diff --git a/contrib/python/google-auth/py3/google/auth/crypt/_cryptography_rsa.py b/contrib/python/google-auth/py3/google/auth/crypt/_cryptography_rsa.py
new file mode 100644
index 0000000000..4f2d611666
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/crypt/_cryptography_rsa.py
@@ -0,0 +1,136 @@
+# Copyright 2017 Google LLC
+#
+# 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.
+
+"""RSA verifier and signer that use the ``cryptography`` library.
+
+This is a much faster implementation than the default (in
+``google.auth.crypt._python_rsa``), which depends on the pure-Python
+``rsa`` library.
+"""
+
+import cryptography.exceptions
+from cryptography.hazmat import backends
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import padding
+import cryptography.x509
+
+from google.auth import _helpers
+from google.auth.crypt import base
+
+_CERTIFICATE_MARKER = b"-----BEGIN CERTIFICATE-----"
+_BACKEND = backends.default_backend()
+_PADDING = padding.PKCS1v15()
+_SHA256 = hashes.SHA256()
+
+
+class RSAVerifier(base.Verifier):
+ """Verifies RSA cryptographic signatures using public keys.
+
+ Args:
+ public_key (
+ cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
+ The public key used to verify signatures.
+ """
+
+ def __init__(self, public_key):
+ self._pubkey = public_key
+
+ @_helpers.copy_docstring(base.Verifier)
+ def verify(self, message, signature):
+ message = _helpers.to_bytes(message)
+ try:
+ self._pubkey.verify(signature, message, _PADDING, _SHA256)
+ return True
+ except (ValueError, cryptography.exceptions.InvalidSignature):
+ return False
+
+ @classmethod
+ def from_string(cls, public_key):
+ """Construct an Verifier instance from a public key or public
+ certificate string.
+
+ Args:
+ public_key (Union[str, bytes]): The public key in PEM format or the
+ x509 public key certificate.
+
+ Returns:
+ Verifier: The constructed verifier.
+
+ Raises:
+ ValueError: If the public key can't be parsed.
+ """
+ public_key_data = _helpers.to_bytes(public_key)
+
+ if _CERTIFICATE_MARKER in public_key_data:
+ cert = cryptography.x509.load_pem_x509_certificate(
+ public_key_data, _BACKEND
+ )
+ pubkey = cert.public_key()
+
+ else:
+ pubkey = serialization.load_pem_public_key(public_key_data, _BACKEND)
+
+ return cls(pubkey)
+
+
+class RSASigner(base.Signer, base.FromServiceAccountMixin):
+ """Signs messages with an RSA private key.
+
+ Args:
+ private_key (
+ cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
+ The private key to sign with.
+ key_id (str): Optional key ID used to identify this private key. This
+ can be useful to associate the private key with its associated
+ public key or certificate.
+ """
+
+ def __init__(self, private_key, key_id=None):
+ self._key = private_key
+ self._key_id = key_id
+
+ @property # type: ignore
+ @_helpers.copy_docstring(base.Signer)
+ def key_id(self):
+ return self._key_id
+
+ @_helpers.copy_docstring(base.Signer)
+ def sign(self, message):
+ message = _helpers.to_bytes(message)
+ return self._key.sign(message, _PADDING, _SHA256)
+
+ @classmethod
+ def from_string(cls, key, key_id=None):
+ """Construct a RSASigner from a private key in PEM format.
+
+ Args:
+ key (Union[bytes, str]): Private key in PEM format.
+ key_id (str): An optional key id used to identify the private key.
+
+ Returns:
+ google.auth.crypt._cryptography_rsa.RSASigner: The
+ constructed signer.
+
+ Raises:
+ ValueError: If ``key`` is not ``bytes`` or ``str`` (unicode).
+ UnicodeDecodeError: If ``key`` is ``bytes`` but cannot be decoded
+ into a UTF-8 ``str``.
+ ValueError: If ``cryptography`` "Could not deserialize key data."
+ """
+ key = _helpers.to_bytes(key)
+ private_key = serialization.load_pem_private_key(
+ key, password=None, backend=_BACKEND
+ )
+ return cls(private_key, key_id=key_id)
diff --git a/contrib/python/google-auth/py3/google/auth/crypt/_helpers.py b/contrib/python/google-auth/py3/google/auth/crypt/_helpers.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/crypt/_helpers.py
diff --git a/contrib/python/google-auth/py3/google/auth/crypt/_python_rsa.py b/contrib/python/google-auth/py3/google/auth/crypt/_python_rsa.py
new file mode 100644
index 0000000000..e553c25ed5
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/crypt/_python_rsa.py
@@ -0,0 +1,175 @@
+# Copyright 2016 Google LLC
+#
+# 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 RSA cryptography implementation.
+
+Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
+to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
+certificates. There is no support for p12 files.
+"""
+
+from __future__ import absolute_import
+
+import io
+
+from pyasn1.codec.der import decoder # type: ignore
+from pyasn1_modules import pem # type: ignore
+from pyasn1_modules.rfc2459 import Certificate # type: ignore
+from pyasn1_modules.rfc5208 import PrivateKeyInfo # type: ignore
+import rsa # type: ignore
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth.crypt import base
+
+_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
+_CERTIFICATE_MARKER = b"-----BEGIN CERTIFICATE-----"
+_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 1s and 0s to bytes.
+
+ Combines the list 8 at a time, treating each group of 8 bits
+ as a single byte.
+
+ Args:
+ bit_list (Sequence): Sequence of 1s and 0s.
+
+ Returns:
+ bytes: The decoded bytes.
+ """
+ num_bits = len(bit_list)
+ byte_vals = bytearray()
+ for start in range(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(base.Verifier):
+ """Verifies RSA cryptographic signatures using public keys.
+
+ Args:
+ public_key (rsa.key.PublicKey): The public key used to verify
+ signatures.
+ """
+
+ def __init__(self, public_key):
+ self._pubkey = public_key
+
+ @_helpers.copy_docstring(base.Verifier)
+ def verify(self, message, signature):
+ message = _helpers.to_bytes(message)
+ try:
+ return rsa.pkcs1.verify(message, signature, self._pubkey)
+ except (ValueError, rsa.pkcs1.VerificationError):
+ return False
+
+ @classmethod
+ def from_string(cls, public_key):
+ """Construct an Verifier instance from a public key or public
+ certificate string.
+
+ Args:
+ public_key (Union[str, bytes]): The public key in PEM format or the
+ x509 public key certificate.
+
+ Returns:
+ google.auth.crypt._python_rsa.RSAVerifier: The constructed verifier.
+
+ Raises:
+ ValueError: If the public_key can't be parsed.
+ """
+ public_key = _helpers.to_bytes(public_key)
+ is_x509_cert = _CERTIFICATE_MARKER in public_key
+
+ # If this is a certificate, extract the public key info.
+ if is_x509_cert:
+ der = rsa.pem.load_pem(public_key, "CERTIFICATE")
+ asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
+ if remaining != b"":
+ raise exceptions.InvalidValue("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(public_key, "PEM")
+ return cls(pubkey)
+
+
+class RSASigner(base.Signer, base.FromServiceAccountMixin):
+ """Signs messages with an RSA private key.
+
+ Args:
+ private_key (rsa.key.PrivateKey): The private key to sign with.
+ key_id (str): Optional key ID used to identify this private key. This
+ can be useful to associate the private key with its associated
+ public key or certificate.
+ """
+
+ def __init__(self, private_key, key_id=None):
+ self._key = private_key
+ self._key_id = key_id
+
+ @property # type: ignore
+ @_helpers.copy_docstring(base.Signer)
+ def key_id(self):
+ return self._key_id
+
+ @_helpers.copy_docstring(base.Signer)
+ def sign(self, message):
+ message = _helpers.to_bytes(message)
+ return rsa.pkcs1.sign(message, self._key, "SHA-256")
+
+ @classmethod
+ def from_string(cls, key, key_id=None):
+ """Construct an Signer instance from a private key in PEM format.
+
+ Args:
+ key (str): Private key in PEM format.
+ key_id (str): An optional key id used to identify the private key.
+
+ Returns:
+ google.auth.crypt.Signer: The constructed signer.
+
+ 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 Python 3
+ marker_id, key_bytes = pem.readPemBlocksFromFile(
+ io.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER
+ )
+
+ # Key is in pkcs1 format.
+ if marker_id == 0:
+ private_key = rsa.key.PrivateKey.load_pkcs1(key_bytes, format="DER")
+ # Key is in pkcs8.
+ elif marker_id == 1:
+ key_info, remaining = decoder.decode(key_bytes, asn1Spec=_PKCS8_SPEC)
+ if remaining != b"":
+ raise exceptions.InvalidValue("Unused bytes", remaining)
+ private_key_info = key_info.getComponentByName("privateKey")
+ private_key = rsa.key.PrivateKey.load_pkcs1(
+ private_key_info.asOctets(), format="DER"
+ )
+ else:
+ raise exceptions.MalformedError("No key could be detected.")
+
+ return cls(private_key, key_id=key_id)
diff --git a/contrib/python/google-auth/py3/google/auth/crypt/base.py b/contrib/python/google-auth/py3/google/auth/crypt/base.py
new file mode 100644
index 0000000000..ad871c3115
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/crypt/base.py
@@ -0,0 +1,127 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Base classes for cryptographic signers and verifiers."""
+
+import abc
+import io
+import json
+
+from google.auth import exceptions
+
+_JSON_FILE_PRIVATE_KEY = "private_key"
+_JSON_FILE_PRIVATE_KEY_ID = "private_key_id"
+
+
+class Verifier(metaclass=abc.ABCMeta):
+ """Abstract base class for crytographic signature verifiers."""
+
+ @abc.abstractmethod
+ def verify(self, message, signature):
+ """Verifies a message against a cryptographic signature.
+
+ Args:
+ message (Union[str, bytes]): The message to verify.
+ signature (Union[str, bytes]): The cryptography signature to check.
+
+ Returns:
+ bool: True if message was signed by the private key associated
+ with the public key that this object was constructed with.
+ """
+ # pylint: disable=missing-raises-doc,redundant-returns-doc
+ # (pylint doesn't recognize that this is abstract)
+ raise NotImplementedError("Verify must be implemented")
+
+
+class Signer(metaclass=abc.ABCMeta):
+ """Abstract base class for cryptographic signers."""
+
+ @abc.abstractproperty
+ def key_id(self):
+ """Optional[str]: The key ID used to identify this private key."""
+ raise NotImplementedError("Key id must be implemented")
+
+ @abc.abstractmethod
+ def sign(self, message):
+ """Signs a message.
+
+ Args:
+ message (Union[str, bytes]): The message to be signed.
+
+ Returns:
+ bytes: The signature of the message.
+ """
+ # pylint: disable=missing-raises-doc,redundant-returns-doc
+ # (pylint doesn't recognize that this is abstract)
+ raise NotImplementedError("Sign must be implemented")
+
+
+class FromServiceAccountMixin(metaclass=abc.ABCMeta):
+ """Mix-in to enable factory constructors for a Signer."""
+
+ @abc.abstractmethod
+ def from_string(cls, key, key_id=None):
+ """Construct an Signer instance from a private key string.
+
+ Args:
+ key (str): Private key as a string.
+ key_id (str): An optional key id used to identify the private key.
+
+ Returns:
+ google.auth.crypt.Signer: The constructed signer.
+
+ Raises:
+ ValueError: If the key cannot be parsed.
+ """
+ raise NotImplementedError("from_string must be implemented")
+
+ @classmethod
+ def from_service_account_info(cls, info):
+ """Creates a Signer instance instance from a dictionary containing
+ service account info in Google format.
+
+ Args:
+ info (Mapping[str, str]): The service account info in Google
+ format.
+
+ Returns:
+ google.auth.crypt.Signer: The constructed signer.
+
+ Raises:
+ ValueError: If the info is not in the expected format.
+ """
+ if _JSON_FILE_PRIVATE_KEY not in info:
+ raise exceptions.MalformedError(
+ "The private_key field was not found in the service account " "info."
+ )
+
+ return cls.from_string(
+ info[_JSON_FILE_PRIVATE_KEY], info.get(_JSON_FILE_PRIVATE_KEY_ID)
+ )
+
+ @classmethod
+ def from_service_account_file(cls, filename):
+ """Creates a Signer instance from a service account .json file
+ in Google format.
+
+ Args:
+ filename (str): The path to the service account .json file.
+
+ Returns:
+ google.auth.crypt.Signer: The constructed signer.
+ """
+ with io.open(filename, "r", encoding="utf-8") as json_file:
+ data = json.load(json_file)
+
+ return cls.from_service_account_info(data)
diff --git a/contrib/python/google-auth/py3/google/auth/crypt/es256.py b/contrib/python/google-auth/py3/google/auth/crypt/es256.py
new file mode 100644
index 0000000000..7920cc7ffb
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/crypt/es256.py
@@ -0,0 +1,160 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""ECDSA (ES256) verifier and signer that use the ``cryptography`` library.
+"""
+
+from cryptography import utils # type: ignore
+import cryptography.exceptions
+from cryptography.hazmat import backends
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
+from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
+import cryptography.x509
+
+from google.auth import _helpers
+from google.auth.crypt import base
+
+
+_CERTIFICATE_MARKER = b"-----BEGIN CERTIFICATE-----"
+_BACKEND = backends.default_backend()
+_PADDING = padding.PKCS1v15()
+
+
+class ES256Verifier(base.Verifier):
+ """Verifies ECDSA cryptographic signatures using public keys.
+
+ Args:
+ public_key (
+ cryptography.hazmat.primitives.asymmetric.ec.ECDSAPublicKey):
+ The public key used to verify signatures.
+ """
+
+ def __init__(self, public_key):
+ self._pubkey = public_key
+
+ @_helpers.copy_docstring(base.Verifier)
+ def verify(self, message, signature):
+ # First convert (r||s) raw signature to ASN1 encoded signature.
+ sig_bytes = _helpers.to_bytes(signature)
+ if len(sig_bytes) != 64:
+ return False
+ r = (
+ int.from_bytes(sig_bytes[:32], byteorder="big")
+ if _helpers.is_python_3()
+ else utils.int_from_bytes(sig_bytes[:32], byteorder="big")
+ )
+ s = (
+ int.from_bytes(sig_bytes[32:], byteorder="big")
+ if _helpers.is_python_3()
+ else utils.int_from_bytes(sig_bytes[32:], byteorder="big")
+ )
+ asn1_sig = encode_dss_signature(r, s)
+
+ message = _helpers.to_bytes(message)
+ try:
+ self._pubkey.verify(asn1_sig, message, ec.ECDSA(hashes.SHA256()))
+ return True
+ except (ValueError, cryptography.exceptions.InvalidSignature):
+ return False
+
+ @classmethod
+ def from_string(cls, public_key):
+ """Construct an Verifier instance from a public key or public
+ certificate string.
+
+ Args:
+ public_key (Union[str, bytes]): The public key in PEM format or the
+ x509 public key certificate.
+
+ Returns:
+ Verifier: The constructed verifier.
+
+ Raises:
+ ValueError: If the public key can't be parsed.
+ """
+ public_key_data = _helpers.to_bytes(public_key)
+
+ if _CERTIFICATE_MARKER in public_key_data:
+ cert = cryptography.x509.load_pem_x509_certificate(
+ public_key_data, _BACKEND
+ )
+ pubkey = cert.public_key()
+
+ else:
+ pubkey = serialization.load_pem_public_key(public_key_data, _BACKEND)
+
+ return cls(pubkey)
+
+
+class ES256Signer(base.Signer, base.FromServiceAccountMixin):
+ """Signs messages with an ECDSA private key.
+
+ Args:
+ private_key (
+ cryptography.hazmat.primitives.asymmetric.ec.ECDSAPrivateKey):
+ The private key to sign with.
+ key_id (str): Optional key ID used to identify this private key. This
+ can be useful to associate the private key with its associated
+ public key or certificate.
+ """
+
+ def __init__(self, private_key, key_id=None):
+ self._key = private_key
+ self._key_id = key_id
+
+ @property # type: ignore
+ @_helpers.copy_docstring(base.Signer)
+ def key_id(self):
+ return self._key_id
+
+ @_helpers.copy_docstring(base.Signer)
+ def sign(self, message):
+ message = _helpers.to_bytes(message)
+ asn1_signature = self._key.sign(message, ec.ECDSA(hashes.SHA256()))
+
+ # Convert ASN1 encoded signature to (r||s) raw signature.
+ (r, s) = decode_dss_signature(asn1_signature)
+ return (
+ (r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big"))
+ if _helpers.is_python_3()
+ else (utils.int_to_bytes(r, 32) + utils.int_to_bytes(s, 32))
+ )
+
+ @classmethod
+ def from_string(cls, key, key_id=None):
+ """Construct a RSASigner from a private key in PEM format.
+
+ Args:
+ key (Union[bytes, str]): Private key in PEM format.
+ key_id (str): An optional key id used to identify the private key.
+
+ Returns:
+ google.auth.crypt._cryptography_rsa.RSASigner: The
+ constructed signer.
+
+ Raises:
+ ValueError: If ``key`` is not ``bytes`` or ``str`` (unicode).
+ UnicodeDecodeError: If ``key`` is ``bytes`` but cannot be decoded
+ into a UTF-8 ``str``.
+ ValueError: If ``cryptography`` "Could not deserialize key data."
+ """
+ key = _helpers.to_bytes(key)
+ private_key = serialization.load_pem_private_key(
+ key, password=None, backend=_BACKEND
+ )
+ return cls(private_key, key_id=key_id)
diff --git a/contrib/python/google-auth/py3/google/auth/crypt/rsa.py b/contrib/python/google-auth/py3/google/auth/crypt/rsa.py
new file mode 100644
index 0000000000..ed842d1eb8
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/crypt/rsa.py
@@ -0,0 +1,30 @@
+# Copyright 2017 Google LLC
+#
+# 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.
+
+"""RSA cryptography signer and verifier."""
+
+
+try:
+ # Prefer cryptograph-based RSA implementation.
+ from google.auth.crypt import _cryptography_rsa
+
+ RSASigner = _cryptography_rsa.RSASigner
+ RSAVerifier = _cryptography_rsa.RSAVerifier
+except ImportError: # pragma: NO COVER
+ # Fallback to pure-python RSA implementation if cryptography is
+ # unavailable.
+ from google.auth.crypt import _python_rsa
+
+ RSASigner = _python_rsa.RSASigner # type: ignore
+ RSAVerifier = _python_rsa.RSAVerifier # type: ignore
diff --git a/contrib/python/google-auth/py3/google/auth/downscoped.py b/contrib/python/google-auth/py3/google/auth/downscoped.py
new file mode 100644
index 0000000000..b4d9d386e5
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/downscoped.py
@@ -0,0 +1,504 @@
+# Copyright 2021 Google LLC
+#
+# 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.
+
+"""Downscoping with Credential Access Boundaries
+
+This module provides the ability to downscope credentials using
+`Downscoping with Credential Access Boundaries`_. This is useful to restrict the
+Identity and Access Management (IAM) permissions that a short-lived credential
+can use.
+
+To downscope permissions of a source credential, a Credential Access Boundary
+that specifies which resources the new credential can access, as well as
+an upper bound on the permissions that are available on each resource, has to
+be defined. A downscoped credential can then be instantiated using the source
+credential and the Credential Access Boundary.
+
+The common pattern of usage is to have a token broker with elevated access
+generate these downscoped credentials from higher access source credentials and
+pass the downscoped short-lived access tokens to a token consumer via some
+secure authenticated channel for limited access to Google Cloud Storage
+resources.
+
+For example, a token broker can be set up on a server in a private network.
+Various workloads (token consumers) in the same network will send authenticated
+requests to that broker for downscoped tokens to access or modify specific google
+cloud storage buckets.
+
+The broker will instantiate downscoped credentials instances that can be used to
+generate short lived downscoped access tokens that can be passed to the token
+consumer. These downscoped access tokens can be injected by the consumer into
+google.oauth2.Credentials and used to initialize a storage client instance to
+access Google Cloud Storage resources with restricted access.
+
+Note: Only Cloud Storage supports Credential Access Boundaries. Other Google
+Cloud services do not support this feature.
+
+.. _Downscoping with Credential Access Boundaries: https://cloud.google.com/iam/docs/downscoping-short-lived-credentials
+"""
+
+import datetime
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.oauth2 import sts
+
+# The maximum number of access boundary rules a Credential Access Boundary can
+# contain.
+_MAX_ACCESS_BOUNDARY_RULES_COUNT = 10
+# The token exchange grant_type used for exchanging credentials.
+_STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
+# The token exchange requested_token_type. This is always an access_token.
+_STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+# The STS token URL used to exchanged a short lived access token for a downscoped one.
+_STS_TOKEN_URL = "https://sts.googleapis.com/v1/token"
+# The subject token type to use when exchanging a short lived access token for a
+# downscoped token.
+_STS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+
+
+class CredentialAccessBoundary(object):
+ """Defines a Credential Access Boundary which contains a list of access boundary
+ rules. Each rule contains information on the resource that the rule applies to,
+ the upper bound of the permissions that are available on that resource and an
+ optional condition to further restrict permissions.
+ """
+
+ def __init__(self, rules=[]):
+ """Instantiates a Credential Access Boundary. A Credential Access Boundary
+ can contain up to 10 access boundary rules.
+
+ Args:
+ rules (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of
+ access boundary rules limiting the access that a downscoped credential
+ will have.
+ Raises:
+ InvalidType: If any of the rules are not a valid type.
+ InvalidValue: If the provided rules exceed the maximum allowed.
+ """
+ self.rules = rules
+
+ @property
+ def rules(self):
+ """Returns the list of access boundary rules defined on the Credential
+ Access Boundary.
+
+ Returns:
+ Tuple[google.auth.downscoped.AccessBoundaryRule, ...]: The list of access
+ boundary rules defined on the Credential Access Boundary. These are returned
+ as an immutable tuple to prevent modification.
+ """
+ return tuple(self._rules)
+
+ @rules.setter
+ def rules(self, value):
+ """Updates the current rules on the Credential Access Boundary. This will overwrite
+ the existing set of rules.
+
+ Args:
+ value (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of
+ access boundary rules limiting the access that a downscoped credential
+ will have.
+ Raises:
+ InvalidType: If any of the rules are not a valid type.
+ InvalidValue: If the provided rules exceed the maximum allowed.
+ """
+ if len(value) > _MAX_ACCESS_BOUNDARY_RULES_COUNT:
+ raise exceptions.InvalidValue(
+ "Credential access boundary rules can have a maximum of {} rules.".format(
+ _MAX_ACCESS_BOUNDARY_RULES_COUNT
+ )
+ )
+ for access_boundary_rule in value:
+ if not isinstance(access_boundary_rule, AccessBoundaryRule):
+ raise exceptions.InvalidType(
+ "List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
+ )
+ # Make a copy of the original list.
+ self._rules = list(value)
+
+ def add_rule(self, rule):
+ """Adds a single access boundary rule to the existing rules.
+
+ Args:
+ rule (google.auth.downscoped.AccessBoundaryRule): The access boundary rule,
+ limiting the access that a downscoped credential will have, to be added to
+ the existing rules.
+ Raises:
+ InvalidType: If any of the rules are not a valid type.
+ InvalidValue: If the provided rules exceed the maximum allowed.
+ """
+ if len(self.rules) == _MAX_ACCESS_BOUNDARY_RULES_COUNT:
+ raise exceptions.InvalidValue(
+ "Credential access boundary rules can have a maximum of {} rules.".format(
+ _MAX_ACCESS_BOUNDARY_RULES_COUNT
+ )
+ )
+ if not isinstance(rule, AccessBoundaryRule):
+ raise exceptions.InvalidType(
+ "The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
+ )
+ self._rules.append(rule)
+
+ def to_json(self):
+ """Generates the dictionary representation of the Credential Access Boundary.
+ This uses the format expected by the Security Token Service API as documented in
+ `Defining a Credential Access Boundary`_.
+
+ .. _Defining a Credential Access Boundary:
+ https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
+
+ Returns:
+ Mapping: Credential Access Boundary Rule represented in a dictionary object.
+ """
+ rules = []
+ for access_boundary_rule in self.rules:
+ rules.append(access_boundary_rule.to_json())
+
+ return {"accessBoundary": {"accessBoundaryRules": rules}}
+
+
+class AccessBoundaryRule(object):
+ """Defines an access boundary rule which contains information on the resource that
+ the rule applies to, the upper bound of the permissions that are available on that
+ resource and an optional condition to further restrict permissions.
+ """
+
+ def __init__(
+ self, available_resource, available_permissions, availability_condition=None
+ ):
+ """Instantiates a single access boundary rule.
+
+ Args:
+ available_resource (str): The full resource name of the Cloud Storage bucket
+ that the rule applies to. Use the format
+ "//storage.googleapis.com/projects/_/buckets/bucket-name".
+ available_permissions (Sequence[str]): A list defining the upper bound that
+ the downscoped token will have on the available permissions for the
+ resource. Each value is the identifier for an IAM predefined role or
+ custom role, with the prefix "inRole:". For example:
+ "inRole:roles/storage.objectViewer".
+ Only the permissions in these roles will be available.
+ availability_condition (Optional[google.auth.downscoped.AvailabilityCondition]):
+ Optional condition that restricts the availability of permissions to
+ specific Cloud Storage objects.
+
+ Raises:
+ InvalidType: If any of the parameters are not of the expected types.
+ InvalidValue: If any of the parameters are not of the expected values.
+ """
+ self.available_resource = available_resource
+ self.available_permissions = available_permissions
+ self.availability_condition = availability_condition
+
+ @property
+ def available_resource(self):
+ """Returns the current available resource.
+
+ Returns:
+ str: The current available resource.
+ """
+ return self._available_resource
+
+ @available_resource.setter
+ def available_resource(self, value):
+ """Updates the current available resource.
+
+ Args:
+ value (str): The updated value of the available resource.
+
+ Raises:
+ google.auth.exceptions.InvalidType: If the value is not a string.
+ """
+ if not isinstance(value, str):
+ raise exceptions.InvalidType(
+ "The provided available_resource is not a string."
+ )
+ self._available_resource = value
+
+ @property
+ def available_permissions(self):
+ """Returns the current available permissions.
+
+ Returns:
+ Tuple[str, ...]: The current available permissions. These are returned
+ as an immutable tuple to prevent modification.
+ """
+ return tuple(self._available_permissions)
+
+ @available_permissions.setter
+ def available_permissions(self, value):
+ """Updates the current available permissions.
+
+ Args:
+ value (Sequence[str]): The updated value of the available permissions.
+
+ Raises:
+ InvalidType: If the value is not a list of strings.
+ InvalidValue: If the value is not valid.
+ """
+ for available_permission in value:
+ if not isinstance(available_permission, str):
+ raise exceptions.InvalidType(
+ "Provided available_permissions are not a list of strings."
+ )
+ if available_permission.find("inRole:") != 0:
+ raise exceptions.InvalidValue(
+ "available_permissions must be prefixed with 'inRole:'."
+ )
+ # Make a copy of the original list.
+ self._available_permissions = list(value)
+
+ @property
+ def availability_condition(self):
+ """Returns the current availability condition.
+
+ Returns:
+ Optional[google.auth.downscoped.AvailabilityCondition]: The current
+ availability condition.
+ """
+ return self._availability_condition
+
+ @availability_condition.setter
+ def availability_condition(self, value):
+ """Updates the current availability condition.
+
+ Args:
+ value (Optional[google.auth.downscoped.AvailabilityCondition]): The updated
+ value of the availability condition.
+
+ Raises:
+ google.auth.exceptions.InvalidType: If the value is not of type google.auth.downscoped.AvailabilityCondition
+ or None.
+ """
+ if not isinstance(value, AvailabilityCondition) and value is not None:
+ raise exceptions.InvalidType(
+ "The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None."
+ )
+ self._availability_condition = value
+
+ def to_json(self):
+ """Generates the dictionary representation of the access boundary rule.
+ This uses the format expected by the Security Token Service API as documented in
+ `Defining a Credential Access Boundary`_.
+
+ .. _Defining a Credential Access Boundary:
+ https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
+
+ Returns:
+ Mapping: The access boundary rule represented in a dictionary object.
+ """
+ json = {
+ "availablePermissions": list(self.available_permissions),
+ "availableResource": self.available_resource,
+ }
+ if self.availability_condition:
+ json["availabilityCondition"] = self.availability_condition.to_json()
+ return json
+
+
+class AvailabilityCondition(object):
+ """An optional condition that can be used as part of a Credential Access Boundary
+ to further restrict permissions."""
+
+ def __init__(self, expression, title=None, description=None):
+ """Instantiates an availability condition using the provided expression and
+ optional title or description.
+
+ Args:
+ expression (str): A condition expression that specifies the Cloud Storage
+ objects where permissions are available. For example, this expression
+ makes permissions available for objects whose name starts with "customer-a":
+ "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
+ title (Optional[str]): An optional short string that identifies the purpose of
+ the condition.
+ description (Optional[str]): Optional details about the purpose of the condition.
+
+ Raises:
+ InvalidType: If any of the parameters are not of the expected types.
+ InvalidValue: If any of the parameters are not of the expected values.
+ """
+ self.expression = expression
+ self.title = title
+ self.description = description
+
+ @property
+ def expression(self):
+ """Returns the current condition expression.
+
+ Returns:
+ str: The current conditon expression.
+ """
+ return self._expression
+
+ @expression.setter
+ def expression(self, value):
+ """Updates the current condition expression.
+
+ Args:
+ value (str): The updated value of the condition expression.
+
+ Raises:
+ google.auth.exceptions.InvalidType: If the value is not of type string.
+ """
+ if not isinstance(value, str):
+ raise exceptions.InvalidType("The provided expression is not a string.")
+ self._expression = value
+
+ @property
+ def title(self):
+ """Returns the current title.
+
+ Returns:
+ Optional[str]: The current title.
+ """
+ return self._title
+
+ @title.setter
+ def title(self, value):
+ """Updates the current title.
+
+ Args:
+ value (Optional[str]): The updated value of the title.
+
+ Raises:
+ google.auth.exceptions.InvalidType: If the value is not of type string or None.
+ """
+ if not isinstance(value, str) and value is not None:
+ raise exceptions.InvalidType("The provided title is not a string or None.")
+ self._title = value
+
+ @property
+ def description(self):
+ """Returns the current description.
+
+ Returns:
+ Optional[str]: The current description.
+ """
+ return self._description
+
+ @description.setter
+ def description(self, value):
+ """Updates the current description.
+
+ Args:
+ value (Optional[str]): The updated value of the description.
+
+ Raises:
+ google.auth.exceptions.InvalidType: If the value is not of type string or None.
+ """
+ if not isinstance(value, str) and value is not None:
+ raise exceptions.InvalidType(
+ "The provided description is not a string or None."
+ )
+ self._description = value
+
+ def to_json(self):
+ """Generates the dictionary representation of the availability condition.
+ This uses the format expected by the Security Token Service API as documented in
+ `Defining a Credential Access Boundary`_.
+
+ .. _Defining a Credential Access Boundary:
+ https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
+
+ Returns:
+ Mapping[str, str]: The availability condition represented in a dictionary
+ object.
+ """
+ json = {"expression": self.expression}
+ if self.title:
+ json["title"] = self.title
+ if self.description:
+ json["description"] = self.description
+ return json
+
+
+class Credentials(credentials.CredentialsWithQuotaProject):
+ """Defines a set of Google credentials that are downscoped from an existing set
+ of Google OAuth2 credentials. This is useful to restrict the Identity and Access
+ Management (IAM) permissions that a short-lived credential can use.
+ The common pattern of usage is to have a token broker with elevated access
+ generate these downscoped credentials from higher access source credentials and
+ pass the downscoped short-lived access tokens to a token consumer via some
+ secure authenticated channel for limited access to Google Cloud Storage
+ resources.
+ """
+
+ def __init__(
+ self, source_credentials, credential_access_boundary, quota_project_id=None
+ ):
+ """Instantiates a downscoped credentials object using the provided source
+ credentials and credential access boundary rules.
+ To downscope permissions of a source credential, a Credential Access Boundary
+ that specifies which resources the new credential can access, as well as an
+ upper bound on the permissions that are available on each resource, has to be
+ defined. A downscoped credential can then be instantiated using the source
+ credential and the Credential Access Boundary.
+
+ Args:
+ source_credentials (google.auth.credentials.Credentials): The source credentials
+ to be downscoped based on the provided Credential Access Boundary rules.
+ credential_access_boundary (google.auth.downscoped.CredentialAccessBoundary):
+ The Credential Access Boundary which contains a list of access boundary
+ rules. Each rule contains information on the resource that the rule applies to,
+ the upper bound of the permissions that are available on that resource and an
+ optional condition to further restrict permissions.
+ quota_project_id (Optional[str]): The optional quota project ID.
+ Raises:
+ google.auth.exceptions.RefreshError: If the source credentials
+ return an error on token refresh.
+ google.auth.exceptions.OAuthError: If the STS token exchange
+ endpoint returned an error during downscoped token generation.
+ """
+
+ super(Credentials, self).__init__()
+ self._source_credentials = source_credentials
+ self._credential_access_boundary = credential_access_boundary
+ self._quota_project_id = quota_project_id
+ self._sts_client = sts.Client(_STS_TOKEN_URL)
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def refresh(self, request):
+ # Generate an access token from the source credentials.
+ self._source_credentials.refresh(request)
+ now = _helpers.utcnow()
+ # Exchange the access token for a downscoped access token.
+ response_data = self._sts_client.exchange_token(
+ request=request,
+ grant_type=_STS_GRANT_TYPE,
+ subject_token=self._source_credentials.token,
+ subject_token_type=_STS_SUBJECT_TOKEN_TYPE,
+ requested_token_type=_STS_REQUESTED_TOKEN_TYPE,
+ additional_options=self._credential_access_boundary.to_json(),
+ )
+ self.token = response_data.get("access_token")
+ # For downscoping CAB flow, the STS endpoint may not return the expiration
+ # field for some flows. The generated downscoped token should always have
+ # the same expiration time as the source credentials. When no expires_in
+ # field is returned in the response, we can just get the expiration time
+ # from the source credentials.
+ if response_data.get("expires_in"):
+ lifetime = datetime.timedelta(seconds=response_data.get("expires_in"))
+ self.expiry = now + lifetime
+ else:
+ self.expiry = self._source_credentials.expiry
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ return self.__class__(
+ self._source_credentials,
+ self._credential_access_boundary,
+ quota_project_id=quota_project_id,
+ )
diff --git a/contrib/python/google-auth/py3/google/auth/environment_vars.py b/contrib/python/google-auth/py3/google/auth/environment_vars.py
new file mode 100644
index 0000000000..81f31571eb
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/environment_vars.py
@@ -0,0 +1,84 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Environment variables used by :mod:`google.auth`."""
+
+
+PROJECT = "GOOGLE_CLOUD_PROJECT"
+"""Environment variable defining default project.
+
+This used by :func:`google.auth.default` to explicitly set a project ID. This
+environment variable is also used by the Google Cloud Python Library.
+"""
+
+LEGACY_PROJECT = "GCLOUD_PROJECT"
+"""Previously used environment variable defining the default project.
+
+This environment variable is used instead of the current one in some
+situations (such as Google App Engine).
+"""
+
+GOOGLE_CLOUD_QUOTA_PROJECT = "GOOGLE_CLOUD_QUOTA_PROJECT"
+"""Environment variable defining the project to be used for
+quota and billing."""
+
+CREDENTIALS = "GOOGLE_APPLICATION_CREDENTIALS"
+"""Environment variable defining the location of Google application default
+credentials."""
+
+# The environment variable name which can replace ~/.config if set.
+CLOUD_SDK_CONFIG_DIR = "CLOUDSDK_CONFIG"
+"""Environment variable defines the location of Google Cloud SDK's config
+files."""
+
+# These two variables allow for customization of the addresses used when
+# contacting the GCE metadata service.
+GCE_METADATA_HOST = "GCE_METADATA_HOST"
+"""Environment variable providing an alternate hostname or host:port to be
+used for GCE metadata requests.
+
+This environment variable was originally named GCE_METADATA_ROOT. The system will
+check this environemnt variable first; should there be no value present,
+the system will fall back to the old variable.
+"""
+
+GCE_METADATA_ROOT = "GCE_METADATA_ROOT"
+"""Old environment variable for GCE_METADATA_HOST."""
+
+GCE_METADATA_IP = "GCE_METADATA_IP"
+"""Environment variable providing an alternate ip:port to be used for ip-only
+GCE metadata requests."""
+
+GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
+"""Environment variable controlling whether to use client certificate or not.
+
+The default value is false. Users have to explicitly set this value to true
+in order to use client certificate to establish a mutual TLS channel."""
+
+LEGACY_APPENGINE_RUNTIME = "APPENGINE_RUNTIME"
+"""Gen1 environment variable defining the App Engine Runtime.
+
+Used to distinguish between GAE gen1 and GAE gen2+.
+"""
+
+# AWS environment variables used with AWS workload identity pools to retrieve
+# AWS security credentials and the AWS region needed to create a serialized
+# signed requests to the AWS STS GetCalledIdentity API that can be exchanged
+# for a Google access tokens via the GCP STS endpoint.
+# When not available the AWS metadata server is used to retrieve these values.
+AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"
+AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"
+AWS_SESSION_TOKEN = "AWS_SESSION_TOKEN"
+AWS_REGION = "AWS_REGION"
+AWS_DEFAULT_REGION = "AWS_DEFAULT_REGION"
diff --git a/contrib/python/google-auth/py3/google/auth/exceptions.py b/contrib/python/google-auth/py3/google/auth/exceptions.py
new file mode 100644
index 0000000000..fcbe61b746
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/exceptions.py
@@ -0,0 +1,100 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Exceptions used in the google.auth package."""
+
+
+class GoogleAuthError(Exception):
+ """Base class for all google.auth errors."""
+
+ def __init__(self, *args, **kwargs):
+ super(GoogleAuthError, self).__init__(*args)
+ retryable = kwargs.get("retryable", False)
+ self._retryable = retryable
+
+ @property
+ def retryable(self):
+ return self._retryable
+
+
+class TransportError(GoogleAuthError):
+ """Used to indicate an error occurred during an HTTP request."""
+
+
+class RefreshError(GoogleAuthError):
+ """Used to indicate that an refreshing the credentials' access token
+ failed."""
+
+
+class UserAccessTokenError(GoogleAuthError):
+ """Used to indicate ``gcloud auth print-access-token`` command failed."""
+
+
+class DefaultCredentialsError(GoogleAuthError):
+ """Used to indicate that acquiring default credentials failed."""
+
+
+class MutualTLSChannelError(GoogleAuthError):
+ """Used to indicate that mutual TLS channel creation is failed, or mutual
+ TLS channel credentials is missing or invalid."""
+
+
+class ClientCertError(GoogleAuthError):
+ """Used to indicate that client certificate is missing or invalid."""
+
+ @property
+ def retryable(self):
+ return False
+
+
+class OAuthError(GoogleAuthError):
+ """Used to indicate an error occurred during an OAuth related HTTP
+ request."""
+
+
+class ReauthFailError(RefreshError):
+ """An exception for when reauth failed."""
+
+ def __init__(self, message=None, **kwargs):
+ super(ReauthFailError, self).__init__(
+ "Reauthentication failed. {0}".format(message), **kwargs
+ )
+
+
+class ReauthSamlChallengeFailError(ReauthFailError):
+ """An exception for SAML reauth challenge failures."""
+
+
+class MalformedError(DefaultCredentialsError, ValueError):
+ """An exception for malformed data."""
+
+
+class InvalidResource(DefaultCredentialsError, ValueError):
+ """An exception for URL error."""
+
+
+class InvalidOperation(DefaultCredentialsError, ValueError):
+ """An exception for invalid operation."""
+
+
+class InvalidValue(DefaultCredentialsError, ValueError):
+ """Used to wrap general ValueError of python."""
+
+
+class InvalidType(DefaultCredentialsError, TypeError):
+ """Used to wrap general TypeError of python."""
+
+
+class OSError(DefaultCredentialsError, EnvironmentError):
+ """Used to wrap EnvironmentError(OSError after python3.3)."""
diff --git a/contrib/python/google-auth/py3/google/auth/external_account.py b/contrib/python/google-auth/py3/google/auth/external_account.py
new file mode 100644
index 0000000000..c45e6f2133
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/external_account.py
@@ -0,0 +1,523 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+
+"""External Account Credentials.
+
+This module provides credentials that exchange workload identity pool external
+credentials for Google access tokens. This facilitates accessing Google Cloud
+Platform resources from on-prem and non-Google Cloud platforms (e.g. AWS,
+Microsoft Azure, OIDC identity providers), using native credentials retrieved
+from the current environment without the need to copy, save and manage
+long-lived service account credentials.
+
+Specifically, this is intended to use access tokens acquired using the GCP STS
+token exchange endpoint following the `OAuth 2.0 Token Exchange`_ spec.
+
+.. _OAuth 2.0 Token Exchange: https://tools.ietf.org/html/rfc8693
+"""
+
+import abc
+import copy
+import datetime
+import io
+import json
+import re
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.auth import impersonated_credentials
+from google.auth import metrics
+from google.oauth2 import sts
+from google.oauth2 import utils
+
+# External account JSON type identifier.
+_EXTERNAL_ACCOUNT_JSON_TYPE = "external_account"
+# The token exchange grant_type used for exchanging credentials.
+_STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
+# The token exchange requested_token_type. This is always an access_token.
+_STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+# Cloud resource manager URL used to retrieve project information.
+_CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/"
+
+_DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
+
+
+class Credentials(
+ credentials.Scoped,
+ credentials.CredentialsWithQuotaProject,
+ credentials.CredentialsWithTokenUri,
+ metaclass=abc.ABCMeta,
+):
+ """Base class for all external account credentials.
+
+ This is used to instantiate Credentials for exchanging external account
+ credentials for Google access token and authorizing requests to Google APIs.
+ The base class implements the common logic for exchanging external account
+ credentials for Google access tokens.
+ """
+
+ def __init__(
+ self,
+ audience,
+ subject_token_type,
+ token_url,
+ credential_source,
+ service_account_impersonation_url=None,
+ service_account_impersonation_options=None,
+ client_id=None,
+ client_secret=None,
+ token_info_url=None,
+ quota_project_id=None,
+ scopes=None,
+ default_scopes=None,
+ workforce_pool_user_project=None,
+ universe_domain=_DEFAULT_UNIVERSE_DOMAIN,
+ trust_boundary=None,
+ ):
+ """Instantiates an external account credentials object.
+
+ Args:
+ audience (str): The STS audience field.
+ subject_token_type (str): The subject token type.
+ token_url (str): The STS endpoint URL.
+ credential_source (Mapping): The credential source dictionary.
+ service_account_impersonation_url (Optional[str]): The optional service account
+ impersonation generateAccessToken URL.
+ client_id (Optional[str]): The optional client ID.
+ client_secret (Optional[str]): The optional client secret.
+ token_info_url (str): The optional STS endpoint URL for token introspection.
+ quota_project_id (Optional[str]): The optional quota project ID.
+ scopes (Optional[Sequence[str]]): Optional scopes to request during the
+ authorization grant.
+ default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+ Google client library. Use 'scopes' for user-defined scopes.
+ workforce_pool_user_project (Optona[str]): The optional workforce pool user
+ project number when the credential corresponds to a workforce pool and not
+ a workload identity pool. The underlying principal must still have
+ serviceusage.services.use IAM permission to use the project for
+ billing/quota.
+ universe_domain (str): The universe domain. The default universe
+ domain is googleapis.com.
+ trust_boundary (str): String representation of trust boundary meta.
+ Raises:
+ google.auth.exceptions.RefreshError: If the generateAccessToken
+ endpoint returned an error.
+ """
+ super(Credentials, self).__init__()
+ self._audience = audience
+ self._subject_token_type = subject_token_type
+ self._token_url = token_url
+ self._token_info_url = token_info_url
+ self._credential_source = credential_source
+ self._service_account_impersonation_url = service_account_impersonation_url
+ self._service_account_impersonation_options = (
+ service_account_impersonation_options or {}
+ )
+ self._client_id = client_id
+ self._client_secret = client_secret
+ self._quota_project_id = quota_project_id
+ self._scopes = scopes
+ self._default_scopes = default_scopes
+ self._workforce_pool_user_project = workforce_pool_user_project
+ self._universe_domain = universe_domain or _DEFAULT_UNIVERSE_DOMAIN
+ self._trust_boundary = "0" # expose a placeholder trust boundary value.
+
+ if self._client_id:
+ self._client_auth = utils.ClientAuthentication(
+ utils.ClientAuthType.basic, self._client_id, self._client_secret
+ )
+ else:
+ self._client_auth = None
+ self._sts_client = sts.Client(self._token_url, self._client_auth)
+
+ self._metrics_options = self._create_default_metrics_options()
+
+ if self._service_account_impersonation_url:
+ self._impersonated_credentials = self._initialize_impersonated_credentials()
+ else:
+ self._impersonated_credentials = None
+ self._project_id = None
+
+ if not self.is_workforce_pool and self._workforce_pool_user_project:
+ # Workload identity pools do not support workforce pool user projects.
+ raise exceptions.InvalidValue(
+ "workforce_pool_user_project should not be set for non-workforce pool "
+ "credentials"
+ )
+
+ @property
+ def info(self):
+ """Generates the dictionary representation of the current credentials.
+
+ Returns:
+ Mapping: The dictionary representation of the credentials. This is the
+ reverse of "from_info" defined on the subclasses of this class. It is
+ useful for serializing the current credentials so it can deserialized
+ later.
+ """
+ config_info = self._constructor_args()
+ config_info.update(
+ type=_EXTERNAL_ACCOUNT_JSON_TYPE,
+ service_account_impersonation=config_info.pop(
+ "service_account_impersonation_options", None
+ ),
+ )
+ config_info.pop("scopes", None)
+ config_info.pop("default_scopes", None)
+ return {key: value for key, value in config_info.items() if value is not None}
+
+ def _constructor_args(self):
+ args = {
+ "audience": self._audience,
+ "subject_token_type": self._subject_token_type,
+ "token_url": self._token_url,
+ "token_info_url": self._token_info_url,
+ "service_account_impersonation_url": self._service_account_impersonation_url,
+ "service_account_impersonation_options": copy.deepcopy(
+ self._service_account_impersonation_options
+ )
+ or None,
+ "credential_source": copy.deepcopy(self._credential_source),
+ "quota_project_id": self._quota_project_id,
+ "client_id": self._client_id,
+ "client_secret": self._client_secret,
+ "workforce_pool_user_project": self._workforce_pool_user_project,
+ "scopes": self._scopes,
+ "default_scopes": self._default_scopes,
+ "universe_domain": self._universe_domain,
+ }
+ if not self.is_workforce_pool:
+ args.pop("workforce_pool_user_project")
+ return args
+
+ @property
+ def service_account_email(self):
+ """Returns the service account email if service account impersonation is used.
+
+ Returns:
+ Optional[str]: The service account email if impersonation is used. Otherwise
+ None is returned.
+ """
+ if self._service_account_impersonation_url:
+ # Parse email from URL. The formal looks as follows:
+ # https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken
+ url = self._service_account_impersonation_url
+ start_index = url.rfind("/")
+ end_index = url.find(":generateAccessToken")
+ if start_index != -1 and end_index != -1 and start_index < end_index:
+ start_index = start_index + 1
+ return url[start_index:end_index]
+ return None
+
+ @property
+ def is_user(self):
+ """Returns whether the credentials represent a user (True) or workload (False).
+ Workloads behave similarly to service accounts. Currently workloads will use
+ service account impersonation but will eventually not require impersonation.
+ As a result, this property is more reliable than the service account email
+ property in determining if the credentials represent a user or workload.
+
+ Returns:
+ bool: True if the credentials represent a user. False if they represent a
+ workload.
+ """
+ # If service account impersonation is used, the credentials will always represent a
+ # service account.
+ if self._service_account_impersonation_url:
+ return False
+ return self.is_workforce_pool
+
+ @property
+ def is_workforce_pool(self):
+ """Returns whether the credentials represent a workforce pool (True) or
+ workload (False) based on the credentials' audience.
+
+ This will also return True for impersonated workforce pool credentials.
+
+ Returns:
+ bool: True if the credentials represent a workforce pool. False if they
+ represent a workload.
+ """
+ # Workforce pools representing users have the following audience format:
+ # //iam.googleapis.com/locations/$location/workforcePools/$poolId/providers/$providerId
+ p = re.compile(r"//iam\.googleapis\.com/locations/[^/]+/workforcePools/")
+ return p.match(self._audience or "") is not None
+
+ @property
+ def requires_scopes(self):
+ """Checks if the credentials requires scopes.
+
+ Returns:
+ bool: True if there are no scopes set otherwise False.
+ """
+ return not self._scopes and not self._default_scopes
+
+ @property
+ def project_number(self):
+ """Optional[str]: The project number corresponding to the workload identity pool."""
+
+ # STS audience pattern:
+ # //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...
+ components = self._audience.split("/")
+ try:
+ project_index = components.index("projects")
+ if project_index + 1 < len(components):
+ return components[project_index + 1] or None
+ except ValueError:
+ return None
+
+ @property
+ def token_info_url(self):
+ """Optional[str]: The STS token introspection endpoint."""
+
+ return self._token_info_url
+
+ @_helpers.copy_docstring(credentials.Scoped)
+ def with_scopes(self, scopes, default_scopes=None):
+ kwargs = self._constructor_args()
+ kwargs.update(scopes=scopes, default_scopes=default_scopes)
+ scoped = self.__class__(**kwargs)
+ scoped._metrics_options = self._metrics_options
+ return scoped
+
+ @abc.abstractmethod
+ def retrieve_subject_token(self, request):
+ """Retrieves the subject token using the credential_source object.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ Returns:
+ str: The retrieved subject token.
+ """
+ # pylint: disable=missing-raises-doc
+ # (pylint doesn't recognize that this is abstract)
+ raise NotImplementedError("retrieve_subject_token must be implemented")
+
+ def get_project_id(self, request):
+ """Retrieves the project ID corresponding to the workload identity or workforce pool.
+ For workforce pool credentials, it returns the project ID corresponding to
+ the workforce_pool_user_project.
+
+ When not determinable, None is returned.
+
+ This is introduced to support the current pattern of using the Auth library:
+
+ credentials, project_id = google.auth.default()
+
+ The resource may not have permission (resourcemanager.projects.get) to
+ call this API or the required scopes may not be selected:
+ https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ Returns:
+ Optional[str]: The project ID corresponding to the workload identity pool
+ or workforce pool if determinable.
+ """
+ if self._project_id:
+ # If already retrieved, return the cached project ID value.
+ return self._project_id
+ scopes = self._scopes if self._scopes is not None else self._default_scopes
+ # Scopes are required in order to retrieve a valid access token.
+ project_number = self.project_number or self._workforce_pool_user_project
+ if project_number and scopes:
+ headers = {}
+ url = _CLOUD_RESOURCE_MANAGER + project_number
+ self.before_request(request, "GET", url, headers)
+ response = request(url=url, method="GET", headers=headers)
+
+ response_body = (
+ response.data.decode("utf-8")
+ if hasattr(response.data, "decode")
+ else response.data
+ )
+ response_data = json.loads(response_body)
+
+ if response.status == 200:
+ # Cache result as this field is immutable.
+ self._project_id = response_data.get("projectId")
+ return self._project_id
+
+ return None
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def refresh(self, request):
+ scopes = self._scopes if self._scopes is not None else self._default_scopes
+ if self._impersonated_credentials:
+ self._impersonated_credentials.refresh(request)
+ self.token = self._impersonated_credentials.token
+ self.expiry = self._impersonated_credentials.expiry
+ else:
+ now = _helpers.utcnow()
+ additional_options = None
+ # Do not pass workforce_pool_user_project when client authentication
+ # is used. The client ID is sufficient for determining the user project.
+ if self._workforce_pool_user_project and not self._client_id:
+ additional_options = {"userProject": self._workforce_pool_user_project}
+ additional_headers = {
+ metrics.API_CLIENT_HEADER: metrics.byoid_metrics_header(
+ self._metrics_options
+ )
+ }
+ response_data = self._sts_client.exchange_token(
+ request=request,
+ grant_type=_STS_GRANT_TYPE,
+ subject_token=self.retrieve_subject_token(request),
+ subject_token_type=self._subject_token_type,
+ audience=self._audience,
+ scopes=scopes,
+ requested_token_type=_STS_REQUESTED_TOKEN_TYPE,
+ additional_options=additional_options,
+ additional_headers=additional_headers,
+ )
+ self.token = response_data.get("access_token")
+ expires_in = response_data.get("expires_in")
+ # Some services do not respect the OAUTH2.0 RFC and send expires_in as a
+ # JSON String.
+ if isinstance(expires_in, str):
+ expires_in = int(expires_in)
+
+ lifetime = datetime.timedelta(seconds=expires_in)
+
+ self.expiry = now + lifetime
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ # Return copy of instance with the provided quota project ID.
+ kwargs = self._constructor_args()
+ kwargs.update(quota_project_id=quota_project_id)
+ new_cred = self.__class__(**kwargs)
+ new_cred._metrics_options = self._metrics_options
+ return new_cred
+
+ @_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
+ def with_token_uri(self, token_uri):
+ kwargs = self._constructor_args()
+ kwargs.update(token_url=token_uri)
+ new_cred = self.__class__(**kwargs)
+ new_cred._metrics_options = self._metrics_options
+ return new_cred
+
+ def _initialize_impersonated_credentials(self):
+ """Generates an impersonated credentials.
+
+ For more details, see `projects.serviceAccounts.generateAccessToken`_.
+
+ .. _projects.serviceAccounts.generateAccessToken: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
+
+ Returns:
+ impersonated_credentials.Credential: The impersonated credentials
+ object.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the generateAccessToken
+ endpoint returned an error.
+ """
+ # Return copy of instance with no service account impersonation.
+ kwargs = self._constructor_args()
+ kwargs.update(
+ service_account_impersonation_url=None,
+ service_account_impersonation_options={},
+ )
+ source_credentials = self.__class__(**kwargs)
+ source_credentials._metrics_options = self._metrics_options
+
+ # Determine target_principal.
+ target_principal = self.service_account_email
+ if not target_principal:
+ raise exceptions.RefreshError(
+ "Unable to determine target principal from service account impersonation URL."
+ )
+
+ scopes = self._scopes if self._scopes is not None else self._default_scopes
+ # Initialize and return impersonated credentials.
+ return impersonated_credentials.Credentials(
+ source_credentials=source_credentials,
+ target_principal=target_principal,
+ target_scopes=scopes,
+ quota_project_id=self._quota_project_id,
+ iam_endpoint_override=self._service_account_impersonation_url,
+ lifetime=self._service_account_impersonation_options.get(
+ "token_lifetime_seconds"
+ ),
+ )
+
+ def _create_default_metrics_options(self):
+ metrics_options = {}
+ if self._service_account_impersonation_url:
+ metrics_options["sa-impersonation"] = "true"
+ else:
+ metrics_options["sa-impersonation"] = "false"
+ if self._service_account_impersonation_options.get("token_lifetime_seconds"):
+ metrics_options["config-lifetime"] = "true"
+ else:
+ metrics_options["config-lifetime"] = "false"
+
+ return metrics_options
+
+ @classmethod
+ def from_info(cls, info, **kwargs):
+ """Creates a Credentials instance from parsed external account info.
+
+ Args:
+ info (Mapping[str, str]): The external account info in Google
+ format.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.identity_pool.Credentials: The constructed
+ credentials.
+
+ Raises:
+ InvalidValue: For invalid parameters.
+ """
+ return cls(
+ audience=info.get("audience"),
+ subject_token_type=info.get("subject_token_type"),
+ token_url=info.get("token_url"),
+ token_info_url=info.get("token_info_url"),
+ service_account_impersonation_url=info.get(
+ "service_account_impersonation_url"
+ ),
+ service_account_impersonation_options=info.get(
+ "service_account_impersonation"
+ )
+ or {},
+ client_id=info.get("client_id"),
+ client_secret=info.get("client_secret"),
+ credential_source=info.get("credential_source"),
+ quota_project_id=info.get("quota_project_id"),
+ workforce_pool_user_project=info.get("workforce_pool_user_project"),
+ universe_domain=info.get("universe_domain", _DEFAULT_UNIVERSE_DOMAIN),
+ **kwargs
+ )
+
+ @classmethod
+ def from_file(cls, filename, **kwargs):
+ """Creates a Credentials instance from an external account json file.
+
+ Args:
+ filename (str): The path to the external account json file.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.identity_pool.Credentials: The constructed
+ credentials.
+ """
+ with io.open(filename, "r", encoding="utf-8") as json_file:
+ data = json.load(json_file)
+ return cls.from_info(data, **kwargs)
diff --git a/contrib/python/google-auth/py3/google/auth/external_account_authorized_user.py b/contrib/python/google-auth/py3/google/auth/external_account_authorized_user.py
new file mode 100644
index 0000000000..a2d4edf6ff
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/external_account_authorized_user.py
@@ -0,0 +1,350 @@
+# Copyright 2022 Google LLC
+#
+# 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.
+
+"""External Account Authorized User Credentials.
+This module provides credentials based on OAuth 2.0 access and refresh tokens.
+These credentials usually access resources on behalf of a user (resource
+owner).
+
+Specifically, these are sourced using external identities via Workforce Identity Federation.
+
+Obtaining the initial access and refresh token can be done through the Google Cloud CLI.
+
+Example credential:
+{
+ "type": "external_account_authorized_user",
+ "audience": "//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID",
+ "refresh_token": "refreshToken",
+ "token_url": "https://sts.googleapis.com/v1/oauth/token",
+ "token_info_url": "https://sts.googleapis.com/v1/instrospect",
+ "client_id": "clientId",
+ "client_secret": "clientSecret"
+}
+"""
+
+import datetime
+import io
+import json
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.oauth2 import sts
+from google.oauth2 import utils
+
+_EXTERNAL_ACCOUNT_AUTHORIZED_USER_JSON_TYPE = "external_account_authorized_user"
+
+
+class Credentials(
+ credentials.CredentialsWithQuotaProject,
+ credentials.ReadOnlyScoped,
+ credentials.CredentialsWithTokenUri,
+):
+ """Credentials for External Account Authorized Users.
+
+ This is used to instantiate Credentials for exchanging refresh tokens from
+ authorized users for Google access token and authorizing requests to Google
+ APIs.
+
+ The credentials are considered immutable. If you want to modify the
+ quota project, use `with_quota_project` and if you want to modify the token
+ uri, use `with_token_uri`.
+ """
+
+ def __init__(
+ self,
+ token=None,
+ expiry=None,
+ refresh_token=None,
+ audience=None,
+ client_id=None,
+ client_secret=None,
+ token_url=None,
+ token_info_url=None,
+ revoke_url=None,
+ scopes=None,
+ quota_project_id=None,
+ ):
+ """Instantiates a external account authorized user credentials object.
+
+ Args:
+ token (str): The OAuth 2.0 access token. Can be None if refresh information
+ is provided.
+ expiry (datetime.datetime): The optional expiration datetime of the OAuth 2.0 access
+ token.
+ refresh_token (str): The optional OAuth 2.0 refresh token. If specified,
+ credentials can be refreshed.
+ audience (str): The optional STS audience which contains the resource name for the workforce
+ pool and the provider identifier in that pool.
+ client_id (str): The OAuth 2.0 client ID. Must be specified for refresh, can be left as
+ None if the token can not be refreshed.
+ client_secret (str): The OAuth 2.0 client secret. Must be specified for refresh, can be
+ left as None if the token can not be refreshed.
+ token_url (str): The optional STS token exchange endpoint for refresh. Must be specified for
+ refresh, can be left as None if the token can not be refreshed.
+ token_info_url (str): The optional STS endpoint URL for token introspection.
+ revoke_url (str): The optional STS endpoint URL for revoking tokens.
+ quota_project_id (str): The optional project ID used for quota and billing.
+ This project may be different from the project used to
+ create the credentials.
+
+ Returns:
+ google.auth.external_account_authorized_user.Credentials: The
+ constructed credentials.
+ """
+ super(Credentials, self).__init__()
+
+ self.token = token
+ self.expiry = expiry
+ self._audience = audience
+ self._refresh_token = refresh_token
+ self._token_url = token_url
+ self._token_info_url = token_info_url
+ self._client_id = client_id
+ self._client_secret = client_secret
+ self._revoke_url = revoke_url
+ self._quota_project_id = quota_project_id
+ self._scopes = scopes
+
+ if not self.valid and not self.can_refresh:
+ raise exceptions.InvalidOperation(
+ "Token should be created with fields to make it valid (`token` and "
+ "`expiry`), or fields to allow it to refresh (`refresh_token`, "
+ "`token_url`, `client_id`, `client_secret`)."
+ )
+
+ self._client_auth = None
+ if self._client_id:
+ self._client_auth = utils.ClientAuthentication(
+ utils.ClientAuthType.basic, self._client_id, self._client_secret
+ )
+ self._sts_client = sts.Client(self._token_url, self._client_auth)
+
+ @property
+ def info(self):
+ """Generates the serializable dictionary representation of the current
+ credentials.
+
+ Returns:
+ Mapping: The dictionary representation of the credentials. This is the
+ reverse of the "from_info" method defined in this class. It is
+ useful for serializing the current credentials so it can deserialized
+ later.
+ """
+ config_info = self.constructor_args()
+ config_info.update(type=_EXTERNAL_ACCOUNT_AUTHORIZED_USER_JSON_TYPE)
+ if config_info["expiry"]:
+ config_info["expiry"] = config_info["expiry"].isoformat() + "Z"
+
+ return {key: value for key, value in config_info.items() if value is not None}
+
+ def constructor_args(self):
+ return {
+ "audience": self._audience,
+ "refresh_token": self._refresh_token,
+ "token_url": self._token_url,
+ "token_info_url": self._token_info_url,
+ "client_id": self._client_id,
+ "client_secret": self._client_secret,
+ "token": self.token,
+ "expiry": self.expiry,
+ "revoke_url": self._revoke_url,
+ "scopes": self._scopes,
+ "quota_project_id": self._quota_project_id,
+ }
+
+ @property
+ def scopes(self):
+ """Optional[str]: The OAuth 2.0 permission scopes."""
+ return self._scopes
+
+ @property
+ def requires_scopes(self):
+ """ False: OAuth 2.0 credentials have their scopes set when
+ the initial token is requested and can not be changed."""
+ return False
+
+ @property
+ def client_id(self):
+ """Optional[str]: The OAuth 2.0 client ID."""
+ return self._client_id
+
+ @property
+ def client_secret(self):
+ """Optional[str]: The OAuth 2.0 client secret."""
+ return self._client_secret
+
+ @property
+ def audience(self):
+ """Optional[str]: The STS audience which contains the resource name for the
+ workforce pool and the provider identifier in that pool."""
+ return self._audience
+
+ @property
+ def refresh_token(self):
+ """Optional[str]: The OAuth 2.0 refresh token."""
+ return self._refresh_token
+
+ @property
+ def token_url(self):
+ """Optional[str]: The STS token exchange endpoint for refresh."""
+ return self._token_url
+
+ @property
+ def token_info_url(self):
+ """Optional[str]: The STS endpoint for token info."""
+ return self._token_info_url
+
+ @property
+ def revoke_url(self):
+ """Optional[str]: The STS endpoint for token revocation."""
+ return self._revoke_url
+
+ @property
+ def is_user(self):
+ """ True: This credential always represents a user."""
+ return True
+
+ @property
+ def can_refresh(self):
+ return all(
+ (self._refresh_token, self._token_url, self._client_id, self._client_secret)
+ )
+
+ def get_project_id(self, request=None):
+ """Retrieves the project ID corresponding to the workload identity or workforce pool.
+ For workforce pool credentials, it returns the project ID corresponding to
+ the workforce_pool_user_project.
+
+ When not determinable, None is returned.
+
+ Args:
+ request (google.auth.transport.requests.Request): Request object.
+ Unused here, but passed from _default.default().
+
+ Return:
+ str: project ID is not determinable for this credential type so it returns None
+ """
+
+ return None
+
+ def to_json(self, strip=None):
+ """Utility function that creates a JSON representation of this
+ credential.
+ Args:
+ strip (Sequence[str]): Optional list of members to exclude from the
+ generated JSON.
+ Returns:
+ str: A JSON representation of this instance. When converted into
+ a dictionary, it can be passed to from_info()
+ to create a new instance.
+ """
+ strip = strip if strip else []
+ return json.dumps({k: v for (k, v) in self.info.items() if k not in strip})
+
+ def refresh(self, request):
+ """Refreshes the access token.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the credentials could
+ not be refreshed.
+ """
+ if not self.can_refresh:
+ raise exceptions.RefreshError(
+ "The credentials do not contain the necessary fields need to "
+ "refresh the access token. You must specify refresh_token, "
+ "token_url, client_id, and client_secret."
+ )
+
+ now = _helpers.utcnow()
+ response_data = self._make_sts_request(request)
+
+ self.token = response_data.get("access_token")
+
+ lifetime = datetime.timedelta(seconds=response_data.get("expires_in"))
+ self.expiry = now + lifetime
+
+ if "refresh_token" in response_data:
+ self._refresh_token = response_data["refresh_token"]
+
+ def _make_sts_request(self, request):
+ return self._sts_client.refresh_token(request, self._refresh_token)
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ kwargs = self.constructor_args()
+ kwargs.update(quota_project_id=quota_project_id)
+ return self.__class__(**kwargs)
+
+ @_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
+ def with_token_uri(self, token_uri):
+ kwargs = self.constructor_args()
+ kwargs.update(token_url=token_uri)
+ return self.__class__(**kwargs)
+
+ @classmethod
+ def from_info(cls, info, **kwargs):
+ """Creates a Credentials instance from parsed external account info.
+
+ Args:
+ info (Mapping[str, str]): The external account info in Google
+ format.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.external_account_authorized_user.Credentials: The
+ constructed credentials.
+
+ Raises:
+ ValueError: For invalid parameters.
+ """
+ expiry = info.get("expiry")
+ if expiry:
+ expiry = datetime.datetime.strptime(
+ expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
+ )
+ return cls(
+ audience=info.get("audience"),
+ refresh_token=info.get("refresh_token"),
+ token_url=info.get("token_url"),
+ token_info_url=info.get("token_info_url"),
+ client_id=info.get("client_id"),
+ client_secret=info.get("client_secret"),
+ token=info.get("token"),
+ expiry=expiry,
+ revoke_url=info.get("revoke_url"),
+ quota_project_id=info.get("quota_project_id"),
+ scopes=info.get("scopes"),
+ **kwargs
+ )
+
+ @classmethod
+ def from_file(cls, filename, **kwargs):
+ """Creates a Credentials instance from an external account json file.
+
+ Args:
+ filename (str): The path to the external account json file.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.external_account_authorized_user.Credentials: The
+ constructed credentials.
+ """
+ with io.open(filename, "r", encoding="utf-8") as json_file:
+ data = json.load(json_file)
+ return cls.from_info(data, **kwargs)
diff --git a/contrib/python/google-auth/py3/google/auth/iam.py b/contrib/python/google-auth/py3/google/auth/iam.py
new file mode 100644
index 0000000000..e9df844178
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/iam.py
@@ -0,0 +1,99 @@
+# Copyright 2017 Google LLC
+#
+# 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.
+
+"""Tools for using the Google `Cloud Identity and Access Management (IAM)
+API`_'s auth-related functionality.
+
+.. _Cloud Identity and Access Management (IAM) API:
+ https://cloud.google.com/iam/docs/
+"""
+
+import base64
+import http.client as http_client
+import json
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import exceptions
+
+_IAM_API_ROOT_URI = "https://iamcredentials.googleapis.com/v1"
+_SIGN_BLOB_URI = _IAM_API_ROOT_URI + "/projects/-/serviceAccounts/{}:signBlob?alt=json"
+
+
+class Signer(crypt.Signer):
+ """Signs messages using the IAM `signBlob API`_.
+
+ This is useful when you need to sign bytes but do not have access to the
+ credential's private key file.
+
+ .. _signBlob API:
+ https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts
+ /signBlob
+ """
+
+ def __init__(self, request, credentials, service_account_email):
+ """
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ credentials (google.auth.credentials.Credentials): The credentials
+ that will be used to authenticate the request to the IAM API.
+ The credentials must have of one the following scopes:
+
+ - https://www.googleapis.com/auth/iam
+ - https://www.googleapis.com/auth/cloud-platform
+ service_account_email (str): The service account email identifying
+ which service account to use to sign bytes. Often, this can
+ be the same as the service account email in the given
+ credentials.
+ """
+ self._request = request
+ self._credentials = credentials
+ self._service_account_email = service_account_email
+
+ def _make_signing_request(self, message):
+ """Makes a request to the API signBlob API."""
+ message = _helpers.to_bytes(message)
+
+ method = "POST"
+ url = _SIGN_BLOB_URI.format(self._service_account_email)
+ headers = {"Content-Type": "application/json"}
+ body = json.dumps(
+ {"payload": base64.b64encode(message).decode("utf-8")}
+ ).encode("utf-8")
+
+ self._credentials.before_request(self._request, method, url, headers)
+ response = self._request(url=url, method=method, body=body, headers=headers)
+
+ if response.status != http_client.OK:
+ raise exceptions.TransportError(
+ "Error calling the IAM signBlob API: {}".format(response.data)
+ )
+
+ return json.loads(response.data.decode("utf-8"))
+
+ @property
+ def key_id(self):
+ """Optional[str]: The key ID used to identify this private key.
+
+ .. warning::
+ This is always ``None``. The key ID used by IAM can not
+ be reliably determined ahead of time.
+ """
+ return None
+
+ @_helpers.copy_docstring(crypt.Signer)
+ def sign(self, message):
+ response = self._make_signing_request(message)
+ return base64.b64decode(response["signedBlob"])
diff --git a/contrib/python/google-auth/py3/google/auth/identity_pool.py b/contrib/python/google-auth/py3/google/auth/identity_pool.py
new file mode 100644
index 0000000000..a515353c37
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/identity_pool.py
@@ -0,0 +1,261 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+
+"""Identity Pool Credentials.
+
+This module provides credentials to access Google Cloud resources from on-prem
+or non-Google Cloud platforms which support external credentials (e.g. OIDC ID
+tokens) retrieved from local file locations or local servers. This includes
+Microsoft Azure and OIDC identity providers (e.g. K8s workloads registered with
+Hub with Hub workload identity enabled).
+
+These credentials are recommended over the use of service account credentials
+in on-prem/non-Google Cloud platforms as they do not involve the management of
+long-live service account private keys.
+
+Identity Pool Credentials are initialized using external_account
+arguments which are typically loaded from an external credentials file or
+an external credentials URL. Unlike other Credentials that can be initialized
+with a list of explicit arguments, secrets or credentials, external account
+clients use the environment and hints/guidelines provided by the
+external_account JSON file to retrieve credentials and exchange them for Google
+access tokens.
+"""
+
+try:
+ from collections.abc import Mapping
+# Python 2.7 compatibility
+except ImportError: # pragma: NO COVER
+ from collections import Mapping
+import io
+import json
+import os
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import external_account
+
+
+class Credentials(external_account.Credentials):
+ """External account credentials sourced from files and URLs."""
+
+ def __init__(
+ self,
+ audience,
+ subject_token_type,
+ token_url,
+ credential_source,
+ *args,
+ **kwargs
+ ):
+ """Instantiates an external account credentials object from a file/URL.
+
+ Args:
+ audience (str): The STS audience field.
+ subject_token_type (str): The subject token type.
+ token_url (str): The STS endpoint URL.
+ credential_source (Mapping): The credential source dictionary used to
+ provide instructions on how to retrieve external credential to be
+ exchanged for Google access tokens.
+
+ Example credential_source for url-sourced credential::
+
+ {
+ "url": "http://www.example.com",
+ "format": {
+ "type": "json",
+ "subject_token_field_name": "access_token",
+ },
+ "headers": {"foo": "bar"},
+ }
+
+ Example credential_source for file-sourced credential::
+
+ {
+ "file": "/path/to/token/file.txt"
+ }
+ args (List): Optional positional arguments passed into the underlying :meth:`~external_account.Credentials.__init__` method.
+ kwargs (Mapping): Optional keyword arguments passed into the underlying :meth:`~external_account.Credentials.__init__` method.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If an error is encountered during
+ access token retrieval logic.
+ ValueError: For invalid parameters.
+
+ .. note:: Typically one of the helper constructors
+ :meth:`from_file` or
+ :meth:`from_info` are used instead of calling the constructor directly.
+ """
+
+ super(Credentials, self).__init__(
+ audience=audience,
+ subject_token_type=subject_token_type,
+ token_url=token_url,
+ credential_source=credential_source,
+ *args,
+ **kwargs
+ )
+ if not isinstance(credential_source, Mapping):
+ self._credential_source_file = None
+ self._credential_source_url = None
+ else:
+ self._credential_source_file = credential_source.get("file")
+ self._credential_source_url = credential_source.get("url")
+ self._credential_source_headers = credential_source.get("headers")
+ credential_source_format = credential_source.get("format", {})
+ # Get credential_source format type. When not provided, this
+ # defaults to text.
+ self._credential_source_format_type = (
+ credential_source_format.get("type") or "text"
+ )
+ # environment_id is only supported in AWS or dedicated future external
+ # account credentials.
+ if "environment_id" in credential_source:
+ raise exceptions.MalformedError(
+ "Invalid Identity Pool credential_source field 'environment_id'"
+ )
+ if self._credential_source_format_type not in ["text", "json"]:
+ raise exceptions.MalformedError(
+ "Invalid credential_source format '{}'".format(
+ self._credential_source_format_type
+ )
+ )
+ # For JSON types, get the required subject_token field name.
+ if self._credential_source_format_type == "json":
+ self._credential_source_field_name = credential_source_format.get(
+ "subject_token_field_name"
+ )
+ if self._credential_source_field_name is None:
+ raise exceptions.MalformedError(
+ "Missing subject_token_field_name for JSON credential_source format"
+ )
+ else:
+ self._credential_source_field_name = None
+
+ if self._credential_source_file and self._credential_source_url:
+ raise exceptions.MalformedError(
+ "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
+ )
+ if not self._credential_source_file and not self._credential_source_url:
+ raise exceptions.MalformedError(
+ "Missing credential_source. A 'file' or 'url' must be provided."
+ )
+
+ @_helpers.copy_docstring(external_account.Credentials)
+ def retrieve_subject_token(self, request):
+ return self._parse_token_data(
+ self._get_token_data(request),
+ self._credential_source_format_type,
+ self._credential_source_field_name,
+ )
+
+ def _get_token_data(self, request):
+ if self._credential_source_file:
+ return self._get_file_data(self._credential_source_file)
+ else:
+ return self._get_url_data(
+ request, self._credential_source_url, self._credential_source_headers
+ )
+
+ def _get_file_data(self, filename):
+ if not os.path.exists(filename):
+ raise exceptions.RefreshError("File '{}' was not found.".format(filename))
+
+ with io.open(filename, "r", encoding="utf-8") as file_obj:
+ return file_obj.read(), filename
+
+ def _get_url_data(self, request, url, headers):
+ response = request(url=url, method="GET", headers=headers)
+
+ # support both string and bytes type response.data
+ response_body = (
+ response.data.decode("utf-8")
+ if hasattr(response.data, "decode")
+ else response.data
+ )
+
+ if response.status != 200:
+ raise exceptions.RefreshError(
+ "Unable to retrieve Identity Pool subject token", response_body
+ )
+
+ return response_body, url
+
+ def _parse_token_data(
+ self, token_content, format_type="text", subject_token_field_name=None
+ ):
+ content, filename = token_content
+ if format_type == "text":
+ token = content
+ else:
+ try:
+ # Parse file content as JSON.
+ response_data = json.loads(content)
+ # Get the subject_token.
+ token = response_data[subject_token_field_name]
+ except (KeyError, ValueError):
+ raise exceptions.RefreshError(
+ "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+ filename, subject_token_field_name
+ )
+ )
+ if not token:
+ raise exceptions.RefreshError(
+ "Missing subject_token in the credential_source file"
+ )
+ return token
+
+ def _create_default_metrics_options(self):
+ metrics_options = super(Credentials, self)._create_default_metrics_options()
+ # Check that credential source is a dict before checking for file vs url. This check needs to be done
+ # here because the external_account credential constructor needs to pass the metrics options to the
+ # impersonated credential object before the identity_pool credentials are validated.
+ if isinstance(self._credential_source, Mapping):
+ if self._credential_source.get("file"):
+ metrics_options["source"] = "file"
+ else:
+ metrics_options["source"] = "url"
+ return metrics_options
+
+ @classmethod
+ def from_info(cls, info, **kwargs):
+ """Creates an Identity Pool Credentials instance from parsed external account info.
+
+ Args:
+ info (Mapping[str, str]): The Identity Pool external account info in Google
+ format.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.identity_pool.Credentials: The constructed
+ credentials.
+
+ Raises:
+ ValueError: For invalid parameters.
+ """
+ return super(Credentials, cls).from_info(info, **kwargs)
+
+ @classmethod
+ def from_file(cls, filename, **kwargs):
+ """Creates an IdentityPool Credentials instance from an external account json file.
+
+ Args:
+ filename (str): The path to the IdentityPool external account json file.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.identity_pool.Credentials: The constructed
+ credentials.
+ """
+ return super(Credentials, cls).from_file(filename, **kwargs)
diff --git a/contrib/python/google-auth/py3/google/auth/impersonated_credentials.py b/contrib/python/google-auth/py3/google/auth/impersonated_credentials.py
new file mode 100644
index 0000000000..c272a3ca28
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/impersonated_credentials.py
@@ -0,0 +1,462 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Google Cloud Impersonated credentials.
+
+This module provides authentication for applications where local credentials
+impersonates a remote service account using `IAM Credentials API`_.
+
+This class can be used to impersonate a service account as long as the original
+Credential object has the "Service Account Token Creator" role on the target
+service account.
+
+ .. _IAM Credentials API:
+ https://cloud.google.com/iam/credentials/reference/rest/
+"""
+
+import base64
+import copy
+from datetime import datetime
+import http.client as http_client
+import json
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth import metrics
+
+_IAM_SCOPE = ["https://www.googleapis.com/auth/iam"]
+
+_IAM_ENDPOINT = (
+ "https://iamcredentials.googleapis.com/v1/projects/-"
+ + "/serviceAccounts/{}:generateAccessToken"
+)
+
+_IAM_SIGN_ENDPOINT = (
+ "https://iamcredentials.googleapis.com/v1/projects/-"
+ + "/serviceAccounts/{}:signBlob"
+)
+
+_IAM_IDTOKEN_ENDPOINT = (
+ "https://iamcredentials.googleapis.com/v1/"
+ + "projects/-/serviceAccounts/{}:generateIdToken"
+)
+
+_REFRESH_ERROR = "Unable to acquire impersonated credentials"
+
+_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
+
+_DEFAULT_TOKEN_URI = "https://oauth2.googleapis.com/token"
+
+
+def _make_iam_token_request(
+ request, principal, headers, body, iam_endpoint_override=None
+):
+ """Makes a request to the Google Cloud IAM service for an access token.
+ Args:
+ request (Request): The Request object to use.
+ principal (str): The principal to request an access token for.
+ headers (Mapping[str, str]): Map of headers to transmit.
+ body (Mapping[str, str]): JSON Payload body for the iamcredentials
+ API call.
+ iam_endpoint_override (Optiona[str]): The full IAM endpoint override
+ with the target_principal embedded. This is useful when supporting
+ impersonation with regional endpoints.
+
+ Raises:
+ google.auth.exceptions.TransportError: Raised if there is an underlying
+ HTTP connection error
+ google.auth.exceptions.RefreshError: Raised if the impersonated
+ credentials are not available. Common reasons are
+ `iamcredentials.googleapis.com` is not enabled or the
+ `Service Account Token Creator` is not assigned
+ """
+ iam_endpoint = iam_endpoint_override or _IAM_ENDPOINT.format(principal)
+
+ body = json.dumps(body).encode("utf-8")
+
+ response = request(url=iam_endpoint, method="POST", headers=headers, body=body)
+
+ # support both string and bytes type response.data
+ response_body = (
+ response.data.decode("utf-8")
+ if hasattr(response.data, "decode")
+ else response.data
+ )
+
+ if response.status != http_client.OK:
+ raise exceptions.RefreshError(_REFRESH_ERROR, response_body)
+
+ try:
+ token_response = json.loads(response_body)
+ token = token_response["accessToken"]
+ expiry = datetime.strptime(token_response["expireTime"], "%Y-%m-%dT%H:%M:%SZ")
+
+ return token, expiry
+
+ except (KeyError, ValueError) as caught_exc:
+ new_exc = exceptions.RefreshError(
+ "{}: No access token or invalid expiration in response.".format(
+ _REFRESH_ERROR
+ ),
+ response_body,
+ )
+ raise new_exc from caught_exc
+
+
+class Credentials(
+ credentials.Scoped, credentials.CredentialsWithQuotaProject, credentials.Signing
+):
+ """This module defines impersonated credentials which are essentially
+ impersonated identities.
+
+ Impersonated Credentials allows credentials issued to a user or
+ service account to impersonate another. The target service account must
+ grant the originating credential principal the
+ `Service Account Token Creator`_ IAM role:
+
+ For more information about Token Creator IAM role and
+ IAMCredentials API, see
+ `Creating Short-Lived Service Account Credentials`_.
+
+ .. _Service Account Token Creator:
+ https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role
+
+ .. _Creating Short-Lived Service Account Credentials:
+ https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials
+
+ Usage:
+
+ First grant source_credentials the `Service Account Token Creator`
+ role on the target account to impersonate. In this example, the
+ service account represented by svc_account.json has the
+ token creator role on
+ `impersonated-account@_project_.iam.gserviceaccount.com`.
+
+ Enable the IAMCredentials API on the source project:
+ `gcloud services enable iamcredentials.googleapis.com`.
+
+ Initialize a source credential which does not have access to
+ list bucket::
+
+ from google.oauth2 import service_account
+
+ target_scopes = [
+ 'https://www.googleapis.com/auth/devstorage.read_only']
+
+ source_credentials = (
+ service_account.Credentials.from_service_account_file(
+ '/path/to/svc_account.json',
+ scopes=target_scopes))
+
+ Now use the source credentials to acquire credentials to impersonate
+ another service account::
+
+ from google.auth import impersonated_credentials
+
+ target_credentials = impersonated_credentials.Credentials(
+ source_credentials=source_credentials,
+ target_principal='impersonated-account@_project_.iam.gserviceaccount.com',
+ target_scopes = target_scopes,
+ lifetime=500)
+
+ Resource access is granted::
+
+ client = storage.Client(credentials=target_credentials)
+ buckets = client.list_buckets(project='your_project')
+ for bucket in buckets:
+ print(bucket.name)
+ """
+
+ def __init__(
+ self,
+ source_credentials,
+ target_principal,
+ target_scopes,
+ delegates=None,
+ lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
+ quota_project_id=None,
+ iam_endpoint_override=None,
+ ):
+ """
+ Args:
+ source_credentials (google.auth.Credentials): The source credential
+ used as to acquire the impersonated credentials.
+ target_principal (str): The service account to impersonate.
+ target_scopes (Sequence[str]): Scopes to request during the
+ authorization grant.
+ delegates (Sequence[str]): The chained list of delegates required
+ to grant the final access_token. If set, the sequence of
+ identities must have "Service Account Token Creator" capability
+ granted to the prceeding identity. For example, if set to
+ [serviceAccountB, serviceAccountC], the source_credential
+ must have the Token Creator role on serviceAccountB.
+ serviceAccountB must have the Token Creator on
+ serviceAccountC.
+ Finally, C must have Token Creator on target_principal.
+ If left unset, source_credential must have that role on
+ target_principal.
+ lifetime (int): Number of seconds the delegated credential should
+ be valid for (upto 3600).
+ quota_project_id (Optional[str]): The project ID used for quota and billing.
+ This project may be different from the project used to
+ create the credentials.
+ iam_endpoint_override (Optiona[str]): The full IAM endpoint override
+ with the target_principal embedded. This is useful when supporting
+ impersonation with regional endpoints.
+ """
+
+ super(Credentials, self).__init__()
+
+ self._source_credentials = copy.copy(source_credentials)
+ # Service account source credentials must have the _IAM_SCOPE
+ # added to refresh correctly. User credentials cannot have
+ # their original scopes modified.
+ if isinstance(self._source_credentials, credentials.Scoped):
+ self._source_credentials = self._source_credentials.with_scopes(_IAM_SCOPE)
+ # If the source credential is service account and self signed jwt
+ # is needed, we need to create a jwt credential inside it
+ if (
+ hasattr(self._source_credentials, "_create_self_signed_jwt")
+ and self._source_credentials._always_use_jwt_access
+ ):
+ self._source_credentials._create_self_signed_jwt(None)
+ self._target_principal = target_principal
+ self._target_scopes = target_scopes
+ self._delegates = delegates
+ self._lifetime = lifetime or _DEFAULT_TOKEN_LIFETIME_SECS
+ self.token = None
+ self.expiry = _helpers.utcnow()
+ self._quota_project_id = quota_project_id
+ self._iam_endpoint_override = iam_endpoint_override
+
+ def _metric_header_for_usage(self):
+ return metrics.CRED_TYPE_SA_IMPERSONATE
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def refresh(self, request):
+ self._update_token(request)
+
+ def _update_token(self, request):
+ """Updates credentials with a new access_token representing
+ the impersonated account.
+
+ Args:
+ request (google.auth.transport.requests.Request): Request object
+ to use for refreshing credentials.
+ """
+
+ # Refresh our source credentials if it is not valid.
+ if not self._source_credentials.valid:
+ self._source_credentials.refresh(request)
+
+ body = {
+ "delegates": self._delegates,
+ "scope": self._target_scopes,
+ "lifetime": str(self._lifetime) + "s",
+ }
+
+ headers = {
+ "Content-Type": "application/json",
+ metrics.API_CLIENT_HEADER: metrics.token_request_access_token_impersonate(),
+ }
+
+ # Apply the source credentials authentication info.
+ self._source_credentials.apply(headers)
+
+ self.token, self.expiry = _make_iam_token_request(
+ request=request,
+ principal=self._target_principal,
+ headers=headers,
+ body=body,
+ iam_endpoint_override=self._iam_endpoint_override,
+ )
+
+ def sign_bytes(self, message):
+ from google.auth.transport.requests import AuthorizedSession
+
+ iam_sign_endpoint = _IAM_SIGN_ENDPOINT.format(self._target_principal)
+
+ body = {
+ "payload": base64.b64encode(message).decode("utf-8"),
+ "delegates": self._delegates,
+ }
+
+ headers = {"Content-Type": "application/json"}
+
+ authed_session = AuthorizedSession(self._source_credentials)
+
+ try:
+ response = authed_session.post(
+ url=iam_sign_endpoint, headers=headers, json=body
+ )
+ finally:
+ authed_session.close()
+
+ if response.status_code != http_client.OK:
+ raise exceptions.TransportError(
+ "Error calling sign_bytes: {}".format(response.json())
+ )
+
+ return base64.b64decode(response.json()["signedBlob"])
+
+ @property
+ def signer_email(self):
+ return self._target_principal
+
+ @property
+ def service_account_email(self):
+ return self._target_principal
+
+ @property
+ def signer(self):
+ return self
+
+ @property
+ def requires_scopes(self):
+ return not self._target_scopes
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ return self.__class__(
+ self._source_credentials,
+ target_principal=self._target_principal,
+ target_scopes=self._target_scopes,
+ delegates=self._delegates,
+ lifetime=self._lifetime,
+ quota_project_id=quota_project_id,
+ iam_endpoint_override=self._iam_endpoint_override,
+ )
+
+ @_helpers.copy_docstring(credentials.Scoped)
+ def with_scopes(self, scopes, default_scopes=None):
+ return self.__class__(
+ self._source_credentials,
+ target_principal=self._target_principal,
+ target_scopes=scopes or default_scopes,
+ delegates=self._delegates,
+ lifetime=self._lifetime,
+ quota_project_id=self._quota_project_id,
+ iam_endpoint_override=self._iam_endpoint_override,
+ )
+
+
+class IDTokenCredentials(credentials.CredentialsWithQuotaProject):
+ """Open ID Connect ID Token-based service account credentials.
+
+ """
+
+ def __init__(
+ self,
+ target_credentials,
+ target_audience=None,
+ include_email=False,
+ quota_project_id=None,
+ ):
+ """
+ Args:
+ target_credentials (google.auth.Credentials): The target
+ credential used as to acquire the id tokens for.
+ target_audience (string): Audience to issue the token for.
+ include_email (bool): Include email in IdToken
+ quota_project_id (Optional[str]): The project ID used for
+ quota and billing.
+ """
+ super(IDTokenCredentials, self).__init__()
+
+ if not isinstance(target_credentials, Credentials):
+ raise exceptions.GoogleAuthError(
+ "Provided Credential must be " "impersonated_credentials"
+ )
+ self._target_credentials = target_credentials
+ self._target_audience = target_audience
+ self._include_email = include_email
+ self._quota_project_id = quota_project_id
+
+ def from_credentials(self, target_credentials, target_audience=None):
+ return self.__class__(
+ target_credentials=target_credentials,
+ target_audience=target_audience,
+ include_email=self._include_email,
+ quota_project_id=self._quota_project_id,
+ )
+
+ def with_target_audience(self, target_audience):
+ return self.__class__(
+ target_credentials=self._target_credentials,
+ target_audience=target_audience,
+ include_email=self._include_email,
+ quota_project_id=self._quota_project_id,
+ )
+
+ def with_include_email(self, include_email):
+ return self.__class__(
+ target_credentials=self._target_credentials,
+ target_audience=self._target_audience,
+ include_email=include_email,
+ quota_project_id=self._quota_project_id,
+ )
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ return self.__class__(
+ target_credentials=self._target_credentials,
+ target_audience=self._target_audience,
+ include_email=self._include_email,
+ quota_project_id=quota_project_id,
+ )
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def refresh(self, request):
+ from google.auth.transport.requests import AuthorizedSession
+
+ iam_sign_endpoint = _IAM_IDTOKEN_ENDPOINT.format(
+ self._target_credentials.signer_email
+ )
+
+ body = {
+ "audience": self._target_audience,
+ "delegates": self._target_credentials._delegates,
+ "includeEmail": self._include_email,
+ }
+
+ headers = {
+ "Content-Type": "application/json",
+ metrics.API_CLIENT_HEADER: metrics.token_request_id_token_impersonate(),
+ }
+
+ authed_session = AuthorizedSession(
+ self._target_credentials._source_credentials, auth_request=request
+ )
+
+ try:
+ response = authed_session.post(
+ url=iam_sign_endpoint,
+ headers=headers,
+ data=json.dumps(body).encode("utf-8"),
+ )
+ finally:
+ authed_session.close()
+
+ if response.status_code != http_client.OK:
+ raise exceptions.RefreshError(
+ "Error getting ID token: {}".format(response.json())
+ )
+
+ id_token = response.json()["token"]
+ self.token = id_token
+ self.expiry = datetime.utcfromtimestamp(
+ jwt.decode(id_token, verify=False)["exp"]
+ )
diff --git a/contrib/python/google-auth/py3/google/auth/jwt.py b/contrib/python/google-auth/py3/google/auth/jwt.py
new file mode 100644
index 0000000000..1ebd565d4e
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/jwt.py
@@ -0,0 +1,878 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""JSON Web Tokens
+
+Provides support for creating (encoding) and verifying (decoding) JWTs,
+especially JWTs generated and consumed by Google infrastructure.
+
+See `rfc7519`_ for more details on JWTs.
+
+To encode a JWT use :func:`encode`::
+
+ from google.auth import crypt
+ from google.auth import jwt
+
+ signer = crypt.Signer(private_key)
+ payload = {'some': 'payload'}
+ encoded = jwt.encode(signer, payload)
+
+To decode a JWT and verify claims use :func:`decode`::
+
+ claims = jwt.decode(encoded, certs=public_certs)
+
+You can also skip verification::
+
+ claims = jwt.decode(encoded, verify=False)
+
+.. _rfc7519: https://tools.ietf.org/html/rfc7519
+
+"""
+
+try:
+ from collections.abc import Mapping
+# Python 2.7 compatibility
+except ImportError: # pragma: NO COVER
+ from collections import Mapping # type: ignore
+import copy
+import datetime
+import json
+import urllib
+
+import cachetools
+
+from google.auth import _helpers
+from google.auth import _service_account_info
+from google.auth import crypt
+from google.auth import exceptions
+import google.auth.credentials
+
+try:
+ from google.auth.crypt import es256
+except ImportError: # pragma: NO COVER
+ es256 = None # type: ignore
+
+_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
+_DEFAULT_MAX_CACHE_SIZE = 10
+_ALGORITHM_TO_VERIFIER_CLASS = {"RS256": crypt.RSAVerifier}
+_CRYPTOGRAPHY_BASED_ALGORITHMS = frozenset(["ES256"])
+
+if es256 is not None: # pragma: NO COVER
+ _ALGORITHM_TO_VERIFIER_CLASS["ES256"] = es256.ES256Verifier # type: ignore
+
+
+def encode(signer, payload, header=None, key_id=None):
+ """Make a signed JWT.
+
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign the JWT.
+ payload (Mapping[str, str]): The JWT payload.
+ header (Mapping[str, str]): Additional JWT header payload.
+ key_id (str): The key id to add to the JWT header. If the
+ signer has a key id it will be used as the default. If this is
+ specified it will override the signer's key id.
+
+ Returns:
+ bytes: The encoded JWT.
+ """
+ if header is None:
+ header = {}
+
+ if key_id is None:
+ key_id = signer.key_id
+
+ header.update({"typ": "JWT"})
+
+ if "alg" not in header:
+ if es256 is not None and isinstance(signer, es256.ES256Signer):
+ header.update({"alg": "ES256"})
+ else:
+ header.update({"alg": "RS256"})
+
+ if key_id is not None:
+ header["kid"] = key_id
+
+ segments = [
+ _helpers.unpadded_urlsafe_b64encode(json.dumps(header).encode("utf-8")),
+ _helpers.unpadded_urlsafe_b64encode(json.dumps(payload).encode("utf-8")),
+ ]
+
+ signing_input = b".".join(segments)
+ signature = signer.sign(signing_input)
+ segments.append(_helpers.unpadded_urlsafe_b64encode(signature))
+
+ return b".".join(segments)
+
+
+def _decode_jwt_segment(encoded_section):
+ """Decodes a single JWT segment."""
+ section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section)
+ try:
+ return json.loads(section_bytes.decode("utf-8"))
+ except ValueError as caught_exc:
+ new_exc = exceptions.MalformedError(
+ "Can't parse segment: {0}".format(section_bytes)
+ )
+ raise new_exc from caught_exc
+
+
+def _unverified_decode(token):
+ """Decodes a token and does no verification.
+
+ Args:
+ token (Union[str, bytes]): The encoded JWT.
+
+ Returns:
+ Tuple[Mapping, Mapping, str, str]: header, payload, signed_section, and
+ signature.
+
+ Raises:
+ google.auth.exceptions.MalformedError: if there are an incorrect amount of segments in the token or segments of the wrong type.
+ """
+ token = _helpers.to_bytes(token)
+
+ if token.count(b".") != 2:
+ raise exceptions.MalformedError(
+ "Wrong number of segments in token: {0}".format(token)
+ )
+
+ encoded_header, encoded_payload, signature = token.split(b".")
+ signed_section = encoded_header + b"." + encoded_payload
+ signature = _helpers.padded_urlsafe_b64decode(signature)
+
+ # Parse segments
+ header = _decode_jwt_segment(encoded_header)
+ payload = _decode_jwt_segment(encoded_payload)
+
+ if not isinstance(header, Mapping):
+ raise exceptions.MalformedError(
+ "Header segment should be a JSON object: {0}".format(encoded_header)
+ )
+
+ if not isinstance(payload, Mapping):
+ raise exceptions.MalformedError(
+ "Payload segment should be a JSON object: {0}".format(encoded_payload)
+ )
+
+ return header, payload, signed_section, signature
+
+
+def decode_header(token):
+ """Return the decoded header of a token.
+
+ No verification is done. This is useful to extract the key id from
+ the header in order to acquire the appropriate certificate to verify
+ the token.
+
+ Args:
+ token (Union[str, bytes]): the encoded JWT.
+
+ Returns:
+ Mapping: The decoded JWT header.
+ """
+ header, _, _, _ = _unverified_decode(token)
+ return header
+
+
+def _verify_iat_and_exp(payload, clock_skew_in_seconds=0):
+ """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
+ payload.
+
+ Args:
+ payload (Mapping[str, str]): The JWT payload.
+ clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+ validation.
+
+ Raises:
+ google.auth.exceptions.InvalidValue: if value validation failed.
+ google.auth.exceptions.MalformedError: if schema validation failed.
+ """
+ now = _helpers.datetime_to_secs(_helpers.utcnow())
+
+ # Make sure the iat and exp claims are present.
+ for key in ("iat", "exp"):
+ if key not in payload:
+ raise exceptions.MalformedError(
+ "Token does not contain required claim {}".format(key)
+ )
+
+ # Make sure the token wasn't issued in the future.
+ iat = payload["iat"]
+ # Err on the side of accepting a token that is slightly early to account
+ # for clock skew.
+ earliest = iat - clock_skew_in_seconds
+ if now < earliest:
+ raise exceptions.InvalidValue(
+ "Token used too early, {} < {}. Check that your computer's clock is set correctly.".format(
+ now, iat
+ )
+ )
+
+ # Make sure the token wasn't issued in the past.
+ exp = payload["exp"]
+ # Err on the side of accepting a token that is slightly out of date
+ # to account for clow skew.
+ latest = exp + clock_skew_in_seconds
+ if latest < now:
+ raise exceptions.InvalidValue("Token expired, {} < {}".format(latest, now))
+
+
+def decode(token, certs=None, verify=True, audience=None, clock_skew_in_seconds=0):
+ """Decode and verify a JWT.
+
+ Args:
+ token (str): The encoded JWT.
+ certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
+ certificate used to validate the JWT signature. If bytes or string,
+ it must the the public key certificate in PEM format. If a mapping,
+ it must be a mapping of key IDs to public key certificates in PEM
+ format. The mapping must contain the same key ID that's specified
+ in the token's header.
+ verify (bool): Whether to perform signature and claim validation.
+ Verification is done by default.
+ audience (str or list): The audience claim, 'aud', that this JWT should
+ contain. Or a list of audience claims. If None then the JWT's 'aud'
+ parameter is not verified.
+ clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+ validation.
+
+ Returns:
+ Mapping[str, str]: The deserialized JSON payload in the JWT.
+
+ Raises:
+ google.auth.exceptions.InvalidValue: if value validation failed.
+ google.auth.exceptions.MalformedError: if schema validation failed.
+ """
+ header, payload, signed_section, signature = _unverified_decode(token)
+
+ if not verify:
+ return payload
+
+ # Pluck the key id and algorithm from the header and make sure we have
+ # a verifier that can support it.
+ key_alg = header.get("alg")
+ key_id = header.get("kid")
+
+ try:
+ verifier_cls = _ALGORITHM_TO_VERIFIER_CLASS[key_alg]
+ except KeyError as exc:
+ if key_alg in _CRYPTOGRAPHY_BASED_ALGORITHMS:
+ raise exceptions.InvalidValue(
+ "The key algorithm {} requires the cryptography package to be installed.".format(
+ key_alg
+ )
+ ) from exc
+ else:
+ raise exceptions.InvalidValue(
+ "Unsupported signature algorithm {}".format(key_alg)
+ ) from exc
+ # If certs is specified as a dictionary of key IDs to certificates, then
+ # use the certificate identified by the key ID in the token header.
+ if isinstance(certs, Mapping):
+ if key_id:
+ if key_id not in certs:
+ raise exceptions.MalformedError(
+ "Certificate for key id {} not found.".format(key_id)
+ )
+ certs_to_check = [certs[key_id]]
+ # If there's no key id in the header, check against all of the certs.
+ else:
+ certs_to_check = certs.values()
+ else:
+ certs_to_check = certs
+
+ # Verify that the signature matches the message.
+ if not crypt.verify_signature(
+ signed_section, signature, certs_to_check, verifier_cls
+ ):
+ raise exceptions.MalformedError("Could not verify token signature.")
+
+ # Verify the issued at and created times in the payload.
+ _verify_iat_and_exp(payload, clock_skew_in_seconds)
+
+ # Check audience.
+ if audience is not None:
+ claim_audience = payload.get("aud")
+ if isinstance(audience, str):
+ audience = [audience]
+ if claim_audience not in audience:
+ raise exceptions.InvalidValue(
+ "Token has wrong audience {}, expected one of {}".format(
+ claim_audience, audience
+ )
+ )
+
+ return payload
+
+
+class Credentials(
+ google.auth.credentials.Signing, google.auth.credentials.CredentialsWithQuotaProject
+):
+ """Credentials that use a JWT as the bearer token.
+
+ These credentials require an "audience" claim. This claim identifies the
+ intended recipient of the bearer token.
+
+ The constructor arguments determine the claims for the JWT that is
+ sent with requests. Usually, you'll construct these credentials with
+ one of the helper constructors as shown in the next section.
+
+ To create JWT credentials using a Google service account private key
+ JSON file::
+
+ audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
+ credentials = jwt.Credentials.from_service_account_file(
+ 'service-account.json',
+ audience=audience)
+
+ If you already have the service account file loaded and parsed::
+
+ service_account_info = json.load(open('service_account.json'))
+ credentials = jwt.Credentials.from_service_account_info(
+ service_account_info,
+ audience=audience)
+
+ Both helper methods pass on arguments to the constructor, so you can
+ specify the JWT claims::
+
+ credentials = jwt.Credentials.from_service_account_file(
+ 'service-account.json',
+ audience=audience,
+ additional_claims={'meta': 'data'})
+
+ You can also construct the credentials directly if you have a
+ :class:`~google.auth.crypt.Signer` instance::
+
+ credentials = jwt.Credentials(
+ signer,
+ issuer='your-issuer',
+ subject='your-subject',
+ audience=audience)
+
+ The claims are considered immutable. If you want to modify the claims,
+ you can easily create another instance using :meth:`with_claims`::
+
+ new_audience = (
+ 'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
+ new_credentials = credentials.with_claims(audience=new_audience)
+ """
+
+ def __init__(
+ self,
+ signer,
+ issuer,
+ subject,
+ audience,
+ additional_claims=None,
+ token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
+ quota_project_id=None,
+ ):
+ """
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ issuer (str): The `iss` claim.
+ subject (str): The `sub` claim.
+ audience (str): the `aud` claim. The intended audience for the
+ credentials.
+ additional_claims (Mapping[str, str]): Any additional claims for
+ the JWT payload.
+ token_lifetime (int): The amount of time in seconds for
+ which the token is valid. Defaults to 1 hour.
+ quota_project_id (Optional[str]): The project ID used for quota
+ and billing.
+ """
+ super(Credentials, self).__init__()
+ self._signer = signer
+ self._issuer = issuer
+ self._subject = subject
+ self._audience = audience
+ self._token_lifetime = token_lifetime
+ self._quota_project_id = quota_project_id
+
+ if additional_claims is None:
+ additional_claims = {}
+
+ self._additional_claims = additional_claims
+
+ @classmethod
+ def _from_signer_and_info(cls, signer, info, **kwargs):
+ """Creates a Credentials instance from a signer and service account
+ info.
+
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ info (Mapping[str, str]): The service account info.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.jwt.Credentials: The constructed credentials.
+
+ Raises:
+ google.auth.exceptions.MalformedError: If the info is not in the expected format.
+ """
+ kwargs.setdefault("subject", info["client_email"])
+ kwargs.setdefault("issuer", info["client_email"])
+ return cls(signer, **kwargs)
+
+ @classmethod
+ def from_service_account_info(cls, info, **kwargs):
+ """Creates an Credentials instance from a dictionary.
+
+ Args:
+ info (Mapping[str, str]): The service account info in Google
+ format.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.jwt.Credentials: The constructed credentials.
+
+ Raises:
+ google.auth.exceptions.MalformedError: If the info is not in the expected format.
+ """
+ signer = _service_account_info.from_dict(info, require=["client_email"])
+ return cls._from_signer_and_info(signer, info, **kwargs)
+
+ @classmethod
+ def from_service_account_file(cls, filename, **kwargs):
+ """Creates a Credentials instance from a service account .json file
+ in Google format.
+
+ Args:
+ filename (str): The path to the service account .json file.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.jwt.Credentials: The constructed credentials.
+ """
+ info, signer = _service_account_info.from_filename(
+ filename, require=["client_email"]
+ )
+ return cls._from_signer_and_info(signer, info, **kwargs)
+
+ @classmethod
+ def from_signing_credentials(cls, credentials, audience, **kwargs):
+ """Creates a new :class:`google.auth.jwt.Credentials` instance from an
+ existing :class:`google.auth.credentials.Signing` instance.
+
+ The new instance will use the same signer as the existing instance and
+ will use the existing instance's signer email as the issuer and
+ subject by default.
+
+ Example::
+
+ svc_creds = service_account.Credentials.from_service_account_file(
+ 'service_account.json')
+ audience = (
+ 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher')
+ jwt_creds = jwt.Credentials.from_signing_credentials(
+ svc_creds, audience=audience)
+
+ Args:
+ credentials (google.auth.credentials.Signing): The credentials to
+ use to construct the new credentials.
+ audience (str): the `aud` claim. The intended audience for the
+ credentials.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.jwt.Credentials: A new Credentials instance.
+ """
+ kwargs.setdefault("issuer", credentials.signer_email)
+ kwargs.setdefault("subject", credentials.signer_email)
+ return cls(credentials.signer, audience=audience, **kwargs)
+
+ def with_claims(
+ self, issuer=None, subject=None, audience=None, additional_claims=None
+ ):
+ """Returns a copy of these credentials with modified claims.
+
+ Args:
+ issuer (str): The `iss` claim. If unspecified the current issuer
+ claim will be used.
+ subject (str): The `sub` claim. If unspecified the current subject
+ claim will be used.
+ audience (str): the `aud` claim. If unspecified the current
+ audience claim will be used.
+ additional_claims (Mapping[str, str]): Any additional claims for
+ the JWT payload. This will be merged with the current
+ additional claims.
+
+ Returns:
+ google.auth.jwt.Credentials: A new credentials instance.
+ """
+ new_additional_claims = copy.deepcopy(self._additional_claims)
+ new_additional_claims.update(additional_claims or {})
+
+ return self.__class__(
+ self._signer,
+ issuer=issuer if issuer is not None else self._issuer,
+ subject=subject if subject is not None else self._subject,
+ audience=audience if audience is not None else self._audience,
+ additional_claims=new_additional_claims,
+ quota_project_id=self._quota_project_id,
+ )
+
+ @_helpers.copy_docstring(google.auth.credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ return self.__class__(
+ self._signer,
+ issuer=self._issuer,
+ subject=self._subject,
+ audience=self._audience,
+ additional_claims=self._additional_claims,
+ quota_project_id=quota_project_id,
+ )
+
+ def _make_jwt(self):
+ """Make a signed JWT.
+
+ Returns:
+ Tuple[bytes, datetime]: The encoded JWT and the expiration.
+ """
+ now = _helpers.utcnow()
+ lifetime = datetime.timedelta(seconds=self._token_lifetime)
+ expiry = now + lifetime
+
+ payload = {
+ "iss": self._issuer,
+ "sub": self._subject,
+ "iat": _helpers.datetime_to_secs(now),
+ "exp": _helpers.datetime_to_secs(expiry),
+ }
+ if self._audience:
+ payload["aud"] = self._audience
+
+ payload.update(self._additional_claims)
+
+ jwt = encode(self._signer, payload)
+
+ return jwt, expiry
+
+ def refresh(self, request):
+ """Refreshes the access token.
+
+ Args:
+ request (Any): Unused.
+ """
+ # pylint: disable=unused-argument
+ # (pylint doesn't correctly recognize overridden methods.)
+ self.token, self.expiry = self._make_jwt()
+
+ @_helpers.copy_docstring(google.auth.credentials.Signing)
+ def sign_bytes(self, message):
+ return self._signer.sign(message)
+
+ @property # type: ignore
+ @_helpers.copy_docstring(google.auth.credentials.Signing)
+ def signer_email(self):
+ return self._issuer
+
+ @property # type: ignore
+ @_helpers.copy_docstring(google.auth.credentials.Signing)
+ def signer(self):
+ return self._signer
+
+ @property # type: ignore
+ def additional_claims(self):
+ """ Additional claims the JWT object was created with."""
+ return self._additional_claims
+
+
+class OnDemandCredentials(
+ google.auth.credentials.Signing, google.auth.credentials.CredentialsWithQuotaProject
+):
+ """On-demand JWT credentials.
+
+ Like :class:`Credentials`, this class uses a JWT as the bearer token for
+ authentication. However, this class does not require the audience at
+ construction time. Instead, it will generate a new token on-demand for
+ each request using the request URI as the audience. It caches tokens
+ so that multiple requests to the same URI do not incur the overhead
+ of generating a new token every time.
+
+ This behavior is especially useful for `gRPC`_ clients. A gRPC service may
+ have multiple audience and gRPC clients may not know all of the audiences
+ required for accessing a particular service. With these credentials,
+ no knowledge of the audiences is required ahead of time.
+
+ .. _grpc: http://www.grpc.io/
+ """
+
+ def __init__(
+ self,
+ signer,
+ issuer,
+ subject,
+ additional_claims=None,
+ token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
+ max_cache_size=_DEFAULT_MAX_CACHE_SIZE,
+ quota_project_id=None,
+ ):
+ """
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ issuer (str): The `iss` claim.
+ subject (str): The `sub` claim.
+ additional_claims (Mapping[str, str]): Any additional claims for
+ the JWT payload.
+ token_lifetime (int): The amount of time in seconds for
+ which the token is valid. Defaults to 1 hour.
+ max_cache_size (int): The maximum number of JWT tokens to keep in
+ cache. Tokens are cached using :class:`cachetools.LRUCache`.
+ quota_project_id (Optional[str]): The project ID used for quota
+ and billing.
+
+ """
+ super(OnDemandCredentials, self).__init__()
+ self._signer = signer
+ self._issuer = issuer
+ self._subject = subject
+ self._token_lifetime = token_lifetime
+ self._quota_project_id = quota_project_id
+
+ if additional_claims is None:
+ additional_claims = {}
+
+ self._additional_claims = additional_claims
+ self._cache = cachetools.LRUCache(maxsize=max_cache_size)
+
+ @classmethod
+ def _from_signer_and_info(cls, signer, info, **kwargs):
+ """Creates an OnDemandCredentials instance from a signer and service
+ account info.
+
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ info (Mapping[str, str]): The service account info.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.jwt.OnDemandCredentials: The constructed credentials.
+
+ Raises:
+ google.auth.exceptions.MalformedError: If the info is not in the expected format.
+ """
+ kwargs.setdefault("subject", info["client_email"])
+ kwargs.setdefault("issuer", info["client_email"])
+ return cls(signer, **kwargs)
+
+ @classmethod
+ def from_service_account_info(cls, info, **kwargs):
+ """Creates an OnDemandCredentials instance from a dictionary.
+
+ Args:
+ info (Mapping[str, str]): The service account info in Google
+ format.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.jwt.OnDemandCredentials: The constructed credentials.
+
+ Raises:
+ google.auth.exceptions.MalformedError: If the info is not in the expected format.
+ """
+ signer = _service_account_info.from_dict(info, require=["client_email"])
+ return cls._from_signer_and_info(signer, info, **kwargs)
+
+ @classmethod
+ def from_service_account_file(cls, filename, **kwargs):
+ """Creates an OnDemandCredentials instance from a service account .json
+ file in Google format.
+
+ Args:
+ filename (str): The path to the service account .json file.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.jwt.OnDemandCredentials: The constructed credentials.
+ """
+ info, signer = _service_account_info.from_filename(
+ filename, require=["client_email"]
+ )
+ return cls._from_signer_and_info(signer, info, **kwargs)
+
+ @classmethod
+ def from_signing_credentials(cls, credentials, **kwargs):
+ """Creates a new :class:`google.auth.jwt.OnDemandCredentials` instance
+ from an existing :class:`google.auth.credentials.Signing` instance.
+
+ The new instance will use the same signer as the existing instance and
+ will use the existing instance's signer email as the issuer and
+ subject by default.
+
+ Example::
+
+ svc_creds = service_account.Credentials.from_service_account_file(
+ 'service_account.json')
+ jwt_creds = jwt.OnDemandCredentials.from_signing_credentials(
+ svc_creds)
+
+ Args:
+ credentials (google.auth.credentials.Signing): The credentials to
+ use to construct the new credentials.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.jwt.Credentials: A new Credentials instance.
+ """
+ kwargs.setdefault("issuer", credentials.signer_email)
+ kwargs.setdefault("subject", credentials.signer_email)
+ return cls(credentials.signer, **kwargs)
+
+ def with_claims(self, issuer=None, subject=None, additional_claims=None):
+ """Returns a copy of these credentials with modified claims.
+
+ Args:
+ issuer (str): The `iss` claim. If unspecified the current issuer
+ claim will be used.
+ subject (str): The `sub` claim. If unspecified the current subject
+ claim will be used.
+ additional_claims (Mapping[str, str]): Any additional claims for
+ the JWT payload. This will be merged with the current
+ additional claims.
+
+ Returns:
+ google.auth.jwt.OnDemandCredentials: A new credentials instance.
+ """
+ new_additional_claims = copy.deepcopy(self._additional_claims)
+ new_additional_claims.update(additional_claims or {})
+
+ return self.__class__(
+ self._signer,
+ issuer=issuer if issuer is not None else self._issuer,
+ subject=subject if subject is not None else self._subject,
+ additional_claims=new_additional_claims,
+ max_cache_size=self._cache.maxsize,
+ quota_project_id=self._quota_project_id,
+ )
+
+ @_helpers.copy_docstring(google.auth.credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+
+ return self.__class__(
+ self._signer,
+ issuer=self._issuer,
+ subject=self._subject,
+ additional_claims=self._additional_claims,
+ max_cache_size=self._cache.maxsize,
+ quota_project_id=quota_project_id,
+ )
+
+ @property
+ def valid(self):
+ """Checks the validity of the credentials.
+
+ These credentials are always valid because it generates tokens on
+ demand.
+ """
+ return True
+
+ def _make_jwt_for_audience(self, audience):
+ """Make a new JWT for the given audience.
+
+ Args:
+ audience (str): The intended audience.
+
+ Returns:
+ Tuple[bytes, datetime]: The encoded JWT and the expiration.
+ """
+ now = _helpers.utcnow()
+ lifetime = datetime.timedelta(seconds=self._token_lifetime)
+ expiry = now + lifetime
+
+ payload = {
+ "iss": self._issuer,
+ "sub": self._subject,
+ "iat": _helpers.datetime_to_secs(now),
+ "exp": _helpers.datetime_to_secs(expiry),
+ "aud": audience,
+ }
+
+ payload.update(self._additional_claims)
+
+ jwt = encode(self._signer, payload)
+
+ return jwt, expiry
+
+ def _get_jwt_for_audience(self, audience):
+ """Get a JWT For a given audience.
+
+ If there is already an existing, non-expired token in the cache for
+ the audience, that token is used. Otherwise, a new token will be
+ created.
+
+ Args:
+ audience (str): The intended audience.
+
+ Returns:
+ bytes: The encoded JWT.
+ """
+ token, expiry = self._cache.get(audience, (None, None))
+
+ if token is None or expiry < _helpers.utcnow():
+ token, expiry = self._make_jwt_for_audience(audience)
+ self._cache[audience] = token, expiry
+
+ return token
+
+ def refresh(self, request):
+ """Raises an exception, these credentials can not be directly
+ refreshed.
+
+ Args:
+ request (Any): Unused.
+
+ Raises:
+ google.auth.RefreshError
+ """
+ # pylint: disable=unused-argument
+ # (pylint doesn't correctly recognize overridden methods.)
+ raise exceptions.RefreshError(
+ "OnDemandCredentials can not be directly refreshed."
+ )
+
+ def before_request(self, request, method, url, headers):
+ """Performs credential-specific before request logic.
+
+ Args:
+ request (Any): Unused. JWT credentials do not need to make an
+ HTTP request to refresh.
+ method (str): The request's HTTP method.
+ url (str): The request's URI. This is used as the audience claim
+ when generating the JWT.
+ headers (Mapping): The request's headers.
+ """
+ # pylint: disable=unused-argument
+ # (pylint doesn't correctly recognize overridden methods.)
+ parts = urllib.parse.urlsplit(url)
+ # Strip query string and fragment
+ audience = urllib.parse.urlunsplit(
+ (parts.scheme, parts.netloc, parts.path, "", "")
+ )
+ token = self._get_jwt_for_audience(audience)
+ self.apply(headers, token=token)
+
+ @_helpers.copy_docstring(google.auth.credentials.Signing)
+ def sign_bytes(self, message):
+ return self._signer.sign(message)
+
+ @property # type: ignore
+ @_helpers.copy_docstring(google.auth.credentials.Signing)
+ def signer_email(self):
+ return self._issuer
+
+ @property # type: ignore
+ @_helpers.copy_docstring(google.auth.credentials.Signing)
+ def signer(self):
+ return self._signer
diff --git a/contrib/python/google-auth/py3/google/auth/metrics.py b/contrib/python/google-auth/py3/google/auth/metrics.py
new file mode 100644
index 0000000000..11e4b07730
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/metrics.py
@@ -0,0 +1,154 @@
+# Copyright 2023 Google LLC
+#
+# 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.
+
+""" We use x-goog-api-client header to report metrics. This module provides
+the constants and helper methods to construct x-goog-api-client header.
+"""
+
+import platform
+
+from google.auth import version
+
+
+API_CLIENT_HEADER = "x-goog-api-client"
+
+# BYOID Specific consts
+BYOID_HEADER_SECTION = "google-byoid-sdk"
+
+# Auth request type
+REQUEST_TYPE_ACCESS_TOKEN = "auth-request-type/at"
+REQUEST_TYPE_ID_TOKEN = "auth-request-type/it"
+REQUEST_TYPE_MDS_PING = "auth-request-type/mds"
+REQUEST_TYPE_REAUTH_START = "auth-request-type/re-start"
+REQUEST_TYPE_REAUTH_CONTINUE = "auth-request-type/re-cont"
+
+# Credential type
+CRED_TYPE_USER = "cred-type/u"
+CRED_TYPE_SA_ASSERTION = "cred-type/sa"
+CRED_TYPE_SA_JWT = "cred-type/jwt"
+CRED_TYPE_SA_MDS = "cred-type/mds"
+CRED_TYPE_SA_IMPERSONATE = "cred-type/imp"
+
+
+# Versions
+def python_and_auth_lib_version():
+ return "gl-python/{} auth/{}".format(platform.python_version(), version.__version__)
+
+
+# Token request metric header values
+
+# x-goog-api-client header value for access token request via metadata server.
+# Example: "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/mds"
+def token_request_access_token_mds():
+ return "{} {} {}".format(
+ python_and_auth_lib_version(), REQUEST_TYPE_ACCESS_TOKEN, CRED_TYPE_SA_MDS
+ )
+
+
+# x-goog-api-client header value for ID token request via metadata server.
+# Example: "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/mds"
+def token_request_id_token_mds():
+ return "{} {} {}".format(
+ python_and_auth_lib_version(), REQUEST_TYPE_ID_TOKEN, CRED_TYPE_SA_MDS
+ )
+
+
+# x-goog-api-client header value for impersonated credentials access token request.
+# Example: "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
+def token_request_access_token_impersonate():
+ return "{} {} {}".format(
+ python_and_auth_lib_version(),
+ REQUEST_TYPE_ACCESS_TOKEN,
+ CRED_TYPE_SA_IMPERSONATE,
+ )
+
+
+# x-goog-api-client header value for impersonated credentials ID token request.
+# Example: "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/imp"
+def token_request_id_token_impersonate():
+ return "{} {} {}".format(
+ python_and_auth_lib_version(), REQUEST_TYPE_ID_TOKEN, CRED_TYPE_SA_IMPERSONATE
+ )
+
+
+# x-goog-api-client header value for service account credentials access token
+# request (assertion flow).
+# Example: "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/sa"
+def token_request_access_token_sa_assertion():
+ return "{} {} {}".format(
+ python_and_auth_lib_version(), REQUEST_TYPE_ACCESS_TOKEN, CRED_TYPE_SA_ASSERTION
+ )
+
+
+# x-goog-api-client header value for service account credentials ID token
+# request (assertion flow).
+# Example: "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/sa"
+def token_request_id_token_sa_assertion():
+ return "{} {} {}".format(
+ python_and_auth_lib_version(), REQUEST_TYPE_ID_TOKEN, CRED_TYPE_SA_ASSERTION
+ )
+
+
+# x-goog-api-client header value for user credentials token request.
+# Example: "gl-python/3.7 auth/1.1 cred-type/u"
+def token_request_user():
+ return "{} {}".format(python_and_auth_lib_version(), CRED_TYPE_USER)
+
+
+# Miscellenous metrics
+
+# x-goog-api-client header value for metadata server ping.
+# Example: "gl-python/3.7 auth/1.1 auth-request-type/mds"
+def mds_ping():
+ return "{} {}".format(python_and_auth_lib_version(), REQUEST_TYPE_MDS_PING)
+
+
+# x-goog-api-client header value for reauth start endpoint calls.
+# Example: "gl-python/3.7 auth/1.1 auth-request-type/re-start"
+def reauth_start():
+ return "{} {}".format(python_and_auth_lib_version(), REQUEST_TYPE_REAUTH_START)
+
+
+# x-goog-api-client header value for reauth continue endpoint calls.
+# Example: "gl-python/3.7 auth/1.1 cred-type/re-cont"
+def reauth_continue():
+ return "{} {}".format(python_and_auth_lib_version(), REQUEST_TYPE_REAUTH_CONTINUE)
+
+
+# x-goog-api-client header value for BYOID calls to the Security Token Service exchange token endpoint.
+# Example: "gl-python/3.7 auth/1.1 google-byoid-sdk source/aws sa-impersonation/true sa-impersonation/true"
+def byoid_metrics_header(metrics_options):
+ header = "{} {}".format(python_and_auth_lib_version(), BYOID_HEADER_SECTION)
+ for key, value in metrics_options.items():
+ header = "{} {}/{}".format(header, key, value)
+ return header
+
+
+def add_metric_header(headers, metric_header_value):
+ """Add x-goog-api-client header with the given value.
+
+ Args:
+ headers (Mapping[str, str]): The headers to which we will add the
+ metric header.
+ metric_header_value (Optional[str]): If value is None, do nothing;
+ if headers already has a x-goog-api-client header, append the value
+ to the existing header; otherwise add a new x-goog-api-client
+ header with the given value.
+ """
+ if not metric_header_value:
+ return
+ if API_CLIENT_HEADER not in headers:
+ headers[API_CLIENT_HEADER] = metric_header_value
+ else:
+ headers[API_CLIENT_HEADER] += " " + metric_header_value
diff --git a/contrib/python/google-auth/py3/google/auth/pluggable.py b/contrib/python/google-auth/py3/google/auth/pluggable.py
new file mode 100644
index 0000000000..53b4eac5b4
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/pluggable.py
@@ -0,0 +1,429 @@
+# Copyright 2022 Google LLC
+#
+# 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.
+
+"""Pluggable Credentials.
+Pluggable Credentials are initialized using external_account arguments which
+are typically loaded from third-party executables. Unlike other
+credentials that can be initialized with a list of explicit arguments, secrets
+or credentials, external account clients use the environment and hints/guidelines
+provided by the external_account JSON file to retrieve credentials and exchange
+them for Google access tokens.
+
+Example credential_source for pluggable credential:
+{
+ "executable": {
+ "command": "/path/to/get/credentials.sh --arg1=value1 --arg2=value2",
+ "timeout_millis": 5000,
+ "output_file": "/path/to/generated/cached/credentials"
+ }
+}
+"""
+
+try:
+ from collections.abc import Mapping
+# Python 2.7 compatibility
+except ImportError: # pragma: NO COVER
+ from collections import Mapping
+import json
+import os
+import subprocess
+import sys
+import time
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import external_account
+
+# The max supported executable spec version.
+EXECUTABLE_SUPPORTED_MAX_VERSION = 1
+
+EXECUTABLE_TIMEOUT_MILLIS_DEFAULT = 30 * 1000 # 30 seconds
+EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND = 5 * 1000 # 5 seconds
+EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND = 120 * 1000 # 2 minutes
+
+EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_LOWER_BOUND = 30 * 1000 # 30 seconds
+EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_UPPER_BOUND = 30 * 60 * 1000 # 30 minutes
+
+
+class Credentials(external_account.Credentials):
+ """External account credentials sourced from executables."""
+
+ def __init__(
+ self,
+ audience,
+ subject_token_type,
+ token_url,
+ credential_source,
+ *args,
+ **kwargs
+ ):
+ """Instantiates an external account credentials object from a executables.
+
+ Args:
+ audience (str): The STS audience field.
+ subject_token_type (str): The subject token type.
+ token_url (str): The STS endpoint URL.
+ credential_source (Mapping): The credential source dictionary used to
+ provide instructions on how to retrieve external credential to be
+ exchanged for Google access tokens.
+
+ Example credential_source for pluggable credential:
+
+ {
+ "executable": {
+ "command": "/path/to/get/credentials.sh --arg1=value1 --arg2=value2",
+ "timeout_millis": 5000,
+ "output_file": "/path/to/generated/cached/credentials"
+ }
+ }
+ args (List): Optional positional arguments passed into the underlying :meth:`~external_account.Credentials.__init__` method.
+ kwargs (Mapping): Optional keyword arguments passed into the underlying :meth:`~external_account.Credentials.__init__` method.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If an error is encountered during
+ access token retrieval logic.
+ google.auth.exceptions.InvalidValue: For invalid parameters.
+ google.auth.exceptions.MalformedError: For invalid parameters.
+
+ .. note:: Typically one of the helper constructors
+ :meth:`from_file` or
+ :meth:`from_info` are used instead of calling the constructor directly.
+ """
+
+ self.interactive = kwargs.pop("interactive", False)
+ super(Credentials, self).__init__(
+ audience=audience,
+ subject_token_type=subject_token_type,
+ token_url=token_url,
+ credential_source=credential_source,
+ *args,
+ **kwargs
+ )
+ if not isinstance(credential_source, Mapping):
+ self._credential_source_executable = None
+ raise exceptions.MalformedError(
+ "Missing credential_source. The credential_source is not a dict."
+ )
+ self._credential_source_executable = credential_source.get("executable")
+ if not self._credential_source_executable:
+ raise exceptions.MalformedError(
+ "Missing credential_source. An 'executable' must be provided."
+ )
+ self._credential_source_executable_command = self._credential_source_executable.get(
+ "command"
+ )
+ self._credential_source_executable_timeout_millis = self._credential_source_executable.get(
+ "timeout_millis"
+ )
+ self._credential_source_executable_interactive_timeout_millis = self._credential_source_executable.get(
+ "interactive_timeout_millis"
+ )
+ self._credential_source_executable_output_file = self._credential_source_executable.get(
+ "output_file"
+ )
+
+ # Dummy value. This variable is only used via injection, not exposed to ctor
+ self._tokeninfo_username = ""
+
+ if not self._credential_source_executable_command:
+ raise exceptions.MalformedError(
+ "Missing command field. Executable command must be provided."
+ )
+ if not self._credential_source_executable_timeout_millis:
+ self._credential_source_executable_timeout_millis = (
+ EXECUTABLE_TIMEOUT_MILLIS_DEFAULT
+ )
+ elif (
+ self._credential_source_executable_timeout_millis
+ < EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND
+ or self._credential_source_executable_timeout_millis
+ > EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND
+ ):
+ raise exceptions.InvalidValue("Timeout must be between 5 and 120 seconds.")
+
+ if self._credential_source_executable_interactive_timeout_millis:
+ if (
+ self._credential_source_executable_interactive_timeout_millis
+ < EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_LOWER_BOUND
+ or self._credential_source_executable_interactive_timeout_millis
+ > EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_UPPER_BOUND
+ ):
+ raise exceptions.InvalidValue(
+ "Interactive timeout must be between 30 seconds and 30 minutes."
+ )
+
+ @_helpers.copy_docstring(external_account.Credentials)
+ def retrieve_subject_token(self, request):
+ self._validate_running_mode()
+
+ # Check output file.
+ if self._credential_source_executable_output_file is not None:
+ try:
+ with open(
+ self._credential_source_executable_output_file, encoding="utf-8"
+ ) as output_file:
+ response = json.load(output_file)
+ except Exception:
+ pass
+ else:
+ try:
+ # If the cached response is expired, _parse_subject_token will raise an error which will be ignored and we will call the executable again.
+ subject_token = self._parse_subject_token(response)
+ if (
+ "expiration_time" not in response
+ ): # Always treat missing expiration_time as expired and proceed to executable run.
+ raise exceptions.RefreshError
+ except (exceptions.MalformedError, exceptions.InvalidValue):
+ raise
+ except exceptions.RefreshError:
+ pass
+ else:
+ return subject_token
+
+ if not _helpers.is_python_3():
+ raise exceptions.RefreshError(
+ "Pluggable auth is only supported for python 3.7+"
+ )
+
+ # Inject env vars.
+ env = os.environ.copy()
+ self._inject_env_variables(env)
+ env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "0"
+
+ # Run executable.
+ exe_timeout = (
+ self._credential_source_executable_interactive_timeout_millis / 1000
+ if self.interactive
+ else self._credential_source_executable_timeout_millis / 1000
+ )
+ exe_stdin = sys.stdin if self.interactive else None
+ exe_stdout = sys.stdout if self.interactive else subprocess.PIPE
+ exe_stderr = sys.stdout if self.interactive else subprocess.STDOUT
+
+ result = subprocess.run(
+ self._credential_source_executable_command.split(),
+ timeout=exe_timeout,
+ stdin=exe_stdin,
+ stdout=exe_stdout,
+ stderr=exe_stderr,
+ env=env,
+ )
+ if result.returncode != 0:
+ raise exceptions.RefreshError(
+ "Executable exited with non-zero return code {}. Error: {}".format(
+ result.returncode, result.stdout
+ )
+ )
+
+ # Handle executable output.
+ response = json.loads(result.stdout.decode("utf-8")) if result.stdout else None
+ if not response and self._credential_source_executable_output_file is not None:
+ response = json.load(
+ open(self._credential_source_executable_output_file, encoding="utf-8")
+ )
+
+ subject_token = self._parse_subject_token(response)
+ return subject_token
+
+ def revoke(self, request):
+ """Revokes the subject token using the credential_source object.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ Raises:
+ google.auth.exceptions.RefreshError: If the executable revocation
+ not properly executed.
+
+ """
+ if not self.interactive:
+ raise exceptions.InvalidValue(
+ "Revoke is only enabled under interactive mode."
+ )
+ self._validate_running_mode()
+
+ if not _helpers.is_python_3():
+ raise exceptions.RefreshError(
+ "Pluggable auth is only supported for python 3.7+"
+ )
+
+ # Inject variables
+ env = os.environ.copy()
+ self._inject_env_variables(env)
+ env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "1"
+
+ # Run executable
+ result = subprocess.run(
+ self._credential_source_executable_command.split(),
+ timeout=self._credential_source_executable_interactive_timeout_millis
+ / 1000,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ env=env,
+ )
+
+ if result.returncode != 0:
+ raise exceptions.RefreshError(
+ "Auth revoke failed on executable. Exit with non-zero return code {}. Error: {}".format(
+ result.returncode, result.stdout
+ )
+ )
+
+ response = json.loads(result.stdout.decode("utf-8"))
+ self._validate_revoke_response(response)
+
+ @property
+ def external_account_id(self):
+ """Returns the external account identifier.
+
+ When service account impersonation is used the identifier is the service
+ account email.
+
+ Without service account impersonation, this returns None, unless it is
+ being used by the Google Cloud CLI which populates this field.
+ """
+
+ return self.service_account_email or self._tokeninfo_username
+
+ @classmethod
+ def from_info(cls, info, **kwargs):
+ """Creates a Pluggable Credentials instance from parsed external account info.
+
+ Args:
+ info (Mapping[str, str]): The Pluggable external account info in Google
+ format.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.pluggable.Credentials: The constructed
+ credentials.
+
+ Raises:
+ google.auth.exceptions.InvalidValue: For invalid parameters.
+ google.auth.exceptions.MalformedError: For invalid parameters.
+ """
+ return super(Credentials, cls).from_info(info, **kwargs)
+
+ @classmethod
+ def from_file(cls, filename, **kwargs):
+ """Creates an Pluggable Credentials instance from an external account json file.
+
+ Args:
+ filename (str): The path to the Pluggable external account json file.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.pluggable.Credentials: The constructed
+ credentials.
+ """
+ return super(Credentials, cls).from_file(filename, **kwargs)
+
+ def _inject_env_variables(self, env):
+ env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience
+ env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type
+ env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id
+ env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" if self.interactive else "0"
+
+ if self._service_account_impersonation_url is not None:
+ env[
+ "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"
+ ] = self.service_account_email
+ if self._credential_source_executable_output_file is not None:
+ env[
+ "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"
+ ] = self._credential_source_executable_output_file
+
+ def _parse_subject_token(self, response):
+ self._validate_response_schema(response)
+ if not response["success"]:
+ if "code" not in response or "message" not in response:
+ raise exceptions.MalformedError(
+ "Error code and message fields are required in the response."
+ )
+ raise exceptions.RefreshError(
+ "Executable returned unsuccessful response: code: {}, message: {}.".format(
+ response["code"], response["message"]
+ )
+ )
+ if "expiration_time" in response and response["expiration_time"] < time.time():
+ raise exceptions.RefreshError(
+ "The token returned by the executable is expired."
+ )
+ if "token_type" not in response:
+ raise exceptions.MalformedError(
+ "The executable response is missing the token_type field."
+ )
+ if (
+ response["token_type"] == "urn:ietf:params:oauth:token-type:jwt"
+ or response["token_type"] == "urn:ietf:params:oauth:token-type:id_token"
+ ): # OIDC
+ return response["id_token"]
+ elif response["token_type"] == "urn:ietf:params:oauth:token-type:saml2": # SAML
+ return response["saml_response"]
+ else:
+ raise exceptions.RefreshError("Executable returned unsupported token type.")
+
+ def _validate_revoke_response(self, response):
+ self._validate_response_schema(response)
+ if not response["success"]:
+ raise exceptions.RefreshError("Revoke failed with unsuccessful response.")
+
+ def _validate_response_schema(self, response):
+ if "version" not in response:
+ raise exceptions.MalformedError(
+ "The executable response is missing the version field."
+ )
+ if response["version"] > EXECUTABLE_SUPPORTED_MAX_VERSION:
+ raise exceptions.RefreshError(
+ "Executable returned unsupported version {}.".format(
+ response["version"]
+ )
+ )
+
+ if "success" not in response:
+ raise exceptions.MalformedError(
+ "The executable response is missing the success field."
+ )
+
+ def _validate_running_mode(self):
+ env_allow_executables = os.environ.get(
+ "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
+ )
+ if env_allow_executables != "1":
+ raise exceptions.MalformedError(
+ "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run."
+ )
+
+ if self.interactive and not self._credential_source_executable_output_file:
+ raise exceptions.MalformedError(
+ "An output_file must be specified in the credential configuration for interactive mode."
+ )
+
+ if (
+ self.interactive
+ and not self._credential_source_executable_interactive_timeout_millis
+ ):
+ raise exceptions.InvalidOperation(
+ "Interactive mode cannot run without an interactive timeout."
+ )
+
+ if self.interactive and not self.is_workforce_pool:
+ raise exceptions.InvalidValue(
+ "Interactive mode is only enabled for workforce pool."
+ )
+
+ def _create_default_metrics_options(self):
+ metrics_options = super(Credentials, self)._create_default_metrics_options()
+ metrics_options["source"] = "executable"
+ return metrics_options
diff --git a/contrib/python/google-auth/py3/google/auth/transport/__init__.py b/contrib/python/google-auth/py3/google/auth/transport/__init__.py
new file mode 100644
index 0000000000..724568e582
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/transport/__init__.py
@@ -0,0 +1,103 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Transport - HTTP client library support.
+
+:mod:`google.auth` is designed to work with various HTTP client libraries such
+as urllib3 and requests. In order to work across these libraries with different
+interfaces some abstraction is needed.
+
+This module provides two interfaces that are implemented by transport adapters
+to support HTTP libraries. :class:`Request` defines the interface expected by
+:mod:`google.auth` to make requests. :class:`Response` defines the interface
+for the return value of :class:`Request`.
+"""
+
+import abc
+import http.client as http_client
+
+DEFAULT_RETRYABLE_STATUS_CODES = (
+ http_client.INTERNAL_SERVER_ERROR,
+ http_client.SERVICE_UNAVAILABLE,
+ http_client.REQUEST_TIMEOUT,
+ http_client.TOO_MANY_REQUESTS,
+)
+"""Sequence[int]: HTTP status codes indicating a request can be retried.
+"""
+
+
+DEFAULT_REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
+"""Sequence[int]: Which HTTP status code indicate that credentials should be
+refreshed.
+"""
+
+DEFAULT_MAX_REFRESH_ATTEMPTS = 2
+"""int: How many times to refresh the credentials and retry a request."""
+
+
+class Response(metaclass=abc.ABCMeta):
+ """HTTP Response data."""
+
+ @abc.abstractproperty
+ def status(self):
+ """int: The HTTP status code."""
+ raise NotImplementedError("status must be implemented.")
+
+ @abc.abstractproperty
+ def headers(self):
+ """Mapping[str, str]: The HTTP response headers."""
+ raise NotImplementedError("headers must be implemented.")
+
+ @abc.abstractproperty
+ def data(self):
+ """bytes: The response body."""
+ raise NotImplementedError("data must be implemented.")
+
+
+class Request(metaclass=abc.ABCMeta):
+ """Interface for a callable that makes HTTP requests.
+
+ Specific transport implementations should provide an implementation of
+ this that adapts their specific request / response API.
+
+ .. automethod:: __call__
+ """
+
+ @abc.abstractmethod
+ def __call__(
+ self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
+ ):
+ """Make an HTTP request.
+
+ Args:
+ url (str): The URI to be requested.
+ method (str): The HTTP method to use for the request. Defaults
+ to 'GET'.
+ body (bytes): The payload / body in HTTP request.
+ headers (Mapping[str, str]): Request headers.
+ timeout (Optional[int]): The number of seconds to wait for a
+ response from the server. If not specified or if None, the
+ transport-specific default timeout will be used.
+ kwargs: Additionally arguments passed on to the transport's
+ request method.
+
+ Returns:
+ Response: The HTTP response.
+
+ Raises:
+ google.auth.exceptions.TransportError: If any exception occurred.
+ """
+ # pylint: disable=redundant-returns-doc, missing-raises-doc
+ # (pylint doesn't play well with abstract docstrings.)
+ raise NotImplementedError("__call__ must be implemented.")
diff --git a/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py b/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py
new file mode 100644
index 0000000000..3a8da917a1
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py
@@ -0,0 +1,390 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+
+"""Transport adapter for Async HTTP (aiohttp).
+
+NOTE: This async support is experimental and marked internal. This surface may
+change in minor releases.
+"""
+
+from __future__ import absolute_import
+
+import asyncio
+import functools
+
+import aiohttp # type: ignore
+import urllib3 # type: ignore
+
+from google.auth import exceptions
+from google.auth import transport
+from google.auth.transport import requests
+
+# Timeout can be re-defined depending on async requirement. Currently made 60s more than
+# sync timeout.
+_DEFAULT_TIMEOUT = 180 # in seconds
+
+
+class _CombinedResponse(transport.Response):
+ """
+ In order to more closely resemble the `requests` interface, where a raw
+ and deflated content could be accessed at once, this class lazily reads the
+ stream in `transport.Response` so both return forms can be used.
+
+ The gzip and deflate transfer-encodings are automatically decoded for you
+ because the default parameter for autodecompress into the ClientSession is set
+ to False, and therefore we add this class to act as a wrapper for a user to be
+ able to access both the raw and decoded response bodies - mirroring the sync
+ implementation.
+ """
+
+ def __init__(self, response):
+ self._response = response
+ self._raw_content = None
+
+ def _is_compressed(self):
+ headers = self._response.headers
+ return "Content-Encoding" in headers and (
+ headers["Content-Encoding"] == "gzip"
+ or headers["Content-Encoding"] == "deflate"
+ )
+
+ @property
+ def status(self):
+ return self._response.status
+
+ @property
+ def headers(self):
+ return self._response.headers
+
+ @property
+ def data(self):
+ return self._response.content
+
+ async def raw_content(self):
+ if self._raw_content is None:
+ self._raw_content = await self._response.content.read()
+ return self._raw_content
+
+ async def content(self):
+ # Load raw_content if necessary
+ await self.raw_content()
+ if self._is_compressed():
+ decoder = urllib3.response.MultiDecoder(
+ self._response.headers["Content-Encoding"]
+ )
+ decompressed = decoder.decompress(self._raw_content)
+ return decompressed
+
+ return self._raw_content
+
+
+class _Response(transport.Response):
+ """
+ Requests transport response adapter.
+
+ Args:
+ response (requests.Response): The raw Requests response.
+ """
+
+ def __init__(self, response):
+ self._response = response
+
+ @property
+ def status(self):
+ return self._response.status
+
+ @property
+ def headers(self):
+ return self._response.headers
+
+ @property
+ def data(self):
+ return self._response.content
+
+
+class Request(transport.Request):
+ """Requests request adapter.
+
+ This class is used internally for making requests using asyncio transports
+ in a consistent way. If you use :class:`AuthorizedSession` you do not need
+ to construct or use this class directly.
+
+ This class can be useful if you want to manually refresh a
+ :class:`~google.auth.credentials.Credentials` instance::
+
+ import google.auth.transport.aiohttp_requests
+
+ request = google.auth.transport.aiohttp_requests.Request()
+
+ credentials.refresh(request)
+
+ Args:
+ session (aiohttp.ClientSession): An instance :class:`aiohttp.ClientSession` used
+ to make HTTP requests. If not specified, a session will be created.
+
+ .. automethod:: __call__
+ """
+
+ def __init__(self, session=None):
+ # TODO: Use auto_decompress property for aiohttp 3.7+
+ if session is not None and session._auto_decompress:
+ raise exceptions.InvalidOperation(
+ "Client sessions with auto_decompress=True are not supported."
+ )
+ self.session = session
+
+ async def __call__(
+ self,
+ url,
+ method="GET",
+ body=None,
+ headers=None,
+ timeout=_DEFAULT_TIMEOUT,
+ **kwargs,
+ ):
+ """
+ Make an HTTP request using aiohttp.
+
+ Args:
+ url (str): The URL to be requested.
+ method (Optional[str]):
+ The HTTP method to use for the request. Defaults to 'GET'.
+ body (Optional[bytes]):
+ The payload or body in HTTP request.
+ headers (Optional[Mapping[str, str]]):
+ Request headers.
+ timeout (Optional[int]): The number of seconds to wait for a
+ response from the server. If not specified or if None, the
+ requests default timeout will be used.
+ kwargs: Additional arguments passed through to the underlying
+ requests :meth:`requests.Session.request` method.
+
+ Returns:
+ google.auth.transport.Response: The HTTP response.
+
+ Raises:
+ google.auth.exceptions.TransportError: If any exception occurred.
+ """
+
+ try:
+ if self.session is None: # pragma: NO COVER
+ self.session = aiohttp.ClientSession(
+ auto_decompress=False
+ ) # pragma: NO COVER
+ requests._LOGGER.debug("Making request: %s %s", method, url)
+ response = await self.session.request(
+ method, url, data=body, headers=headers, timeout=timeout, **kwargs
+ )
+ return _CombinedResponse(response)
+
+ except aiohttp.ClientError as caught_exc:
+ new_exc = exceptions.TransportError(caught_exc)
+ raise new_exc from caught_exc
+
+ except asyncio.TimeoutError as caught_exc:
+ new_exc = exceptions.TransportError(caught_exc)
+ raise new_exc from caught_exc
+
+
+class AuthorizedSession(aiohttp.ClientSession):
+ """This is an async implementation of the Authorized Session class. We utilize an
+ aiohttp transport instance, and the interface mirrors the google.auth.transport.requests
+ Authorized Session class, except for the change in the transport used in the async use case.
+
+ A Requests Session class with credentials.
+
+ This class is used to perform requests to API endpoints that require
+ authorization::
+
+ from google.auth.transport import aiohttp_requests
+
+ async with aiohttp_requests.AuthorizedSession(credentials) as authed_session:
+ response = await authed_session.request(
+ 'GET', 'https://www.googleapis.com/storage/v1/b')
+
+ The underlying :meth:`request` implementation handles adding the
+ credentials' headers to the request and refreshing credentials as needed.
+
+ Args:
+ credentials (google.auth._credentials_async.Credentials):
+ The credentials to add to the request.
+ refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
+ that credentials should be refreshed and the request should be
+ retried.
+ max_refresh_attempts (int): The maximum number of times to attempt to
+ refresh the credentials and retry the request.
+ refresh_timeout (Optional[int]): The timeout value in seconds for
+ credential refresh HTTP requests.
+ auth_request (google.auth.transport.aiohttp_requests.Request):
+ (Optional) An instance of
+ :class:`~google.auth.transport.aiohttp_requests.Request` used when
+ refreshing credentials. If not passed,
+ an instance of :class:`~google.auth.transport.aiohttp_requests.Request`
+ is created.
+ kwargs: Additional arguments passed through to the underlying
+ ClientSession :meth:`aiohttp.ClientSession` object.
+ """
+
+ def __init__(
+ self,
+ credentials,
+ refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
+ max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
+ refresh_timeout=None,
+ auth_request=None,
+ auto_decompress=False,
+ **kwargs,
+ ):
+ super(AuthorizedSession, self).__init__(**kwargs)
+ self.credentials = credentials
+ self._refresh_status_codes = refresh_status_codes
+ self._max_refresh_attempts = max_refresh_attempts
+ self._refresh_timeout = refresh_timeout
+ self._is_mtls = False
+ self._auth_request = auth_request
+ self._auth_request_session = None
+ self._loop = asyncio.get_event_loop()
+ self._refresh_lock = asyncio.Lock()
+ self._auto_decompress = auto_decompress
+
+ async def request(
+ self,
+ method,
+ url,
+ data=None,
+ headers=None,
+ max_allowed_time=None,
+ timeout=_DEFAULT_TIMEOUT,
+ auto_decompress=False,
+ **kwargs,
+ ):
+
+ """Implementation of Authorized Session aiohttp request.
+
+ Args:
+ method (str):
+ The http request method used (e.g. GET, PUT, DELETE)
+ url (str):
+ The url at which the http request is sent.
+ data (Optional[dict]): Dictionary, list of tuples, bytes, or file-like
+ object to send in the body of the Request.
+ headers (Optional[dict]): Dictionary of HTTP Headers to send with the
+ Request.
+ timeout (Optional[Union[float, aiohttp.ClientTimeout]]):
+ The amount of time in seconds to wait for the server response
+ with each individual request. Can also be passed as an
+ ``aiohttp.ClientTimeout`` object.
+ max_allowed_time (Optional[float]):
+ If the method runs longer than this, a ``Timeout`` exception is
+ automatically raised. Unlike the ``timeout`` parameter, this
+ value applies to the total method execution time, even if
+ multiple requests are made under the hood.
+
+ Mind that it is not guaranteed that the timeout error is raised
+ at ``max_allowed_time``. It might take longer, for example, if
+ an underlying request takes a lot of time, but the request
+ itself does not timeout, e.g. if a large file is being
+ transmitted. The timout error will be raised after such
+ request completes.
+ """
+ # Headers come in as bytes which isn't expected behavior, the resumable
+ # media libraries in some cases expect a str type for the header values,
+ # but sometimes the operations return these in bytes types.
+ if headers:
+ for key in headers.keys():
+ if type(headers[key]) is bytes:
+ headers[key] = headers[key].decode("utf-8")
+
+ async with aiohttp.ClientSession(
+ auto_decompress=self._auto_decompress
+ ) as self._auth_request_session:
+ auth_request = Request(self._auth_request_session)
+ self._auth_request = auth_request
+
+ # Use a kwarg for this instead of an attribute to maintain
+ # thread-safety.
+ _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
+ # Make a copy of the headers. They will be modified by the credentials
+ # and we want to pass the original headers if we recurse.
+ request_headers = headers.copy() if headers is not None else {}
+
+ # Do not apply the timeout unconditionally in order to not override the
+ # _auth_request's default timeout.
+ auth_request = (
+ self._auth_request
+ if timeout is None
+ else functools.partial(self._auth_request, timeout=timeout)
+ )
+
+ remaining_time = max_allowed_time
+
+ with requests.TimeoutGuard(remaining_time, asyncio.TimeoutError) as guard:
+ await self.credentials.before_request(
+ auth_request, method, url, request_headers
+ )
+
+ with requests.TimeoutGuard(remaining_time, asyncio.TimeoutError) as guard:
+ response = await super(AuthorizedSession, self).request(
+ method,
+ url,
+ data=data,
+ headers=request_headers,
+ timeout=timeout,
+ **kwargs,
+ )
+
+ remaining_time = guard.remaining_timeout
+
+ if (
+ response.status in self._refresh_status_codes
+ and _credential_refresh_attempt < self._max_refresh_attempts
+ ):
+
+ requests._LOGGER.info(
+ "Refreshing credentials due to a %s response. Attempt %s/%s.",
+ response.status,
+ _credential_refresh_attempt + 1,
+ self._max_refresh_attempts,
+ )
+
+ # Do not apply the timeout unconditionally in order to not override the
+ # _auth_request's default timeout.
+ auth_request = (
+ self._auth_request
+ if timeout is None
+ else functools.partial(self._auth_request, timeout=timeout)
+ )
+
+ with requests.TimeoutGuard(
+ remaining_time, asyncio.TimeoutError
+ ) as guard:
+ async with self._refresh_lock:
+ await self._loop.run_in_executor(
+ None, self.credentials.refresh, auth_request
+ )
+
+ remaining_time = guard.remaining_timeout
+
+ return await self.request(
+ method,
+ url,
+ data=data,
+ headers=headers,
+ max_allowed_time=remaining_time,
+ timeout=timeout,
+ _credential_refresh_attempt=_credential_refresh_attempt + 1,
+ **kwargs,
+ )
+
+ return response
diff --git a/contrib/python/google-auth/py3/google/auth/transport/_custom_tls_signer.py b/contrib/python/google-auth/py3/google/auth/transport/_custom_tls_signer.py
new file mode 100644
index 0000000000..07f14df02d
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/transport/_custom_tls_signer.py
@@ -0,0 +1,234 @@
+# Copyright 2022 Google LLC
+#
+# 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.
+
+"""
+Code for configuring client side TLS to offload the signing operation to
+signing libraries.
+"""
+
+import ctypes
+import json
+import logging
+import os
+import sys
+
+import cffi # type: ignore
+
+from google.auth import exceptions
+
+_LOGGER = logging.getLogger(__name__)
+
+# C++ offload lib requires google-auth lib to provide the following callback:
+# using SignFunc = int (*)(unsigned char *sig, size_t *sig_len,
+# const unsigned char *tbs, size_t tbs_len)
+# The bytes to be signed and the length are provided via `tbs` and `tbs_len`,
+# the callback computes the signature, and write the signature and its length
+# into `sig` and `sig_len`.
+# If the signing is successful, the callback returns 1, otherwise it returns 0.
+SIGN_CALLBACK_CTYPE = ctypes.CFUNCTYPE(
+ ctypes.c_int, # return type
+ ctypes.POINTER(ctypes.c_ubyte), # sig
+ ctypes.POINTER(ctypes.c_size_t), # sig_len
+ ctypes.POINTER(ctypes.c_ubyte), # tbs
+ ctypes.c_size_t, # tbs_len
+)
+
+
+# Cast SSL_CTX* to void*
+def _cast_ssl_ctx_to_void_p(ssl_ctx):
+ return ctypes.cast(int(cffi.FFI().cast("intptr_t", ssl_ctx)), ctypes.c_void_p)
+
+
+# Load offload library and set up the function types.
+def load_offload_lib(offload_lib_path):
+ _LOGGER.debug("loading offload library from %s", offload_lib_path)
+
+ # winmode parameter is only available for python 3.8+.
+ lib = (
+ ctypes.CDLL(offload_lib_path, winmode=0)
+ if sys.version_info >= (3, 8) and os.name == "nt"
+ else ctypes.CDLL(offload_lib_path)
+ )
+
+ # Set up types for:
+ # int ConfigureSslContext(SignFunc sign_func, const char *cert, SSL_CTX *ctx)
+ lib.ConfigureSslContext.argtypes = [
+ SIGN_CALLBACK_CTYPE,
+ ctypes.c_char_p,
+ ctypes.c_void_p,
+ ]
+ lib.ConfigureSslContext.restype = ctypes.c_int
+
+ return lib
+
+
+# Load signer library and set up the function types.
+# See: https://github.com/googleapis/enterprise-certificate-proxy/blob/main/cshared/main.go
+def load_signer_lib(signer_lib_path):
+ _LOGGER.debug("loading signer library from %s", signer_lib_path)
+
+ # winmode parameter is only available for python 3.8+.
+ lib = (
+ ctypes.CDLL(signer_lib_path, winmode=0)
+ if sys.version_info >= (3, 8) and os.name == "nt"
+ else ctypes.CDLL(signer_lib_path)
+ )
+
+ # Set up types for:
+ # func GetCertPemForPython(configFilePath *C.char, certHolder *byte, certHolderLen int)
+ lib.GetCertPemForPython.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
+ # Returns: certLen
+ lib.GetCertPemForPython.restype = ctypes.c_int
+
+ # Set up types for:
+ # func SignForPython(configFilePath *C.char, digest *byte, digestLen int,
+ # sigHolder *byte, sigHolderLen int)
+ lib.SignForPython.argtypes = [
+ ctypes.c_char_p,
+ ctypes.c_char_p,
+ ctypes.c_int,
+ ctypes.c_char_p,
+ ctypes.c_int,
+ ]
+ # Returns: the signature length
+ lib.SignForPython.restype = ctypes.c_int
+
+ return lib
+
+
+# Computes SHA256 hash.
+def _compute_sha256_digest(to_be_signed, to_be_signed_len):
+ from cryptography.hazmat.primitives import hashes
+
+ data = ctypes.string_at(to_be_signed, to_be_signed_len)
+ hash = hashes.Hash(hashes.SHA256())
+ hash.update(data)
+ return hash.finalize()
+
+
+# Create the signing callback. The actual signing work is done by the
+# `SignForPython` method from the signer lib.
+def get_sign_callback(signer_lib, config_file_path):
+ def sign_callback(sig, sig_len, tbs, tbs_len):
+ _LOGGER.debug("calling sign callback...")
+
+ digest = _compute_sha256_digest(tbs, tbs_len)
+ digestArray = ctypes.c_char * len(digest)
+
+ # reserve 2000 bytes for the signature, shoud be more then enough.
+ # RSA signature is 256 bytes, EC signature is 70~72.
+ sig_holder_len = 2000
+ sig_holder = ctypes.create_string_buffer(sig_holder_len)
+
+ signature_len = signer_lib.SignForPython(
+ config_file_path.encode(), # configFilePath
+ digestArray.from_buffer(bytearray(digest)), # digest
+ len(digest), # digestLen
+ sig_holder, # sigHolder
+ sig_holder_len, # sigHolderLen
+ )
+
+ if signature_len == 0:
+ # signing failed, return 0
+ return 0
+
+ sig_len[0] = signature_len
+ bs = bytearray(sig_holder)
+ for i in range(signature_len):
+ sig[i] = bs[i]
+
+ return 1
+
+ return SIGN_CALLBACK_CTYPE(sign_callback)
+
+
+# Obtain the certificate bytes by calling the `GetCertPemForPython` method from
+# the signer lib. The method is called twice, the first time is to compute the
+# cert length, then we create a buffer to hold the cert, and call it again to
+# fill the buffer.
+def get_cert(signer_lib, config_file_path):
+ # First call to calculate the cert length
+ cert_len = signer_lib.GetCertPemForPython(
+ config_file_path.encode(), # configFilePath
+ None, # certHolder
+ 0, # certHolderLen
+ )
+ if cert_len == 0:
+ raise exceptions.MutualTLSChannelError("failed to get certificate")
+
+ # Then we create an array to hold the cert, and call again to fill the cert
+ cert_holder = ctypes.create_string_buffer(cert_len)
+ signer_lib.GetCertPemForPython(
+ config_file_path.encode(), # configFilePath
+ cert_holder, # certHolder
+ cert_len, # certHolderLen
+ )
+ return bytes(cert_holder)
+
+
+class CustomTlsSigner(object):
+ def __init__(self, enterprise_cert_file_path):
+ """
+ This class loads the offload and signer library, and calls APIs from
+ these libraries to obtain the cert and a signing callback, and attach
+ them to SSL context. The cert and the signing callback will be used
+ for client authentication in TLS handshake.
+
+ Args:
+ enterprise_cert_file_path (str): the path to a enterprise cert JSON
+ file. The file should contain the following field:
+
+ {
+ "libs": {
+ "ecp_client": "...",
+ "tls_offload": "..."
+ }
+ }
+ """
+ self._enterprise_cert_file_path = enterprise_cert_file_path
+ self._cert = None
+ self._sign_callback = None
+
+ def load_libraries(self):
+ try:
+ with open(self._enterprise_cert_file_path, "r") as f:
+ enterprise_cert_json = json.load(f)
+ libs = enterprise_cert_json["libs"]
+ signer_library = libs["ecp_client"]
+ offload_library = libs["tls_offload"]
+ except (KeyError, ValueError) as caught_exc:
+ new_exc = exceptions.MutualTLSChannelError(
+ "enterprise cert file is invalid", caught_exc
+ )
+ raise new_exc from caught_exc
+ self._offload_lib = load_offload_lib(offload_library)
+ self._signer_lib = load_signer_lib(signer_library)
+
+ def set_up_custom_key(self):
+ # We need to keep a reference of the cert and sign callback so it won't
+ # be garbage collected, otherwise it will crash when used by signer lib.
+ self._cert = get_cert(self._signer_lib, self._enterprise_cert_file_path)
+ self._sign_callback = get_sign_callback(
+ self._signer_lib, self._enterprise_cert_file_path
+ )
+
+ def attach_to_ssl_context(self, ctx):
+ # In the TLS handshake, the signing operation will be done by the
+ # sign_callback.
+ if not self._offload_lib.ConfigureSslContext(
+ self._sign_callback,
+ ctypes.c_char_p(self._cert),
+ _cast_ssl_ctx_to_void_p(ctx._ctx._context),
+ ):
+ raise exceptions.MutualTLSChannelError("failed to configure SSL context")
diff --git a/contrib/python/google-auth/py3/google/auth/transport/_http_client.py b/contrib/python/google-auth/py3/google/auth/transport/_http_client.py
new file mode 100644
index 0000000000..cec0ab73fb
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/transport/_http_client.py
@@ -0,0 +1,113 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Transport adapter for http.client, for internal use only."""
+
+import http.client as http_client
+import logging
+import socket
+import urllib
+
+from google.auth import exceptions
+from google.auth import transport
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class Response(transport.Response):
+ """http.client transport response adapter.
+
+ Args:
+ response (http.client.HTTPResponse): The raw http client response.
+ """
+
+ def __init__(self, response):
+ self._status = response.status
+ self._headers = {key.lower(): value for key, value in response.getheaders()}
+ self._data = response.read()
+
+ @property
+ def status(self):
+ return self._status
+
+ @property
+ def headers(self):
+ return self._headers
+
+ @property
+ def data(self):
+ return self._data
+
+
+class Request(transport.Request):
+ """http.client transport request adapter."""
+
+ def __call__(
+ self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
+ ):
+ """Make an HTTP request using http.client.
+
+ Args:
+ url (str): The URI to be requested.
+ method (str): The HTTP method to use for the request. Defaults
+ to 'GET'.
+ body (bytes): The payload / body in HTTP request.
+ headers (Mapping): Request headers.
+ timeout (Optional(int)): The number of seconds to wait for a
+ response from the server. If not specified or if None, the
+ socket global default timeout will be used.
+ kwargs: Additional arguments passed throught to the underlying
+ :meth:`~http.client.HTTPConnection.request` method.
+
+ Returns:
+ Response: The HTTP response.
+
+ Raises:
+ google.auth.exceptions.TransportError: If any exception occurred.
+ """
+ # socket._GLOBAL_DEFAULT_TIMEOUT is the default in http.client.
+ if timeout is None:
+ timeout = socket._GLOBAL_DEFAULT_TIMEOUT
+
+ # http.client doesn't allow None as the headers argument.
+ if headers is None:
+ headers = {}
+
+ # http.client needs the host and path parts specified separately.
+ parts = urllib.parse.urlsplit(url)
+ path = urllib.parse.urlunsplit(
+ ("", "", parts.path, parts.query, parts.fragment)
+ )
+
+ if parts.scheme != "http":
+ raise exceptions.TransportError(
+ "http.client transport only supports the http scheme, {}"
+ "was specified".format(parts.scheme)
+ )
+
+ connection = http_client.HTTPConnection(parts.netloc, timeout=timeout)
+
+ try:
+ _LOGGER.debug("Making request: %s %s", method, url)
+
+ connection.request(method, path, body=body, headers=headers, **kwargs)
+ response = connection.getresponse()
+ return Response(response)
+
+ except (http_client.HTTPException, socket.error) as caught_exc:
+ new_exc = exceptions.TransportError(caught_exc)
+ raise new_exc from caught_exc
+
+ finally:
+ connection.close()
diff --git a/contrib/python/google-auth/py3/google/auth/transport/_mtls_helper.py b/contrib/python/google-auth/py3/google/auth/transport/_mtls_helper.py
new file mode 100644
index 0000000000..1b9b9c285c
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/transport/_mtls_helper.py
@@ -0,0 +1,252 @@
+# Copyright 2020 Google LLC
+#
+# 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 getting mTLS cert and key."""
+
+import json
+import logging
+from os import path
+import re
+import subprocess
+
+from google.auth import exceptions
+
+CONTEXT_AWARE_METADATA_PATH = "~/.secureConnect/context_aware_metadata.json"
+_CERT_PROVIDER_COMMAND = "cert_provider_command"
+_CERT_REGEX = re.compile(
+ b"-----BEGIN CERTIFICATE-----.+-----END CERTIFICATE-----\r?\n?", re.DOTALL
+)
+
+# support various format of key files, e.g.
+# "-----BEGIN PRIVATE KEY-----...",
+# "-----BEGIN EC PRIVATE KEY-----...",
+# "-----BEGIN RSA PRIVATE KEY-----..."
+# "-----BEGIN ENCRYPTED PRIVATE KEY-----"
+_KEY_REGEX = re.compile(
+ b"-----BEGIN [A-Z ]*PRIVATE KEY-----.+-----END [A-Z ]*PRIVATE KEY-----\r?\n?",
+ re.DOTALL,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+_PASSPHRASE_REGEX = re.compile(
+ b"-----BEGIN PASSPHRASE-----(.+)-----END PASSPHRASE-----", re.DOTALL
+)
+
+
+def _check_dca_metadata_path(metadata_path):
+ """Checks for context aware metadata. If it exists, returns the absolute path;
+ otherwise returns None.
+
+ Args:
+ metadata_path (str): context aware metadata path.
+
+ Returns:
+ str: absolute path if exists and None otherwise.
+ """
+ metadata_path = path.expanduser(metadata_path)
+ if not path.exists(metadata_path):
+ _LOGGER.debug("%s is not found, skip client SSL authentication.", metadata_path)
+ return None
+ return metadata_path
+
+
+def _read_dca_metadata_file(metadata_path):
+ """Loads context aware metadata from the given path.
+
+ Args:
+ metadata_path (str): context aware metadata path.
+
+ Returns:
+ Dict[str, str]: The metadata.
+
+ Raises:
+ google.auth.exceptions.ClientCertError: If failed to parse metadata as JSON.
+ """
+ try:
+ with open(metadata_path) as f:
+ metadata = json.load(f)
+ except ValueError as caught_exc:
+ new_exc = exceptions.ClientCertError(caught_exc)
+ raise new_exc from caught_exc
+
+ return metadata
+
+
+def _run_cert_provider_command(command, expect_encrypted_key=False):
+ """Run the provided command, and return client side mTLS cert, key and
+ passphrase.
+
+ Args:
+ command (List[str]): cert provider command.
+ expect_encrypted_key (bool): If encrypted private key is expected.
+
+ Returns:
+ Tuple[bytes, bytes, bytes]: client certificate bytes in PEM format, key
+ bytes in PEM format and passphrase bytes.
+
+ Raises:
+ google.auth.exceptions.ClientCertError: if problems occurs when running
+ the cert provider command or generating cert, key and passphrase.
+ """
+ try:
+ process = subprocess.Popen(
+ command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
+ stdout, stderr = process.communicate()
+ except OSError as caught_exc:
+ new_exc = exceptions.ClientCertError(caught_exc)
+ raise new_exc from caught_exc
+
+ # Check cert provider command execution error.
+ if process.returncode != 0:
+ raise exceptions.ClientCertError(
+ "Cert provider command returns non-zero status code %s" % process.returncode
+ )
+
+ # Extract certificate (chain), key and passphrase.
+ cert_match = re.findall(_CERT_REGEX, stdout)
+ if len(cert_match) != 1:
+ raise exceptions.ClientCertError("Client SSL certificate is missing or invalid")
+ key_match = re.findall(_KEY_REGEX, stdout)
+ if len(key_match) != 1:
+ raise exceptions.ClientCertError("Client SSL key is missing or invalid")
+ passphrase_match = re.findall(_PASSPHRASE_REGEX, stdout)
+
+ if expect_encrypted_key:
+ if len(passphrase_match) != 1:
+ raise exceptions.ClientCertError("Passphrase is missing or invalid")
+ if b"ENCRYPTED" not in key_match[0]:
+ raise exceptions.ClientCertError("Encrypted private key is expected")
+ return cert_match[0], key_match[0], passphrase_match[0].strip()
+
+ if b"ENCRYPTED" in key_match[0]:
+ raise exceptions.ClientCertError("Encrypted private key is not expected")
+ if len(passphrase_match) > 0:
+ raise exceptions.ClientCertError("Passphrase is not expected")
+ return cert_match[0], key_match[0], None
+
+
+def get_client_ssl_credentials(
+ generate_encrypted_key=False,
+ context_aware_metadata_path=CONTEXT_AWARE_METADATA_PATH,
+):
+ """Returns the client side certificate, private key and passphrase.
+
+ Args:
+ generate_encrypted_key (bool): If set to True, encrypted private key
+ and passphrase will be generated; otherwise, unencrypted private key
+ will be generated and passphrase will be None.
+ context_aware_metadata_path (str): The context_aware_metadata.json file path.
+
+ Returns:
+ Tuple[bool, bytes, bytes, bytes]:
+ A boolean indicating if cert, key and passphrase are obtained, the
+ cert bytes and key bytes both in PEM format, and passphrase bytes.
+
+ Raises:
+ google.auth.exceptions.ClientCertError: if problems occurs when getting
+ the cert, key and passphrase.
+ """
+ metadata_path = _check_dca_metadata_path(context_aware_metadata_path)
+
+ if metadata_path:
+ metadata_json = _read_dca_metadata_file(metadata_path)
+
+ if _CERT_PROVIDER_COMMAND not in metadata_json:
+ raise exceptions.ClientCertError("Cert provider command is not found")
+
+ command = metadata_json[_CERT_PROVIDER_COMMAND]
+
+ if generate_encrypted_key and "--with_passphrase" not in command:
+ command.append("--with_passphrase")
+
+ # Execute the command.
+ cert, key, passphrase = _run_cert_provider_command(
+ command, expect_encrypted_key=generate_encrypted_key
+ )
+ return True, cert, key, passphrase
+
+ return False, None, None, None
+
+
+def get_client_cert_and_key(client_cert_callback=None):
+ """Returns the client side certificate and private key. The function first
+ tries to get certificate and key from client_cert_callback; if the callback
+ is None or doesn't provide certificate and key, the function tries application
+ default SSL credentials.
+
+ Args:
+ client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): An
+ optional callback which returns client certificate bytes and private
+ key bytes both in PEM format.
+
+ Returns:
+ Tuple[bool, bytes, bytes]:
+ A boolean indicating if cert and key are obtained, the cert bytes
+ and key bytes both in PEM format.
+
+ Raises:
+ google.auth.exceptions.ClientCertError: if problems occurs when getting
+ the cert and key.
+ """
+ if client_cert_callback:
+ cert, key = client_cert_callback()
+ return True, cert, key
+
+ has_cert, cert, key, _ = get_client_ssl_credentials(generate_encrypted_key=False)
+ return has_cert, cert, key
+
+
+def decrypt_private_key(key, passphrase):
+ """A helper function to decrypt the private key with the given passphrase.
+ google-auth library doesn't support passphrase protected private key for
+ mutual TLS channel. This helper function can be used to decrypt the
+ passphrase protected private key in order to estalish mutual TLS channel.
+
+ For example, if you have a function which produces client cert, passphrase
+ protected private key and passphrase, you can convert it to a client cert
+ callback function accepted by google-auth::
+
+ from google.auth.transport import _mtls_helper
+
+ def your_client_cert_function():
+ return cert, encrypted_key, passphrase
+
+ # callback accepted by google-auth for mutual TLS channel.
+ def client_cert_callback():
+ cert, encrypted_key, passphrase = your_client_cert_function()
+ decrypted_key = _mtls_helper.decrypt_private_key(encrypted_key,
+ passphrase)
+ return cert, decrypted_key
+
+ Args:
+ key (bytes): The private key bytes in PEM format.
+ passphrase (bytes): The passphrase bytes.
+
+ Returns:
+ bytes: The decrypted private key in PEM format.
+
+ Raises:
+ ImportError: If pyOpenSSL is not installed.
+ OpenSSL.crypto.Error: If there is any problem decrypting the private key.
+ """
+ from OpenSSL import crypto
+
+ # First convert encrypted_key_bytes to PKey object
+ pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key, passphrase=passphrase)
+
+ # Then dump the decrypted key bytes
+ return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
diff --git a/contrib/python/google-auth/py3/google/auth/transport/grpc.py b/contrib/python/google-auth/py3/google/auth/transport/grpc.py
new file mode 100644
index 0000000000..9a817976d7
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/transport/grpc.py
@@ -0,0 +1,343 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Authorization support for gRPC."""
+
+from __future__ import absolute_import
+
+import logging
+import os
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth.transport import _mtls_helper
+from google.oauth2 import service_account
+
+try:
+ import grpc # type: ignore
+except ImportError as caught_exc: # pragma: NO COVER
+ raise ImportError(
+ "gRPC is not installed from please install the grpcio package to use the gRPC transport."
+ ) from caught_exc
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class AuthMetadataPlugin(grpc.AuthMetadataPlugin):
+ """A `gRPC AuthMetadataPlugin`_ that inserts the credentials into each
+ request.
+
+ .. _gRPC AuthMetadataPlugin:
+ http://www.grpc.io/grpc/python/grpc.html#grpc.AuthMetadataPlugin
+
+ Args:
+ credentials (google.auth.credentials.Credentials): The credentials to
+ add to requests.
+ request (google.auth.transport.Request): A HTTP transport request
+ object used to refresh credentials as needed.
+ default_host (Optional[str]): A host like "pubsub.googleapis.com".
+ This is used when a self-signed JWT is created from service
+ account credentials.
+ """
+
+ def __init__(self, credentials, request, default_host=None):
+ # pylint: disable=no-value-for-parameter
+ # pylint doesn't realize that the super method takes no arguments
+ # because this class is the same name as the superclass.
+ super(AuthMetadataPlugin, self).__init__()
+ self._credentials = credentials
+ self._request = request
+ self._default_host = default_host
+
+ def _get_authorization_headers(self, context):
+ """Gets the authorization headers for a request.
+
+ Returns:
+ Sequence[Tuple[str, str]]: A list of request headers (key, value)
+ to add to the request.
+ """
+ headers = {}
+
+ # https://google.aip.dev/auth/4111
+ # Attempt to use self-signed JWTs when a service account is used.
+ # A default host must be explicitly provided since it cannot always
+ # be determined from the context.service_url.
+ if isinstance(self._credentials, service_account.Credentials):
+ self._credentials._create_self_signed_jwt(
+ "https://{}/".format(self._default_host) if self._default_host else None
+ )
+
+ self._credentials.before_request(
+ self._request, context.method_name, context.service_url, headers
+ )
+
+ return list(headers.items())
+
+ def __call__(self, context, callback):
+ """Passes authorization metadata into the given callback.
+
+ Args:
+ context (grpc.AuthMetadataContext): The RPC context.
+ callback (grpc.AuthMetadataPluginCallback): The callback that will
+ be invoked to pass in the authorization metadata.
+ """
+ callback(self._get_authorization_headers(context), None)
+
+
+def secure_authorized_channel(
+ credentials,
+ request,
+ target,
+ ssl_credentials=None,
+ client_cert_callback=None,
+ **kwargs
+):
+ """Creates a secure authorized gRPC channel.
+
+ This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
+ channel can be used to create a stub that can make authorized requests.
+ Users can configure client certificate or rely on device certificates to
+ establish a mutual TLS channel, if the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+ variable is explicitly set to `true`.
+
+ Example::
+
+ import google.auth
+ import google.auth.transport.grpc
+ import google.auth.transport.requests
+ from google.cloud.speech.v1 import cloud_speech_pb2
+
+ # Get credentials.
+ credentials, _ = google.auth.default()
+
+ # Get an HTTP request function to refresh credentials.
+ request = google.auth.transport.requests.Request()
+
+ # Create a channel.
+ channel = google.auth.transport.grpc.secure_authorized_channel(
+ credentials, regular_endpoint, request,
+ ssl_credentials=grpc.ssl_channel_credentials())
+
+ # Use the channel to create a stub.
+ cloud_speech.create_Speech_stub(channel)
+
+ Usage:
+
+ There are actually a couple of options to create a channel, depending on if
+ you want to create a regular or mutual TLS channel.
+
+ First let's list the endpoints (regular vs mutual TLS) to choose from::
+
+ regular_endpoint = 'speech.googleapis.com:443'
+ mtls_endpoint = 'speech.mtls.googleapis.com:443'
+
+ Option 1: create a regular (non-mutual) TLS channel by explicitly setting
+ the ssl_credentials::
+
+ regular_ssl_credentials = grpc.ssl_channel_credentials()
+
+ channel = google.auth.transport.grpc.secure_authorized_channel(
+ credentials, regular_endpoint, request,
+ ssl_credentials=regular_ssl_credentials)
+
+ Option 2: create a mutual TLS channel by calling a callback which returns
+ the client side certificate and the key (Note that
+ `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
+ set to `true`)::
+
+ def my_client_cert_callback():
+ code_to_load_client_cert_and_key()
+ if loaded:
+ return (pem_cert_bytes, pem_key_bytes)
+ raise MyClientCertFailureException()
+
+ try:
+ channel = google.auth.transport.grpc.secure_authorized_channel(
+ credentials, mtls_endpoint, request,
+ client_cert_callback=my_client_cert_callback)
+ except MyClientCertFailureException:
+ # handle the exception
+
+ Option 3: use application default SSL credentials. It searches and uses
+ the command in a context aware metadata file, which is available on devices
+ with endpoint verification support (Note that
+ `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
+ set to `true`).
+ See https://cloud.google.com/endpoint-verification/docs/overview::
+
+ try:
+ default_ssl_credentials = SslCredentials()
+ except:
+ # Exception can be raised if the context aware metadata is malformed.
+ # See :class:`SslCredentials` for the possible exceptions.
+
+ # Choose the endpoint based on the SSL credentials type.
+ if default_ssl_credentials.is_mtls:
+ endpoint_to_use = mtls_endpoint
+ else:
+ endpoint_to_use = regular_endpoint
+ channel = google.auth.transport.grpc.secure_authorized_channel(
+ credentials, endpoint_to_use, request,
+ ssl_credentials=default_ssl_credentials)
+
+ Option 4: not setting ssl_credentials and client_cert_callback. For devices
+ without endpoint verification support or `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+ environment variable is not `true`, a regular TLS channel is created;
+ otherwise, a mutual TLS channel is created, however, the call should be
+ wrapped in a try/except block in case of malformed context aware metadata.
+
+ The following code uses regular_endpoint, it works the same no matter the
+ created channle is regular or mutual TLS. Regular endpoint ignores client
+ certificate and key::
+
+ channel = google.auth.transport.grpc.secure_authorized_channel(
+ credentials, regular_endpoint, request)
+
+ The following code uses mtls_endpoint, if the created channle is regular,
+ and API mtls_endpoint is confgured to require client SSL credentials, API
+ calls using this channel will be rejected::
+
+ channel = google.auth.transport.grpc.secure_authorized_channel(
+ credentials, mtls_endpoint, request)
+
+ Args:
+ credentials (google.auth.credentials.Credentials): The credentials to
+ add to requests.
+ request (google.auth.transport.Request): A HTTP transport request
+ object used to refresh credentials as needed. Even though gRPC
+ is a separate transport, there's no way to refresh the credentials
+ without using a standard http transport.
+ target (str): The host and port of the service.
+ ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
+ credentials. This can be used to specify different certificates.
+ This argument is mutually exclusive with client_cert_callback;
+ providing both will raise an exception.
+ If ssl_credentials and client_cert_callback are None, application
+ default SSL credentials are used if `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+ environment variable is explicitly set to `true`, otherwise one way TLS
+ SSL credentials are used.
+ client_cert_callback (Callable[[], (bytes, bytes)]): Optional
+ callback function to obtain client certicate and key for mutual TLS
+ connection. This argument is mutually exclusive with
+ ssl_credentials; providing both will raise an exception.
+ This argument does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+ environment variable is explicitly set to `true`.
+ kwargs: Additional arguments to pass to :func:`grpc.secure_channel`.
+
+ Returns:
+ grpc.Channel: The created gRPC channel.
+
+ Raises:
+ google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
+ creation failed for any reason.
+ """
+ # Create the metadata plugin for inserting the authorization header.
+ metadata_plugin = AuthMetadataPlugin(credentials, request)
+
+ # Create a set of grpc.CallCredentials using the metadata plugin.
+ google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
+
+ if ssl_credentials and client_cert_callback:
+ raise exceptions.MalformedError(
+ "Received both ssl_credentials and client_cert_callback; "
+ "these are mutually exclusive."
+ )
+
+ # If SSL credentials are not explicitly set, try client_cert_callback and ADC.
+ if not ssl_credentials:
+ use_client_cert = os.getenv(
+ environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
+ )
+ if use_client_cert == "true" and client_cert_callback:
+ # Use the callback if provided.
+ cert, key = client_cert_callback()
+ ssl_credentials = grpc.ssl_channel_credentials(
+ certificate_chain=cert, private_key=key
+ )
+ elif use_client_cert == "true":
+ # Use application default SSL credentials.
+ adc_ssl_credentils = SslCredentials()
+ ssl_credentials = adc_ssl_credentils.ssl_credentials
+ else:
+ ssl_credentials = grpc.ssl_channel_credentials()
+
+ # Combine the ssl credentials and the authorization credentials.
+ composite_credentials = grpc.composite_channel_credentials(
+ ssl_credentials, google_auth_credentials
+ )
+
+ return grpc.secure_channel(target, composite_credentials, **kwargs)
+
+
+class SslCredentials:
+ """Class for application default SSL credentials.
+
+ The behavior is controlled by `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment
+ variable whose default value is `false`. Client certificate will not be used
+ unless the environment variable is explicitly set to `true`. See
+ https://google.aip.dev/auth/4114
+
+ If the environment variable is `true`, then for devices with endpoint verification
+ support, a device certificate will be automatically loaded and mutual TLS will
+ be established.
+ See https://cloud.google.com/endpoint-verification/docs/overview.
+ """
+
+ def __init__(self):
+ use_client_cert = os.getenv(
+ environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
+ )
+ if use_client_cert != "true":
+ self._is_mtls = False
+ else:
+ # Load client SSL credentials.
+ metadata_path = _mtls_helper._check_dca_metadata_path(
+ _mtls_helper.CONTEXT_AWARE_METADATA_PATH
+ )
+ self._is_mtls = metadata_path is not None
+
+ @property
+ def ssl_credentials(self):
+ """Get the created SSL channel credentials.
+
+ For devices with endpoint verification support, if the device certificate
+ loading has any problems, corresponding exceptions will be raised. For
+ a device without endpoint verification support, no exceptions will be
+ raised.
+
+ Returns:
+ grpc.ChannelCredentials: The created grpc channel credentials.
+
+ Raises:
+ google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
+ creation failed for any reason.
+ """
+ if self._is_mtls:
+ try:
+ _, cert, key, _ = _mtls_helper.get_client_ssl_credentials()
+ self._ssl_credentials = grpc.ssl_channel_credentials(
+ certificate_chain=cert, private_key=key
+ )
+ except exceptions.ClientCertError as caught_exc:
+ new_exc = exceptions.MutualTLSChannelError(caught_exc)
+ raise new_exc from caught_exc
+ else:
+ self._ssl_credentials = grpc.ssl_channel_credentials()
+
+ return self._ssl_credentials
+
+ @property
+ def is_mtls(self):
+ """Indicates if the created SSL channel credentials is mutual TLS."""
+ return self._is_mtls
diff --git a/contrib/python/google-auth/py3/google/auth/transport/mtls.py b/contrib/python/google-auth/py3/google/auth/transport/mtls.py
new file mode 100644
index 0000000000..c5707617ff
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/transport/mtls.py
@@ -0,0 +1,103 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+
+"""Utilites for mutual TLS."""
+
+from google.auth import exceptions
+from google.auth.transport import _mtls_helper
+
+
+def has_default_client_cert_source():
+ """Check if default client SSL credentials exists on the device.
+
+ Returns:
+ bool: indicating if the default client cert source exists.
+ """
+ metadata_path = _mtls_helper._check_dca_metadata_path(
+ _mtls_helper.CONTEXT_AWARE_METADATA_PATH
+ )
+ return metadata_path is not None
+
+
+def default_client_cert_source():
+ """Get a callback which returns the default client SSL credentials.
+
+ Returns:
+ Callable[[], [bytes, bytes]]: A callback which returns the default
+ client certificate bytes and private key bytes, both in PEM format.
+
+ Raises:
+ google.auth.exceptions.DefaultClientCertSourceError: If the default
+ client SSL credentials don't exist or are malformed.
+ """
+ if not has_default_client_cert_source():
+ raise exceptions.MutualTLSChannelError(
+ "Default client cert source doesn't exist"
+ )
+
+ def callback():
+ try:
+ _, cert_bytes, key_bytes = _mtls_helper.get_client_cert_and_key()
+ except (OSError, RuntimeError, ValueError) as caught_exc:
+ new_exc = exceptions.MutualTLSChannelError(caught_exc)
+ raise new_exc from caught_exc
+
+ return cert_bytes, key_bytes
+
+ return callback
+
+
+def default_client_encrypted_cert_source(cert_path, key_path):
+ """Get a callback which returns the default encrpyted client SSL credentials.
+
+ Args:
+ cert_path (str): The cert file path. The default client certificate will
+ be written to this file when the returned callback is called.
+ key_path (str): The key file path. The default encrypted client key will
+ be written to this file when the returned callback is called.
+
+ Returns:
+ Callable[[], [str, str, bytes]]: A callback which generates the default
+ client certificate, encrpyted private key and passphrase. It writes
+ the certificate and private key into the cert_path and key_path, and
+ returns the cert_path, key_path and passphrase bytes.
+
+ Raises:
+ google.auth.exceptions.DefaultClientCertSourceError: If any problem
+ occurs when loading or saving the client certificate and key.
+ """
+ if not has_default_client_cert_source():
+ raise exceptions.MutualTLSChannelError(
+ "Default client encrypted cert source doesn't exist"
+ )
+
+ def callback():
+ try:
+ (
+ _,
+ cert_bytes,
+ key_bytes,
+ passphrase_bytes,
+ ) = _mtls_helper.get_client_ssl_credentials(generate_encrypted_key=True)
+ with open(cert_path, "wb") as cert_file:
+ cert_file.write(cert_bytes)
+ with open(key_path, "wb") as key_file:
+ key_file.write(key_bytes)
+ except (exceptions.ClientCertError, OSError) as caught_exc:
+ new_exc = exceptions.MutualTLSChannelError(caught_exc)
+ raise new_exc from caught_exc
+
+ return cert_path, key_path, passphrase_bytes
+
+ return callback
diff --git a/contrib/python/google-auth/py3/google/auth/transport/requests.py b/contrib/python/google-auth/py3/google/auth/transport/requests.py
new file mode 100644
index 0000000000..b9bcad359f
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/transport/requests.py
@@ -0,0 +1,604 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Transport adapter for Requests."""
+
+from __future__ import absolute_import
+
+import functools
+import logging
+import numbers
+import os
+import time
+
+try:
+ import requests
+except ImportError as caught_exc: # pragma: NO COVER
+ raise ImportError(
+ "The requests library is not installed from please install the requests package to use the requests transport."
+ ) from caught_exc
+import requests.adapters # pylint: disable=ungrouped-imports
+import requests.exceptions # pylint: disable=ungrouped-imports
+from requests.packages.urllib3.util.ssl_ import ( # type: ignore
+ create_urllib3_context,
+) # pylint: disable=ungrouped-imports
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+import google.auth.transport._mtls_helper
+from google.oauth2 import service_account
+
+_LOGGER = logging.getLogger(__name__)
+
+_DEFAULT_TIMEOUT = 120 # in seconds
+
+
+class _Response(transport.Response):
+ """Requests transport response adapter.
+
+ Args:
+ response (requests.Response): The raw Requests response.
+ """
+
+ def __init__(self, response):
+ self._response = response
+
+ @property
+ def status(self):
+ return self._response.status_code
+
+ @property
+ def headers(self):
+ return self._response.headers
+
+ @property
+ def data(self):
+ return self._response.content
+
+
+class TimeoutGuard(object):
+ """A context manager raising an error if the suite execution took too long.
+
+ Args:
+ timeout (Union[None, Union[float, Tuple[float, float]]]):
+ The maximum number of seconds a suite can run without the context
+ manager raising a timeout exception on exit. If passed as a tuple,
+ the smaller of the values is taken as a timeout. If ``None``, a
+ timeout error is never raised.
+ timeout_error_type (Optional[Exception]):
+ The type of the error to raise on timeout. Defaults to
+ :class:`requests.exceptions.Timeout`.
+ """
+
+ def __init__(self, timeout, timeout_error_type=requests.exceptions.Timeout):
+ self._timeout = timeout
+ self.remaining_timeout = timeout
+ self._timeout_error_type = timeout_error_type
+
+ def __enter__(self):
+ self._start = time.time()
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if exc_value:
+ return # let the error bubble up automatically
+
+ if self._timeout is None:
+ return # nothing to do, the timeout was not specified
+
+ elapsed = time.time() - self._start
+ deadline_hit = False
+
+ if isinstance(self._timeout, numbers.Number):
+ self.remaining_timeout = self._timeout - elapsed
+ deadline_hit = self.remaining_timeout <= 0
+ else:
+ self.remaining_timeout = tuple(x - elapsed for x in self._timeout)
+ deadline_hit = min(self.remaining_timeout) <= 0
+
+ if deadline_hit:
+ raise self._timeout_error_type()
+
+
+class Request(transport.Request):
+ """Requests request adapter.
+
+ This class is used internally for making requests using various transports
+ in a consistent way. If you use :class:`AuthorizedSession` you do not need
+ to construct or use this class directly.
+
+ This class can be useful if you want to manually refresh a
+ :class:`~google.auth.credentials.Credentials` instance::
+
+ import google.auth.transport.requests
+ import requests
+
+ request = google.auth.transport.requests.Request()
+
+ credentials.refresh(request)
+
+ Args:
+ session (requests.Session): An instance :class:`requests.Session` used
+ to make HTTP requests. If not specified, a session will be created.
+
+ .. automethod:: __call__
+ """
+
+ def __init__(self, session=None):
+ if not session:
+ session = requests.Session()
+
+ self.session = session
+
+ def __del__(self):
+ try:
+ if hasattr(self, "session") and self.session is not None:
+ self.session.close()
+ except TypeError:
+ # NOTE: For certain Python binary built, the queue.Empty exception
+ # might not be considered a normal Python exception causing
+ # TypeError.
+ pass
+
+ def __call__(
+ self,
+ url,
+ method="GET",
+ body=None,
+ headers=None,
+ timeout=_DEFAULT_TIMEOUT,
+ **kwargs
+ ):
+ """Make an HTTP request using requests.
+
+ Args:
+ url (str): The URI to be requested.
+ method (str): The HTTP method to use for the request. Defaults
+ to 'GET'.
+ body (bytes): The payload or body in HTTP request.
+ headers (Mapping[str, str]): Request headers.
+ timeout (Optional[int]): The number of seconds to wait for a
+ response from the server. If not specified or if None, the
+ requests default timeout will be used.
+ kwargs: Additional arguments passed through to the underlying
+ requests :meth:`~requests.Session.request` method.
+
+ Returns:
+ google.auth.transport.Response: The HTTP response.
+
+ Raises:
+ google.auth.exceptions.TransportError: If any exception occurred.
+ """
+ try:
+ _LOGGER.debug("Making request: %s %s", method, url)
+ response = self.session.request(
+ method, url, data=body, headers=headers, timeout=timeout, **kwargs
+ )
+ return _Response(response)
+ except requests.exceptions.RequestException as caught_exc:
+ new_exc = exceptions.TransportError(caught_exc)
+ raise new_exc from caught_exc
+
+
+class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
+ """
+ A TransportAdapter that enables mutual TLS.
+
+ Args:
+ cert (bytes): client certificate in PEM format
+ key (bytes): client private key in PEM format
+
+ Raises:
+ ImportError: if certifi or pyOpenSSL is not installed
+ OpenSSL.crypto.Error: if client cert or key is invalid
+ """
+
+ def __init__(self, cert, key):
+ import certifi
+ from OpenSSL import crypto
+ import urllib3.contrib.pyopenssl # type: ignore
+
+ urllib3.contrib.pyopenssl.inject_into_urllib3()
+
+ pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
+ x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
+
+ ctx_poolmanager = create_urllib3_context()
+ ctx_poolmanager.load_verify_locations(cafile=certifi.where())
+ ctx_poolmanager._ctx.use_certificate(x509)
+ ctx_poolmanager._ctx.use_privatekey(pkey)
+ self._ctx_poolmanager = ctx_poolmanager
+
+ ctx_proxymanager = create_urllib3_context()
+ ctx_proxymanager.load_verify_locations(cafile=certifi.where())
+ ctx_proxymanager._ctx.use_certificate(x509)
+ ctx_proxymanager._ctx.use_privatekey(pkey)
+ self._ctx_proxymanager = ctx_proxymanager
+
+ super(_MutualTlsAdapter, self).__init__()
+
+ def init_poolmanager(self, *args, **kwargs):
+ kwargs["ssl_context"] = self._ctx_poolmanager
+ super(_MutualTlsAdapter, self).init_poolmanager(*args, **kwargs)
+
+ def proxy_manager_for(self, *args, **kwargs):
+ kwargs["ssl_context"] = self._ctx_proxymanager
+ return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs)
+
+
+class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
+ """
+ A TransportAdapter that enables mutual TLS and offloads the client side
+ signing operation to the signing library.
+
+ Args:
+ enterprise_cert_file_path (str): the path to a enterprise cert JSON
+ file. The file should contain the following field:
+
+ {
+ "libs": {
+ "signer_library": "...",
+ "offload_library": "..."
+ }
+ }
+
+ Raises:
+ ImportError: if certifi or pyOpenSSL is not installed
+ google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
+ creation failed for any reason.
+ """
+
+ def __init__(self, enterprise_cert_file_path):
+ import certifi
+ import urllib3.contrib.pyopenssl
+
+ from google.auth.transport import _custom_tls_signer
+
+ # Call inject_into_urllib3 to activate certificate checking. See the
+ # following links for more info:
+ # (1) doc: https://github.com/urllib3/urllib3/blob/cb9ebf8aac5d75f64c8551820d760b72b619beff/src/urllib3/contrib/pyopenssl.py#L31-L32
+ # (2) mTLS example: https://github.com/urllib3/urllib3/issues/474#issuecomment-253168415
+ urllib3.contrib.pyopenssl.inject_into_urllib3()
+
+ self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path)
+ self.signer.load_libraries()
+ self.signer.set_up_custom_key()
+
+ poolmanager = create_urllib3_context()
+ poolmanager.load_verify_locations(cafile=certifi.where())
+ self.signer.attach_to_ssl_context(poolmanager)
+ self._ctx_poolmanager = poolmanager
+
+ proxymanager = create_urllib3_context()
+ proxymanager.load_verify_locations(cafile=certifi.where())
+ self.signer.attach_to_ssl_context(proxymanager)
+ self._ctx_proxymanager = proxymanager
+
+ super(_MutualTlsOffloadAdapter, self).__init__()
+
+ def init_poolmanager(self, *args, **kwargs):
+ kwargs["ssl_context"] = self._ctx_poolmanager
+ super(_MutualTlsOffloadAdapter, self).init_poolmanager(*args, **kwargs)
+
+ def proxy_manager_for(self, *args, **kwargs):
+ kwargs["ssl_context"] = self._ctx_proxymanager
+ return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs)
+
+
+class AuthorizedSession(requests.Session):
+ """A Requests Session class with credentials.
+
+ This class is used to perform requests to API endpoints that require
+ authorization::
+
+ from google.auth.transport.requests import AuthorizedSession
+
+ authed_session = AuthorizedSession(credentials)
+
+ response = authed_session.request(
+ 'GET', 'https://www.googleapis.com/storage/v1/b')
+
+
+ The underlying :meth:`request` implementation handles adding the
+ credentials' headers to the request and refreshing credentials as needed.
+
+ This class also supports mutual TLS via :meth:`configure_mtls_channel`
+ method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+ environment variable must be explicitly set to ``true``, otherwise it does
+ nothing. Assume the environment is set to ``true``, the method behaves in the
+ following manner:
+
+ If client_cert_callback is provided, client certificate and private
+ key are loaded using the callback; if client_cert_callback is None,
+ application default SSL credentials will be used. Exceptions are raised if
+ there are problems with the certificate, private key, or the loading process,
+ so it should be called within a try/except block.
+
+ First we set the environment variable to ``true``, then create an :class:`AuthorizedSession`
+ instance and specify the endpoints::
+
+ regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
+ mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
+
+ authed_session = AuthorizedSession(credentials)
+
+ Now we can pass a callback to :meth:`configure_mtls_channel`::
+
+ def my_cert_callback():
+ # some code to load client cert bytes and private key bytes, both in
+ # PEM format.
+ some_code_to_load_client_cert_and_key()
+ if loaded:
+ return cert, key
+ raise MyClientCertFailureException()
+
+ # Always call configure_mtls_channel within a try/except block.
+ try:
+ authed_session.configure_mtls_channel(my_cert_callback)
+ except:
+ # handle exceptions.
+
+ if authed_session.is_mtls:
+ response = authed_session.request('GET', mtls_endpoint)
+ else:
+ response = authed_session.request('GET', regular_endpoint)
+
+
+ You can alternatively use application default SSL credentials like this::
+
+ try:
+ authed_session.configure_mtls_channel()
+ except:
+ # handle exceptions.
+
+ Args:
+ credentials (google.auth.credentials.Credentials): The credentials to
+ add to the request.
+ refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
+ that credentials should be refreshed and the request should be
+ retried.
+ max_refresh_attempts (int): The maximum number of times to attempt to
+ refresh the credentials and retry the request.
+ refresh_timeout (Optional[int]): The timeout value in seconds for
+ credential refresh HTTP requests.
+ auth_request (google.auth.transport.requests.Request):
+ (Optional) An instance of
+ :class:`~google.auth.transport.requests.Request` used when
+ refreshing credentials. If not passed,
+ an instance of :class:`~google.auth.transport.requests.Request`
+ is created.
+ default_host (Optional[str]): A host like "pubsub.googleapis.com".
+ This is used when a self-signed JWT is created from service
+ account credentials.
+ """
+
+ def __init__(
+ self,
+ credentials,
+ refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
+ max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
+ refresh_timeout=None,
+ auth_request=None,
+ default_host=None,
+ ):
+ super(AuthorizedSession, self).__init__()
+ self.credentials = credentials
+ self._refresh_status_codes = refresh_status_codes
+ self._max_refresh_attempts = max_refresh_attempts
+ self._refresh_timeout = refresh_timeout
+ self._is_mtls = False
+ self._default_host = default_host
+
+ if auth_request is None:
+ self._auth_request_session = requests.Session()
+
+ # Using an adapter to make HTTP requests robust to network errors.
+ # This adapter retrys HTTP requests when network errors occur
+ # and the requests seems safely retryable.
+ retry_adapter = requests.adapters.HTTPAdapter(max_retries=3)
+ self._auth_request_session.mount("https://", retry_adapter)
+
+ # Do not pass `self` as the session here, as it can lead to
+ # infinite recursion.
+ auth_request = Request(self._auth_request_session)
+ else:
+ self._auth_request_session = None
+
+ # Request instance used by internal methods (for example,
+ # credentials.refresh).
+ self._auth_request = auth_request
+
+ # https://google.aip.dev/auth/4111
+ # Attempt to use self-signed JWTs when a service account is used.
+ if isinstance(self.credentials, service_account.Credentials):
+ self.credentials._create_self_signed_jwt(
+ "https://{}/".format(self._default_host) if self._default_host else None
+ )
+
+ def configure_mtls_channel(self, client_cert_callback=None):
+ """Configure the client certificate and key for SSL connection.
+
+ The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is
+ explicitly set to `true`. In this case if client certificate and key are
+ successfully obtained (from the given client_cert_callback or from application
+ default SSL credentials), a :class:`_MutualTlsAdapter` instance will be mounted
+ to "https://" prefix.
+
+ Args:
+ client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
+ The optional callback returns the client certificate and private
+ key bytes both in PEM format.
+ If the callback is None, application default SSL credentials
+ will be used.
+
+ Raises:
+ google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
+ creation failed for any reason.
+ """
+ use_client_cert = os.getenv(
+ environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
+ )
+ if use_client_cert != "true":
+ self._is_mtls = False
+ return
+
+ try:
+ import OpenSSL
+ except ImportError as caught_exc:
+ new_exc = exceptions.MutualTLSChannelError(caught_exc)
+ raise new_exc from caught_exc
+
+ try:
+ (
+ self._is_mtls,
+ cert,
+ key,
+ ) = google.auth.transport._mtls_helper.get_client_cert_and_key(
+ client_cert_callback
+ )
+
+ if self._is_mtls:
+ mtls_adapter = _MutualTlsAdapter(cert, key)
+ self.mount("https://", mtls_adapter)
+ except (
+ exceptions.ClientCertError,
+ ImportError,
+ OpenSSL.crypto.Error,
+ ) as caught_exc:
+ new_exc = exceptions.MutualTLSChannelError(caught_exc)
+ raise new_exc from caught_exc
+
+ def request(
+ self,
+ method,
+ url,
+ data=None,
+ headers=None,
+ max_allowed_time=None,
+ timeout=_DEFAULT_TIMEOUT,
+ **kwargs
+ ):
+ """Implementation of Requests' request.
+
+ Args:
+ timeout (Optional[Union[float, Tuple[float, float]]]):
+ The amount of time in seconds to wait for the server response
+ with each individual request. Can also be passed as a tuple
+ ``(connect_timeout, read_timeout)``. See :meth:`requests.Session.request`
+ documentation for details.
+ max_allowed_time (Optional[float]):
+ If the method runs longer than this, a ``Timeout`` exception is
+ automatically raised. Unlike the ``timeout`` parameter, this
+ value applies to the total method execution time, even if
+ multiple requests are made under the hood.
+
+ Mind that it is not guaranteed that the timeout error is raised
+ at ``max_allowed_time``. It might take longer, for example, if
+ an underlying request takes a lot of time, but the request
+ itself does not timeout, e.g. if a large file is being
+ transmitted. The timout error will be raised after such
+ request completes.
+ """
+ # pylint: disable=arguments-differ
+ # Requests has a ton of arguments to request, but only two
+ # (method, url) are required. We pass through all of the other
+ # arguments to super, so no need to exhaustively list them here.
+
+ # Use a kwarg for this instead of an attribute to maintain
+ # thread-safety.
+ _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
+
+ # Make a copy of the headers. They will be modified by the credentials
+ # and we want to pass the original headers if we recurse.
+ request_headers = headers.copy() if headers is not None else {}
+
+ # Do not apply the timeout unconditionally in order to not override the
+ # _auth_request's default timeout.
+ auth_request = (
+ self._auth_request
+ if timeout is None
+ else functools.partial(self._auth_request, timeout=timeout)
+ )
+
+ remaining_time = max_allowed_time
+
+ with TimeoutGuard(remaining_time) as guard:
+ self.credentials.before_request(auth_request, method, url, request_headers)
+ remaining_time = guard.remaining_timeout
+
+ with TimeoutGuard(remaining_time) as guard:
+ response = super(AuthorizedSession, self).request(
+ method,
+ url,
+ data=data,
+ headers=request_headers,
+ timeout=timeout,
+ **kwargs
+ )
+ remaining_time = guard.remaining_timeout
+
+ # If the response indicated that the credentials needed to be
+ # refreshed, then refresh the credentials and re-attempt the
+ # request.
+ # 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.
+ if (
+ response.status_code in self._refresh_status_codes
+ and _credential_refresh_attempt < self._max_refresh_attempts
+ ):
+
+ _LOGGER.info(
+ "Refreshing credentials due to a %s response. Attempt %s/%s.",
+ response.status_code,
+ _credential_refresh_attempt + 1,
+ self._max_refresh_attempts,
+ )
+
+ # Do not apply the timeout unconditionally in order to not override the
+ # _auth_request's default timeout.
+ auth_request = (
+ self._auth_request
+ if timeout is None
+ else functools.partial(self._auth_request, timeout=timeout)
+ )
+
+ with TimeoutGuard(remaining_time) as guard:
+ self.credentials.refresh(auth_request)
+ remaining_time = guard.remaining_timeout
+
+ # Recurse. Pass in the original headers, not our modified set, but
+ # do pass the adjusted max allowed time (i.e. the remaining total time).
+ return self.request(
+ method,
+ url,
+ data=data,
+ headers=headers,
+ max_allowed_time=remaining_time,
+ timeout=timeout,
+ _credential_refresh_attempt=_credential_refresh_attempt + 1,
+ **kwargs
+ )
+
+ return response
+
+ @property
+ def is_mtls(self):
+ """Indicates if the created SSL channel is mutual TLS."""
+ return self._is_mtls
+
+ def close(self):
+ if self._auth_request_session is not None:
+ self._auth_request_session.close()
+ super(AuthorizedSession, self).close()
diff --git a/contrib/python/google-auth/py3/google/auth/transport/urllib3.py b/contrib/python/google-auth/py3/google/auth/transport/urllib3.py
new file mode 100644
index 0000000000..053d6f7b72
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/transport/urllib3.py
@@ -0,0 +1,437 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Transport adapter for urllib3."""
+
+from __future__ import absolute_import
+
+import logging
+import os
+import warnings
+
+# Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
+# to verify HTTPS requests, and certifi is the recommended and most reliable
+# way to get a root certificate bundle. See
+# http://urllib3.readthedocs.io/en/latest/user-guide.html\
+# #certificate-verification
+# For more details.
+try:
+ import certifi
+except ImportError: # pragma: NO COVER
+ certifi = None # type: ignore
+
+try:
+ import urllib3 # type: ignore
+ import urllib3.exceptions # type: ignore
+except ImportError as caught_exc: # pragma: NO COVER
+ raise ImportError(
+ "The urllib3 library is not installed from please install the "
+ "urllib3 package to use the urllib3 transport."
+ ) from caught_exc
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+from google.oauth2 import service_account
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class _Response(transport.Response):
+ """urllib3 transport response adapter.
+
+ Args:
+ response (urllib3.response.HTTPResponse): The raw urllib3 response.
+ """
+
+ def __init__(self, response):
+ self._response = response
+
+ @property
+ def status(self):
+ return self._response.status
+
+ @property
+ def headers(self):
+ return self._response.headers
+
+ @property
+ def data(self):
+ return self._response.data
+
+
+class Request(transport.Request):
+ """urllib3 request adapter.
+
+ This class is used internally for making requests using various transports
+ in a consistent way. If you use :class:`AuthorizedHttp` you do not need
+ to construct or use this class directly.
+
+ This class can be useful if you want to manually refresh a
+ :class:`~google.auth.credentials.Credentials` instance::
+
+ import google.auth.transport.urllib3
+ import urllib3
+
+ http = urllib3.PoolManager()
+ request = google.auth.transport.urllib3.Request(http)
+
+ credentials.refresh(request)
+
+ Args:
+ http (urllib3.request.RequestMethods): An instance of any urllib3
+ class that implements :class:`~urllib3.request.RequestMethods`,
+ usually :class:`urllib3.PoolManager`.
+
+ .. automethod:: __call__
+ """
+
+ def __init__(self, http):
+ self.http = http
+
+ def __call__(
+ self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
+ ):
+ """Make an HTTP request using urllib3.
+
+ Args:
+ url (str): The URI to be requested.
+ method (str): The HTTP method to use for the request. Defaults
+ to 'GET'.
+ body (bytes): The payload / body in HTTP request.
+ headers (Mapping[str, str]): Request headers.
+ timeout (Optional[int]): The number of seconds to wait for a
+ response from the server. If not specified or if None, the
+ urllib3 default timeout will be used.
+ kwargs: Additional arguments passed throught to the underlying
+ urllib3 :meth:`urlopen` method.
+
+ Returns:
+ google.auth.transport.Response: The HTTP response.
+
+ Raises:
+ google.auth.exceptions.TransportError: If any exception occurred.
+ """
+ # urllib3 uses a sentinel default value for timeout, so only set it if
+ # specified.
+ if timeout is not None:
+ kwargs["timeout"] = timeout
+
+ try:
+ _LOGGER.debug("Making request: %s %s", method, url)
+ response = self.http.request(
+ method, url, body=body, headers=headers, **kwargs
+ )
+ return _Response(response)
+ except urllib3.exceptions.HTTPError as caught_exc:
+ new_exc = exceptions.TransportError(caught_exc)
+ raise new_exc from caught_exc
+
+
+def _make_default_http():
+ if certifi is not None:
+ return urllib3.PoolManager(cert_reqs="CERT_REQUIRED", ca_certs=certifi.where())
+ else:
+ return urllib3.PoolManager()
+
+
+def _make_mutual_tls_http(cert, key):
+ """Create a mutual TLS HTTP connection with the given client cert and key.
+ See https://github.com/urllib3/urllib3/issues/474#issuecomment-253168415
+
+ Args:
+ cert (bytes): client certificate in PEM format
+ key (bytes): client private key in PEM format
+
+ Returns:
+ urllib3.PoolManager: Mutual TLS HTTP connection.
+
+ Raises:
+ ImportError: If certifi or pyOpenSSL is not installed.
+ OpenSSL.crypto.Error: If the cert or key is invalid.
+ """
+ import certifi
+ from OpenSSL import crypto
+ import urllib3.contrib.pyopenssl # type: ignore
+
+ urllib3.contrib.pyopenssl.inject_into_urllib3()
+ ctx = urllib3.util.ssl_.create_urllib3_context()
+ ctx.load_verify_locations(cafile=certifi.where())
+
+ pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
+ x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
+
+ ctx._ctx.use_certificate(x509)
+ ctx._ctx.use_privatekey(pkey)
+
+ http = urllib3.PoolManager(ssl_context=ctx)
+ return http
+
+
+class AuthorizedHttp(urllib3.request.RequestMethods):
+ """A urllib3 HTTP class with credentials.
+
+ This class is used to perform requests to API endpoints that require
+ authorization::
+
+ from google.auth.transport.urllib3 import AuthorizedHttp
+
+ authed_http = AuthorizedHttp(credentials)
+
+ response = authed_http.request(
+ 'GET', 'https://www.googleapis.com/storage/v1/b')
+
+ This class implements :class:`urllib3.request.RequestMethods` and can be
+ used just like any other :class:`urllib3.PoolManager`.
+
+ The underlying :meth:`urlopen` implementation handles adding the
+ credentials' headers to the request and refreshing credentials as needed.
+
+ This class also supports mutual TLS via :meth:`configure_mtls_channel`
+ method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+ environment variable must be explicitly set to `true`, otherwise it does
+ nothing. Assume the environment is set to `true`, the method behaves in the
+ following manner:
+ If client_cert_callback is provided, client certificate and private
+ key are loaded using the callback; if client_cert_callback is None,
+ application default SSL credentials will be used. Exceptions are raised if
+ there are problems with the certificate, private key, or the loading process,
+ so it should be called within a try/except block.
+
+ First we set the environment variable to `true`, then create an :class:`AuthorizedHttp`
+ instance and specify the endpoints::
+
+ regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
+ mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
+
+ authed_http = AuthorizedHttp(credentials)
+
+ Now we can pass a callback to :meth:`configure_mtls_channel`::
+
+ def my_cert_callback():
+ # some code to load client cert bytes and private key bytes, both in
+ # PEM format.
+ some_code_to_load_client_cert_and_key()
+ if loaded:
+ return cert, key
+ raise MyClientCertFailureException()
+
+ # Always call configure_mtls_channel within a try/except block.
+ try:
+ is_mtls = authed_http.configure_mtls_channel(my_cert_callback)
+ except:
+ # handle exceptions.
+
+ if is_mtls:
+ response = authed_http.request('GET', mtls_endpoint)
+ else:
+ response = authed_http.request('GET', regular_endpoint)
+
+ You can alternatively use application default SSL credentials like this::
+
+ try:
+ is_mtls = authed_http.configure_mtls_channel()
+ except:
+ # handle exceptions.
+
+ Args:
+ credentials (google.auth.credentials.Credentials): The credentials to
+ add to the request.
+ http (urllib3.PoolManager): The underlying HTTP object to
+ use to make requests. If not specified, a
+ :class:`urllib3.PoolManager` instance will be constructed with
+ sane defaults.
+ refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
+ that credentials should be refreshed and the request should be
+ retried.
+ max_refresh_attempts (int): The maximum number of times to attempt to
+ refresh the credentials and retry the request.
+ default_host (Optional[str]): A host like "pubsub.googleapis.com".
+ This is used when a self-signed JWT is created from service
+ account credentials.
+ """
+
+ def __init__(
+ self,
+ credentials,
+ http=None,
+ refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
+ max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
+ default_host=None,
+ ):
+ if http is None:
+ self.http = _make_default_http()
+ self._has_user_provided_http = False
+ else:
+ self.http = http
+ self._has_user_provided_http = True
+
+ self.credentials = credentials
+ self._refresh_status_codes = refresh_status_codes
+ self._max_refresh_attempts = max_refresh_attempts
+ self._default_host = default_host
+ # Request instance used by internal methods (for example,
+ # credentials.refresh).
+ self._request = Request(self.http)
+
+ # https://google.aip.dev/auth/4111
+ # Attempt to use self-signed JWTs when a service account is used.
+ if isinstance(self.credentials, service_account.Credentials):
+ self.credentials._create_self_signed_jwt(
+ "https://{}/".format(self._default_host) if self._default_host else None
+ )
+
+ super(AuthorizedHttp, self).__init__()
+
+ def configure_mtls_channel(self, client_cert_callback=None):
+ """Configures mutual TLS channel using the given client_cert_callback or
+ application default SSL credentials. The behavior is controlled by
+ `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable.
+ (1) If the environment variable value is `true`, the function returns True
+ if the channel is mutual TLS and False otherwise. The `http` provided
+ in the constructor will be overwritten.
+ (2) If the environment variable is not set or `false`, the function does
+ nothing and it always return False.
+
+ Args:
+ client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
+ The optional callback returns the client certificate and private
+ key bytes both in PEM format.
+ If the callback is None, application default SSL credentials
+ will be used.
+
+ Returns:
+ True if the channel is mutual TLS and False otherwise.
+
+ Raises:
+ google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
+ creation failed for any reason.
+ """
+ use_client_cert = os.getenv(
+ environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
+ )
+ if use_client_cert != "true":
+ return False
+
+ try:
+ import OpenSSL
+ except ImportError as caught_exc:
+ new_exc = exceptions.MutualTLSChannelError(caught_exc)
+ raise new_exc from caught_exc
+
+ try:
+ found_cert_key, cert, key = transport._mtls_helper.get_client_cert_and_key(
+ client_cert_callback
+ )
+
+ if found_cert_key:
+ self.http = _make_mutual_tls_http(cert, key)
+ else:
+ self.http = _make_default_http()
+ except (
+ exceptions.ClientCertError,
+ ImportError,
+ OpenSSL.crypto.Error,
+ ) as caught_exc:
+ new_exc = exceptions.MutualTLSChannelError(caught_exc)
+ raise new_exc from caught_exc
+
+ if self._has_user_provided_http:
+ self._has_user_provided_http = False
+ warnings.warn(
+ "`http` provided in the constructor is overwritten", UserWarning
+ )
+
+ return found_cert_key
+
+ def urlopen(self, method, url, body=None, headers=None, **kwargs):
+ """Implementation of urllib3's urlopen."""
+ # pylint: disable=arguments-differ
+ # We use kwargs to collect additional args that we don't need to
+ # introspect here. However, we do explicitly collect the two
+ # positional arguments.
+
+ # Use a kwarg for this instead of an attribute to maintain
+ # thread-safety.
+ _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
+
+ if headers is None:
+ headers = self.headers
+
+ # Make a copy of the headers. They will be modified by the credentials
+ # and we want to pass the original headers if we recurse.
+ request_headers = headers.copy()
+
+ self.credentials.before_request(self._request, method, url, request_headers)
+
+ response = self.http.urlopen(
+ method, url, body=body, headers=request_headers, **kwargs
+ )
+
+ # If the response indicated that the credentials needed to be
+ # refreshed, then refresh the credentials and re-attempt the
+ # request.
+ # 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.
+ # The reason urllib3's retries aren't used is because they
+ # don't allow you to modify the request headers. :/
+ if (
+ response.status in self._refresh_status_codes
+ and _credential_refresh_attempt < self._max_refresh_attempts
+ ):
+
+ _LOGGER.info(
+ "Refreshing credentials due to a %s response. Attempt %s/%s.",
+ response.status,
+ _credential_refresh_attempt + 1,
+ self._max_refresh_attempts,
+ )
+
+ self.credentials.refresh(self._request)
+
+ # Recurse. Pass in the original headers, not our modified set.
+ return self.urlopen(
+ method,
+ url,
+ body=body,
+ headers=headers,
+ _credential_refresh_attempt=_credential_refresh_attempt + 1,
+ **kwargs
+ )
+
+ return response
+
+ # Proxy methods for compliance with the urllib3.PoolManager interface
+
+ def __enter__(self):
+ """Proxy to ``self.http``."""
+ return self.http.__enter__()
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """Proxy to ``self.http``."""
+ return self.http.__exit__(exc_type, exc_val, exc_tb)
+
+ def __del__(self):
+ if hasattr(self, "http") and self.http is not None:
+ self.http.clear()
+
+ @property
+ def headers(self):
+ """Proxy to ``self.http``."""
+ return self.http.headers
+
+ @headers.setter
+ def headers(self, value):
+ """Proxy to ``self.http``."""
+ self.http.headers = value
diff --git a/contrib/python/google-auth/py3/google/auth/version.py b/contrib/python/google-auth/py3/google/auth/version.py
new file mode 100644
index 0000000000..491187e6d7
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/version.py
@@ -0,0 +1,15 @@
+# Copyright 2021 Google LLC
+#
+# 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.
+
+__version__ = "2.23.0"
diff --git a/contrib/python/google-auth/py3/google/oauth2/__init__.py b/contrib/python/google-auth/py3/google/oauth2/__init__.py
new file mode 100644
index 0000000000..4fb71fd1ad
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2016 Google LLC
+#
+# 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 OAuth 2.0 Library for Python."""
diff --git a/contrib/python/google-auth/py3/google/oauth2/_client.py b/contrib/python/google-auth/py3/google/oauth2/_client.py
new file mode 100644
index 0000000000..d2af6c8aa8
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/_client.py
@@ -0,0 +1,507 @@
+# Copyright 2016 Google LLC
+#
+# 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 client.
+
+This is a client for interacting with an OAuth 2.0 authorization server's
+token endpoint.
+
+For more information about the token endpoint, see
+`Section 3.1 of rfc6749`_
+
+.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
+"""
+
+import datetime
+import http.client as http_client
+import json
+import urllib
+
+from google.auth import _exponential_backoff
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth import metrics
+from google.auth import transport
+
+_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
+_JSON_CONTENT_TYPE = "application/json"
+_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
+_REFRESH_GRANT_TYPE = "refresh_token"
+_IAM_IDTOKEN_ENDPOINT = (
+ "https://iamcredentials.googleapis.com/v1/"
+ + "projects/-/serviceAccounts/{}:generateIdToken"
+)
+
+
+def _handle_error_response(response_data, retryable_error):
+ """Translates an error response into an exception.
+
+ Args:
+ response_data (Mapping | str): The decoded response data.
+ retryable_error Optional[bool]: A boolean indicating if an error is retryable.
+ Defaults to False.
+
+ Raises:
+ google.auth.exceptions.RefreshError: The errors contained in response_data.
+ """
+
+ retryable_error = retryable_error if retryable_error else False
+
+ if isinstance(response_data, str):
+ raise exceptions.RefreshError(response_data, retryable=retryable_error)
+ try:
+ error_details = "{}: {}".format(
+ response_data["error"], response_data.get("error_description")
+ )
+ # If no details could be extracted, use the response data.
+ except (KeyError, ValueError):
+ error_details = json.dumps(response_data)
+
+ raise exceptions.RefreshError(
+ error_details, response_data, retryable=retryable_error
+ )
+
+
+def _can_retry(status_code, response_data):
+ """Checks if a request can be retried by inspecting the status code
+ and response body of the request.
+
+ Args:
+ status_code (int): The response status code.
+ response_data (Mapping | str): The decoded response data.
+
+ Returns:
+ bool: True if the response is retryable. False otherwise.
+ """
+ if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES:
+ return True
+
+ try:
+ # For a failed response, response_body could be a string
+ error_desc = response_data.get("error_description") or ""
+ error_code = response_data.get("error") or ""
+
+ if not isinstance(error_code, str) or not isinstance(error_desc, str):
+ return False
+
+ # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1
+ # This is needed because a redirect will not return a 500 status code.
+ retryable_error_descriptions = {
+ "internal_failure",
+ "server_error",
+ "temporarily_unavailable",
+ }
+
+ if any(e in retryable_error_descriptions for e in (error_code, error_desc)):
+ return True
+
+ except AttributeError:
+ pass
+
+ return False
+
+
+def _parse_expiry(response_data):
+ """Parses the expiry field from a response into a datetime.
+
+ Args:
+ response_data (Mapping): The JSON-parsed response data.
+
+ Returns:
+ Optional[datetime]: The expiration or ``None`` if no expiration was
+ specified.
+ """
+ expires_in = response_data.get("expires_in", None)
+
+ if expires_in is not None:
+ # Some services do not respect the OAUTH2.0 RFC and send expires_in as a
+ # JSON String.
+ if isinstance(expires_in, str):
+ expires_in = int(expires_in)
+
+ return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
+ else:
+ return None
+
+
+def _token_endpoint_request_no_throw(
+ request,
+ token_uri,
+ body,
+ access_token=None,
+ use_json=False,
+ can_retry=True,
+ headers=None,
+ **kwargs
+):
+ """Makes a request to the OAuth 2.0 authorization server's token endpoint.
+ This function doesn't throw on response errors.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+ URI.
+ body (Mapping[str, str]): The parameters to send in the request body.
+ access_token (Optional(str)): The access token needed to make the request.
+ use_json (Optional(bool)): Use urlencoded format or json format for the
+ content type. The default value is False.
+ can_retry (bool): Enable or disable request retry behavior.
+ headers (Optional[Mapping[str, str]]): The headers for the request.
+ kwargs: Additional arguments passed on to the request method. The
+ kwargs will be passed to `requests.request` method, see:
+ https://docs.python-requests.org/en/latest/api/#requests.request.
+ For example, you can use `cert=("cert_pem_path", "key_pem_path")`
+ to set up client side SSL certificate, and use
+ `verify="ca_bundle_path"` to set up the CA certificates for sever
+ side SSL certificate verification.
+
+ Returns:
+ Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
+ if the request is successful, a mapping for the JSON-decoded response
+ data and in the case of an error a boolean indicating if the error
+ is retryable.
+ """
+ if use_json:
+ headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE}
+ body = json.dumps(body).encode("utf-8")
+ else:
+ headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE}
+ body = urllib.parse.urlencode(body).encode("utf-8")
+
+ if access_token:
+ headers_to_use["Authorization"] = "Bearer {}".format(access_token)
+
+ if headers:
+ headers_to_use.update(headers)
+
+ def _perform_request():
+ response = request(
+ method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs
+ )
+ response_body = (
+ response.data.decode("utf-8")
+ if hasattr(response.data, "decode")
+ else response.data
+ )
+ response_data = ""
+ try:
+ # response_body should be a JSON
+ response_data = json.loads(response_body)
+ except ValueError:
+ response_data = response_body
+
+ if response.status == http_client.OK:
+ return True, response_data, None
+
+ retryable_error = _can_retry(
+ status_code=response.status, response_data=response_data
+ )
+
+ return False, response_data, retryable_error
+
+ request_succeeded, response_data, retryable_error = _perform_request()
+
+ if request_succeeded or not retryable_error or not can_retry:
+ return request_succeeded, response_data, retryable_error
+
+ retries = _exponential_backoff.ExponentialBackoff()
+ for _ in retries:
+ request_succeeded, response_data, retryable_error = _perform_request()
+ if request_succeeded or not retryable_error:
+ return request_succeeded, response_data, retryable_error
+
+ return False, response_data, retryable_error
+
+
+def _token_endpoint_request(
+ request,
+ token_uri,
+ body,
+ access_token=None,
+ use_json=False,
+ can_retry=True,
+ headers=None,
+ **kwargs
+):
+ """Makes a request to the OAuth 2.0 authorization server's token endpoint.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+ URI.
+ body (Mapping[str, str]): The parameters to send in the request body.
+ access_token (Optional(str)): The access token needed to make the request.
+ use_json (Optional(bool)): Use urlencoded format or json format for the
+ content type. The default value is False.
+ can_retry (bool): Enable or disable request retry behavior.
+ headers (Optional[Mapping[str, str]]): The headers for the request.
+ kwargs: Additional arguments passed on to the request method. The
+ kwargs will be passed to `requests.request` method, see:
+ https://docs.python-requests.org/en/latest/api/#requests.request.
+ For example, you can use `cert=("cert_pem_path", "key_pem_path")`
+ to set up client side SSL certificate, and use
+ `verify="ca_bundle_path"` to set up the CA certificates for sever
+ side SSL certificate verification.
+
+ Returns:
+ Mapping[str, str]: The JSON-decoded response data.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+ """
+
+ response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw(
+ request,
+ token_uri,
+ body,
+ access_token=access_token,
+ use_json=use_json,
+ can_retry=can_retry,
+ headers=headers,
+ **kwargs
+ )
+ if not response_status_ok:
+ _handle_error_response(response_data, retryable_error)
+ return response_data
+
+
+def jwt_grant(request, token_uri, assertion, can_retry=True):
+ """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
+
+ For more details, see `rfc7523 section 4`_.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+ URI.
+ assertion (str): The OAuth 2.0 assertion.
+ can_retry (bool): Enable or disable request retry behavior.
+
+ Returns:
+ Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
+ expiration, and additional data returned by the token endpoint.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+
+ .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
+ """
+ body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
+
+ response_data = _token_endpoint_request(
+ request,
+ token_uri,
+ body,
+ can_retry=can_retry,
+ headers={
+ metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion()
+ },
+ )
+
+ try:
+ access_token = response_data["access_token"]
+ except KeyError as caught_exc:
+ new_exc = exceptions.RefreshError(
+ "No access token in response.", response_data, retryable=False
+ )
+ raise new_exc from caught_exc
+
+ expiry = _parse_expiry(response_data)
+
+ return access_token, expiry, response_data
+
+
+def call_iam_generate_id_token_endpoint(request, signer_email, audience, access_token):
+ """Call iam.generateIdToken endpoint to get ID token.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ signer_email (str): The signer email used to form the IAM
+ generateIdToken endpoint.
+ audience (str): The audience for the ID token.
+ access_token (str): The access token used to call the IAM endpoint.
+
+ Returns:
+ Tuple[str, datetime]: The ID token and expiration.
+ """
+ body = {"audience": audience, "includeEmail": "true", "useEmailAzp": "true"}
+
+ response_data = _token_endpoint_request(
+ request,
+ _IAM_IDTOKEN_ENDPOINT.format(signer_email),
+ body,
+ access_token=access_token,
+ use_json=True,
+ )
+
+ try:
+ id_token = response_data["token"]
+ except KeyError as caught_exc:
+ new_exc = exceptions.RefreshError(
+ "No ID token in response.", response_data, retryable=False
+ )
+ raise new_exc from caught_exc
+
+ payload = jwt.decode(id_token, verify=False)
+ expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
+
+ return id_token, expiry
+
+
+def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
+ """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
+ requests an OpenID Connect ID Token instead of an access token.
+
+ This is a variant on the standard JWT Profile that is currently unique
+ to Google. This was added for the benefit of authenticating to services
+ that require ID Tokens instead of access tokens or JWT bearer tokens.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorization server's token endpoint
+ URI.
+ assertion (str): JWT token signed by a service account. The token's
+ payload must include a ``target_audience`` claim.
+ can_retry (bool): Enable or disable request retry behavior.
+
+ Returns:
+ Tuple[str, Optional[datetime], Mapping[str, str]]:
+ The (encoded) Open ID Connect ID Token, expiration, and additional
+ data returned by the endpoint.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+ """
+ body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
+
+ response_data = _token_endpoint_request(
+ request,
+ token_uri,
+ body,
+ can_retry=can_retry,
+ headers={
+ metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion()
+ },
+ )
+
+ try:
+ id_token = response_data["id_token"]
+ except KeyError as caught_exc:
+ new_exc = exceptions.RefreshError(
+ "No ID token in response.", response_data, retryable=False
+ )
+ raise new_exc from caught_exc
+
+ payload = jwt.decode(id_token, verify=False)
+ expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
+
+ return id_token, expiry, response_data
+
+
+def _handle_refresh_grant_response(response_data, refresh_token):
+ """Extract tokens from refresh grant response.
+
+ Args:
+ response_data (Mapping[str, str]): Refresh grant response data.
+ refresh_token (str): Current refresh token.
+
+ Returns:
+ Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,
+ refresh token, expiration, and additional data returned by the token
+ endpoint. If response_data doesn't have refresh token, then the current
+ refresh token will be returned.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+ """
+ try:
+ access_token = response_data["access_token"]
+ except KeyError as caught_exc:
+ new_exc = exceptions.RefreshError(
+ "No access token in response.", response_data, retryable=False
+ )
+ raise new_exc from caught_exc
+
+ refresh_token = response_data.get("refresh_token", refresh_token)
+ expiry = _parse_expiry(response_data)
+
+ return access_token, refresh_token, expiry, response_data
+
+
+def refresh_grant(
+ request,
+ token_uri,
+ refresh_token,
+ client_id,
+ client_secret,
+ scopes=None,
+ rapt_token=None,
+ can_retry=True,
+):
+ """Implements the OAuth 2.0 refresh token grant.
+
+ For more details, see `rfc678 section 6`_.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+ URI.
+ refresh_token (str): The refresh token to use to get a new access
+ token.
+ client_id (str): The OAuth 2.0 application's client ID.
+ client_secret (str): The Oauth 2.0 appliaction's client secret.
+ scopes (Optional(Sequence[str])): Scopes to request. If present, all
+ scopes must be authorized for the refresh token. Useful if refresh
+ token has a wild card scope (e.g.
+ 'https://www.googleapis.com/auth/any-api').
+ rapt_token (Optional(str)): The reauth Proof Token.
+ can_retry (bool): Enable or disable request retry behavior.
+
+ Returns:
+ Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
+ token, new or current refresh token, expiration, and additional data
+ returned by the token endpoint.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+
+ .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
+ """
+ body = {
+ "grant_type": _REFRESH_GRANT_TYPE,
+ "client_id": client_id,
+ "client_secret": client_secret,
+ "refresh_token": refresh_token,
+ }
+ if scopes:
+ body["scope"] = " ".join(scopes)
+ if rapt_token:
+ body["rapt"] = rapt_token
+
+ response_data = _token_endpoint_request(
+ request, token_uri, body, can_retry=can_retry
+ )
+ return _handle_refresh_grant_response(response_data, refresh_token)
diff --git a/contrib/python/google-auth/py3/google/oauth2/_client_async.py b/contrib/python/google-auth/py3/google/oauth2/_client_async.py
new file mode 100644
index 0000000000..2858d862b0
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/_client_async.py
@@ -0,0 +1,292 @@
+# Copyright 2020 Google LLC
+#
+# 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 async client.
+
+This is a client for interacting with an OAuth 2.0 authorization server's
+token endpoint.
+
+For more information about the token endpoint, see
+`Section 3.1 of rfc6749`_
+
+.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
+"""
+
+import datetime
+import http.client as http_client
+import json
+import urllib
+
+from google.auth import _exponential_backoff
+from google.auth import exceptions
+from google.auth import jwt
+from google.oauth2 import _client as client
+
+
+async def _token_endpoint_request_no_throw(
+ request, token_uri, body, access_token=None, use_json=False, can_retry=True
+):
+ """Makes a request to the OAuth 2.0 authorization server's token endpoint.
+ This function doesn't throw on response errors.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+ URI.
+ body (Mapping[str, str]): The parameters to send in the request body.
+ access_token (Optional(str)): The access token needed to make the request.
+ use_json (Optional(bool)): Use urlencoded format or json format for the
+ content type. The default value is False.
+ can_retry (bool): Enable or disable request retry behavior.
+
+ Returns:
+ Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
+ if the request is successful, a mapping for the JSON-decoded response
+ data and in the case of an error a boolean indicating if the error
+ is retryable.
+ """
+ if use_json:
+ headers = {"Content-Type": client._JSON_CONTENT_TYPE}
+ body = json.dumps(body).encode("utf-8")
+ else:
+ headers = {"Content-Type": client._URLENCODED_CONTENT_TYPE}
+ body = urllib.parse.urlencode(body).encode("utf-8")
+
+ if access_token:
+ headers["Authorization"] = "Bearer {}".format(access_token)
+
+ async def _perform_request():
+ response = await request(
+ method="POST", url=token_uri, headers=headers, body=body
+ )
+
+ # Using data.read() resulted in zlib decompression errors. This may require future investigation.
+ response_body1 = await response.content()
+
+ response_body = (
+ response_body1.decode("utf-8")
+ if hasattr(response_body1, "decode")
+ else response_body1
+ )
+
+ try:
+ response_data = json.loads(response_body)
+ except ValueError:
+ response_data = response_body
+
+ if response.status == http_client.OK:
+ return True, response_data, None
+
+ retryable_error = client._can_retry(
+ status_code=response.status, response_data=response_data
+ )
+
+ return False, response_data, retryable_error
+
+ request_succeeded, response_data, retryable_error = await _perform_request()
+
+ if request_succeeded or not retryable_error or not can_retry:
+ return request_succeeded, response_data, retryable_error
+
+ retries = _exponential_backoff.ExponentialBackoff()
+ for _ in retries:
+ request_succeeded, response_data, retryable_error = await _perform_request()
+ if request_succeeded or not retryable_error:
+ return request_succeeded, response_data, retryable_error
+
+ return False, response_data, retryable_error
+
+
+async def _token_endpoint_request(
+ request, token_uri, body, access_token=None, use_json=False, can_retry=True
+):
+ """Makes a request to the OAuth 2.0 authorization server's token endpoint.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+ URI.
+ body (Mapping[str, str]): The parameters to send in the request body.
+ access_token (Optional(str)): The access token needed to make the request.
+ use_json (Optional(bool)): Use urlencoded format or json format for the
+ content type. The default value is False.
+ can_retry (bool): Enable or disable request retry behavior.
+
+ Returns:
+ Mapping[str, str]: The JSON-decoded response data.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+ """
+
+ response_status_ok, response_data, retryable_error = await _token_endpoint_request_no_throw(
+ request,
+ token_uri,
+ body,
+ access_token=access_token,
+ use_json=use_json,
+ can_retry=can_retry,
+ )
+ if not response_status_ok:
+ client._handle_error_response(response_data, retryable_error)
+ return response_data
+
+
+async def jwt_grant(request, token_uri, assertion, can_retry=True):
+ """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
+
+ For more details, see `rfc7523 section 4`_.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+ URI.
+ assertion (str): The OAuth 2.0 assertion.
+ can_retry (bool): Enable or disable request retry behavior.
+
+ Returns:
+ Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
+ expiration, and additional data returned by the token endpoint.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+
+ .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
+ """
+ body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE}
+
+ response_data = await _token_endpoint_request(
+ request, token_uri, body, can_retry=can_retry
+ )
+
+ try:
+ access_token = response_data["access_token"]
+ except KeyError as caught_exc:
+ new_exc = exceptions.RefreshError(
+ "No access token in response.", response_data, retryable=False
+ )
+ raise new_exc from caught_exc
+
+ expiry = client._parse_expiry(response_data)
+
+ return access_token, expiry, response_data
+
+
+async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
+ """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
+ requests an OpenID Connect ID Token instead of an access token.
+
+ This is a variant on the standard JWT Profile that is currently unique
+ to Google. This was added for the benefit of authenticating to services
+ that require ID Tokens instead of access tokens or JWT bearer tokens.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorization server's token endpoint
+ URI.
+ assertion (str): JWT token signed by a service account. The token's
+ payload must include a ``target_audience`` claim.
+ can_retry (bool): Enable or disable request retry behavior.
+
+ Returns:
+ Tuple[str, Optional[datetime], Mapping[str, str]]:
+ The (encoded) Open ID Connect ID Token, expiration, and additional
+ data returned by the endpoint.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+ """
+ body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE}
+
+ response_data = await _token_endpoint_request(
+ request, token_uri, body, can_retry=can_retry
+ )
+
+ try:
+ id_token = response_data["id_token"]
+ except KeyError as caught_exc:
+ new_exc = exceptions.RefreshError(
+ "No ID token in response.", response_data, retryable=False
+ )
+ raise new_exc from caught_exc
+
+ payload = jwt.decode(id_token, verify=False)
+ expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
+
+ return id_token, expiry, response_data
+
+
+async def refresh_grant(
+ request,
+ token_uri,
+ refresh_token,
+ client_id,
+ client_secret,
+ scopes=None,
+ rapt_token=None,
+ can_retry=True,
+):
+ """Implements the OAuth 2.0 refresh token grant.
+
+ For more details, see `rfc678 section 6`_.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+ URI.
+ refresh_token (str): The refresh token to use to get a new access
+ token.
+ client_id (str): The OAuth 2.0 application's client ID.
+ client_secret (str): The Oauth 2.0 appliaction's client secret.
+ scopes (Optional(Sequence[str])): Scopes to request. If present, all
+ scopes must be authorized for the refresh token. Useful if refresh
+ token has a wild card scope (e.g.
+ 'https://www.googleapis.com/auth/any-api').
+ rapt_token (Optional(str)): The reauth Proof Token.
+ can_retry (bool): Enable or disable request retry behavior.
+
+ Returns:
+ Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
+ access token, new or current refresh token, expiration, and additional data
+ returned by the token endpoint.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+
+ .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
+ """
+ body = {
+ "grant_type": client._REFRESH_GRANT_TYPE,
+ "client_id": client_id,
+ "client_secret": client_secret,
+ "refresh_token": refresh_token,
+ }
+ if scopes:
+ body["scope"] = " ".join(scopes)
+ if rapt_token:
+ body["rapt"] = rapt_token
+
+ response_data = await _token_endpoint_request(
+ request, token_uri, body, can_retry=can_retry
+ )
+ return client._handle_refresh_grant_response(response_data, refresh_token)
diff --git a/contrib/python/google-auth/py3/google/oauth2/_credentials_async.py b/contrib/python/google-auth/py3/google/oauth2/_credentials_async.py
new file mode 100644
index 0000000000..e7b9637c82
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/_credentials_async.py
@@ -0,0 +1,112 @@
+# Copyright 2020 Google LLC
+#
+# 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 Async Credentials.
+
+This module provides credentials based on OAuth 2.0 access and refresh tokens.
+These credentials usually access resources on behalf of a user (resource
+owner).
+
+Specifically, this is intended to use access tokens acquired using the
+`Authorization Code grant`_ and can refresh those tokens using a
+optional `refresh token`_.
+
+Obtaining the initial access and refresh token is outside of the scope of this
+module. Consult `rfc6749 section 4.1`_ for complete details on the
+Authorization Code grant flow.
+
+.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1
+.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6
+.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
+"""
+
+from google.auth import _credentials_async as credentials
+from google.auth import _helpers
+from google.auth import exceptions
+from google.oauth2 import _reauth_async as reauth
+from google.oauth2 import credentials as oauth2_credentials
+
+
+class Credentials(oauth2_credentials.Credentials):
+ """Credentials using OAuth 2.0 access and refresh tokens.
+
+ The credentials are considered immutable. If you want to modify the
+ quota project, use :meth:`with_quota_project` or ::
+
+ credentials = credentials.with_quota_project('myproject-123)
+ """
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ async def refresh(self, request):
+ if (
+ self._refresh_token is None
+ or self._token_uri is None
+ or self._client_id is None
+ or self._client_secret is None
+ ):
+ raise exceptions.RefreshError(
+ "The credentials do not contain the necessary fields need to "
+ "refresh the access token. You must specify refresh_token, "
+ "token_uri, client_id, and client_secret."
+ )
+
+ (
+ access_token,
+ refresh_token,
+ expiry,
+ grant_response,
+ rapt_token,
+ ) = await reauth.refresh_grant(
+ request,
+ self._token_uri,
+ self._refresh_token,
+ self._client_id,
+ self._client_secret,
+ scopes=self._scopes,
+ rapt_token=self._rapt_token,
+ enable_reauth_refresh=self._enable_reauth_refresh,
+ )
+
+ self.token = access_token
+ self.expiry = expiry
+ self._refresh_token = refresh_token
+ self._id_token = grant_response.get("id_token")
+ self._rapt_token = rapt_token
+
+ if self._scopes and "scope" in grant_response:
+ requested_scopes = frozenset(self._scopes)
+ granted_scopes = frozenset(grant_response["scope"].split())
+ scopes_requested_but_not_granted = requested_scopes - granted_scopes
+ if scopes_requested_but_not_granted:
+ raise exceptions.RefreshError(
+ "Not all requested scopes were granted by the "
+ "authorization server, missing scopes {}.".format(
+ ", ".join(scopes_requested_but_not_granted)
+ )
+ )
+
+
+class UserAccessTokenCredentials(oauth2_credentials.UserAccessTokenCredentials):
+ """Access token credentials for user account.
+
+ Obtain the access token for a given user account or the current active
+ user account with the ``gcloud auth print-access-token`` command.
+
+ Args:
+ account (Optional[str]): Account to get the access token for. If not
+ specified, the current active account will be used.
+ quota_project_id (Optional[str]): The project ID used for quota
+ and billing.
+
+ """
diff --git a/contrib/python/google-auth/py3/google/oauth2/_id_token_async.py b/contrib/python/google-auth/py3/google/oauth2/_id_token_async.py
new file mode 100644
index 0000000000..6594e416ae
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/_id_token_async.py
@@ -0,0 +1,285 @@
+# Copyright 2020 Google LLC
+#
+# 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 ID Token helpers.
+
+Provides support for verifying `OpenID Connect ID Tokens`_, especially ones
+generated by Google infrastructure.
+
+To parse and verify an ID Token issued by Google's OAuth 2.0 authorization
+server use :func:`verify_oauth2_token`. To verify an ID Token issued by
+Firebase, use :func:`verify_firebase_token`.
+
+A general purpose ID Token verifier is available as :func:`verify_token`.
+
+Example::
+
+ from google.oauth2 import _id_token_async
+ from google.auth.transport import aiohttp_requests
+
+ request = aiohttp_requests.Request()
+
+ id_info = await _id_token_async.verify_oauth2_token(
+ token, request, 'my-client-id.example.com')
+
+ if id_info['iss'] != 'https://accounts.google.com':
+ raise ValueError('Wrong issuer.')
+
+ userid = id_info['sub']
+
+By default, this will re-fetch certificates for each verification. Because
+Google's public keys are only changed infrequently (on the order of once per
+day), you may wish to take advantage of caching to reduce latency and the
+potential for network errors. This can be accomplished using an external
+library like `CacheControl`_ to create a cache-aware
+:class:`google.auth.transport.Request`::
+
+ import cachecontrol
+ import google.auth.transport.requests
+ import requests
+
+ session = requests.session()
+ cached_session = cachecontrol.CacheControl(session)
+ request = google.auth.transport.requests.Request(session=cached_session)
+
+.. _OpenID Connect ID Token:
+ http://openid.net/specs/openid-connect-core-1_0.html#IDToken
+.. _CacheControl: https://cachecontrol.readthedocs.io
+"""
+
+import http.client as http_client
+import json
+import os
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth.transport import requests
+from google.oauth2 import id_token as sync_id_token
+
+
+async def _fetch_certs(request, certs_url):
+ """Fetches certificates.
+
+ Google-style cerificate endpoints return JSON in the format of
+ ``{'key id': 'x509 certificate'}``.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests. This must be an aiohttp request.
+ certs_url (str): The certificate endpoint URL.
+
+ Returns:
+ Mapping[str, str]: A mapping of public key ID to x.509 certificate
+ data.
+ """
+ response = await request(certs_url, method="GET")
+
+ if response.status != http_client.OK:
+ raise exceptions.TransportError(
+ "Could not fetch certificates at {}".format(certs_url)
+ )
+
+ data = await response.content()
+
+ return json.loads(data)
+
+
+async def verify_token(
+ id_token,
+ request,
+ audience=None,
+ certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
+ clock_skew_in_seconds=0,
+):
+ """Verifies an ID token and returns the decoded token.
+
+ Args:
+ id_token (Union[str, bytes]): The encoded token.
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests. This must be an aiohttp request.
+ audience (str): The audience that this token is intended for. If None
+ then the audience is not verified.
+ certs_url (str): The URL that specifies the certificates to use to
+ verify the token. This URL should return JSON in the format of
+ ``{'key id': 'x509 certificate'}``.
+ clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+ validation.
+
+ Returns:
+ Mapping[str, Any]: The decoded token.
+ """
+ certs = await _fetch_certs(request, certs_url)
+
+ return jwt.decode(
+ id_token,
+ certs=certs,
+ audience=audience,
+ clock_skew_in_seconds=clock_skew_in_seconds,
+ )
+
+
+async def verify_oauth2_token(
+ id_token, request, audience=None, clock_skew_in_seconds=0
+):
+ """Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
+
+ Args:
+ id_token (Union[str, bytes]): The encoded token.
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests. This must be an aiohttp request.
+ audience (str): The audience that this token is intended for. This is
+ typically your application's OAuth 2.0 client ID. If None then the
+ audience is not verified.
+ clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+ validation.
+
+ Returns:
+ Mapping[str, Any]: The decoded token.
+
+ Raises:
+ exceptions.GoogleAuthError: If the issuer is invalid.
+ """
+ idinfo = await verify_token(
+ id_token,
+ request,
+ audience=audience,
+ certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
+ clock_skew_in_seconds=clock_skew_in_seconds,
+ )
+
+ if idinfo["iss"] not in sync_id_token._GOOGLE_ISSUERS:
+ raise exceptions.GoogleAuthError(
+ "Wrong issuer. 'iss' should be one of the following: {}".format(
+ sync_id_token._GOOGLE_ISSUERS
+ )
+ )
+
+ return idinfo
+
+
+async def verify_firebase_token(
+ id_token, request, audience=None, clock_skew_in_seconds=0
+):
+ """Verifies an ID Token issued by Firebase Authentication.
+
+ Args:
+ id_token (Union[str, bytes]): The encoded token.
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests. This must be an aiohttp request.
+ audience (str): The audience that this token is intended for. This is
+ typically your Firebase application ID. If None then the audience
+ is not verified.
+ clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+ validation.
+
+ Returns:
+ Mapping[str, Any]: The decoded token.
+ """
+ return await verify_token(
+ id_token,
+ request,
+ audience=audience,
+ certs_url=sync_id_token._GOOGLE_APIS_CERTS_URL,
+ clock_skew_in_seconds=clock_skew_in_seconds,
+ )
+
+
+async def fetch_id_token(request, audience):
+ """Fetch the ID Token from the current environment.
+
+ This function acquires ID token from the environment in the following order.
+ See https://google.aip.dev/auth/4110.
+
+ 1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+ to the path of a valid service account JSON file, then ID token is
+ acquired using this service account credentials.
+ 2. If the application is running in Compute Engine, App Engine or Cloud Run,
+ then the ID token are obtained from the metadata server.
+ 3. If metadata server doesn't exist and no valid service account credentials
+ are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
+ be raised.
+
+ Example::
+
+ import google.oauth2._id_token_async
+ import google.auth.transport.aiohttp_requests
+
+ request = google.auth.transport.aiohttp_requests.Request()
+ target_audience = "https://pubsub.googleapis.com"
+
+ id_token = await google.oauth2._id_token_async.fetch_id_token(request, target_audience)
+
+ Args:
+ request (google.auth.transport.aiohttp_requests.Request): A callable used to make
+ HTTP requests.
+ audience (str): The audience that this ID token is intended for.
+
+ Returns:
+ str: The ID token.
+
+ Raises:
+ ~google.auth.exceptions.DefaultCredentialsError:
+ If metadata server doesn't exist and no valid service account
+ credentials are found.
+ """
+ # 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
+ # variable.
+ credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
+ if credentials_filename:
+ if not (
+ os.path.exists(credentials_filename)
+ and os.path.isfile(credentials_filename)
+ ):
+ raise exceptions.DefaultCredentialsError(
+ "GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
+ )
+
+ try:
+ with open(credentials_filename, "r") as f:
+ from google.oauth2 import _service_account_async as service_account
+
+ info = json.load(f)
+ if info.get("type") == "service_account":
+ credentials = service_account.IDTokenCredentials.from_service_account_info(
+ info, target_audience=audience
+ )
+ await credentials.refresh(request)
+ return credentials.token
+ except ValueError as caught_exc:
+ new_exc = exceptions.DefaultCredentialsError(
+ "GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
+ caught_exc,
+ )
+ raise new_exc from caught_exc
+
+ # 2. Try to fetch ID token from metada server if it exists. The code works
+ # for GAE and Cloud Run metadata server as well.
+ try:
+ from google.auth import compute_engine
+ from google.auth.compute_engine import _metadata
+
+ request_new = requests.Request()
+ if _metadata.ping(request_new):
+ credentials = compute_engine.IDTokenCredentials(
+ request_new, audience, use_metadata_identity_endpoint=True
+ )
+ credentials.refresh(request_new)
+ return credentials.token
+ except (ImportError, exceptions.TransportError):
+ pass
+
+ raise exceptions.DefaultCredentialsError(
+ "Neither metadata server or valid service account credentials are found."
+ )
diff --git a/contrib/python/google-auth/py3/google/oauth2/_reauth_async.py b/contrib/python/google-auth/py3/google/oauth2/_reauth_async.py
new file mode 100644
index 0000000000..de3675c523
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/_reauth_async.py
@@ -0,0 +1,328 @@
+# Copyright 2021 Google LLC
+#
+# 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 module that provides functions for handling rapt authentication.
+
+Reauth is a process of obtaining additional authentication (such as password,
+security token, etc.) while refreshing OAuth 2.0 credentials for a user.
+
+Credentials that use the Reauth flow must have the reauth scope,
+``https://www.googleapis.com/auth/accounts.reauth``.
+
+This module provides a high-level function for executing the Reauth process,
+:func:`refresh_grant`, and lower-level helpers for doing the individual
+steps of the reauth process.
+
+Those steps are:
+
+1. Obtaining a list of challenges from the reauth server.
+2. Running through each challenge and sending the result back to the reauth
+ server.
+3. Refreshing the access token using the returned rapt token.
+"""
+
+import sys
+
+from google.auth import exceptions
+from google.oauth2 import _client
+from google.oauth2 import _client_async
+from google.oauth2 import challenges
+from google.oauth2 import reauth
+
+
+async def _get_challenges(
+ request, supported_challenge_types, access_token, requested_scopes=None
+):
+ """Does initial request to reauth API to get the challenges.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests. This must be an aiohttp request.
+ supported_challenge_types (Sequence[str]): list of challenge names
+ supported by the manager.
+ access_token (str): Access token with reauth scopes.
+ requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.
+
+ Returns:
+ dict: The response from the reauth API.
+ """
+ body = {"supportedChallengeTypes": supported_challenge_types}
+ if requested_scopes:
+ body["oauthScopesForDomainPolicyLookup"] = requested_scopes
+
+ return await _client_async._token_endpoint_request(
+ request,
+ reauth._REAUTH_API + ":start",
+ body,
+ access_token=access_token,
+ use_json=True,
+ )
+
+
+async def _send_challenge_result(
+ request, session_id, challenge_id, client_input, access_token
+):
+ """Attempt to refresh access token by sending next challenge result.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests. This must be an aiohttp request.
+ session_id (str): session id returned by the initial reauth call.
+ challenge_id (str): challenge id returned by the initial reauth call.
+ client_input: dict with a challenge-specific client input. For example:
+ ``{'credential': password}`` for password challenge.
+ access_token (str): Access token with reauth scopes.
+
+ Returns:
+ dict: The response from the reauth API.
+ """
+ body = {
+ "sessionId": session_id,
+ "challengeId": challenge_id,
+ "action": "RESPOND",
+ "proposalResponse": client_input,
+ }
+
+ return await _client_async._token_endpoint_request(
+ request,
+ reauth._REAUTH_API + "/{}:continue".format(session_id),
+ body,
+ access_token=access_token,
+ use_json=True,
+ )
+
+
+async def _run_next_challenge(msg, request, access_token):
+ """Get the next challenge from msg and run it.
+
+ Args:
+ msg (dict): Reauth API response body (either from the initial request to
+ https://reauth.googleapis.com/v2/sessions:start or from sending the
+ previous challenge response to
+ https://reauth.googleapis.com/v2/sessions/id:continue)
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests. This must be an aiohttp request.
+ access_token (str): reauth access token
+
+ Returns:
+ dict: The response from the reauth API.
+
+ Raises:
+ google.auth.exceptions.ReauthError: if reauth failed.
+ """
+ for challenge in msg["challenges"]:
+ if challenge["status"] != "READY":
+ # Skip non-activated challenges.
+ continue
+ c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)
+ if not c:
+ raise exceptions.ReauthFailError(
+ "Unsupported challenge type {0}. Supported types: {1}".format(
+ challenge["challengeType"],
+ ",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),
+ )
+ )
+ if not c.is_locally_eligible:
+ raise exceptions.ReauthFailError(
+ "Challenge {0} is not locally eligible".format(
+ challenge["challengeType"]
+ )
+ )
+ client_input = c.obtain_challenge_input(challenge)
+ if not client_input:
+ return None
+ return await _send_challenge_result(
+ request,
+ msg["sessionId"],
+ challenge["challengeId"],
+ client_input,
+ access_token,
+ )
+ return None
+
+
+async def _obtain_rapt(request, access_token, requested_scopes):
+ """Given an http request method and reauth access token, get rapt token.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests. This must be an aiohttp request.
+ access_token (str): reauth access token
+ requested_scopes (Sequence[str]): scopes required by the client application
+
+ Returns:
+ str: The rapt token.
+
+ Raises:
+ google.auth.exceptions.ReauthError: if reauth failed
+ """
+ msg = await _get_challenges(
+ request,
+ list(challenges.AVAILABLE_CHALLENGES.keys()),
+ access_token,
+ requested_scopes,
+ )
+
+ if msg["status"] == reauth._AUTHENTICATED:
+ return msg["encodedProofOfReauthToken"]
+
+ for _ in range(0, reauth.RUN_CHALLENGE_RETRY_LIMIT):
+ if not (
+ msg["status"] == reauth._CHALLENGE_REQUIRED
+ or msg["status"] == reauth._CHALLENGE_PENDING
+ ):
+ raise exceptions.ReauthFailError(
+ "Reauthentication challenge failed due to API error: {}".format(
+ msg["status"]
+ )
+ )
+
+ if not reauth.is_interactive():
+ raise exceptions.ReauthFailError(
+ "Reauthentication challenge could not be answered because you are not"
+ " in an interactive session."
+ )
+
+ msg = await _run_next_challenge(msg, request, access_token)
+
+ if msg["status"] == reauth._AUTHENTICATED:
+ return msg["encodedProofOfReauthToken"]
+
+ # If we got here it means we didn't get authenticated.
+ raise exceptions.ReauthFailError("Failed to obtain rapt token.")
+
+
+async def get_rapt_token(
+ request, client_id, client_secret, refresh_token, token_uri, scopes=None
+):
+ """Given an http request method and refresh_token, get rapt token.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests. This must be an aiohttp request.
+ client_id (str): client id to get access token for reauth scope.
+ client_secret (str): client secret for the client_id
+ refresh_token (str): refresh token to refresh access token
+ token_uri (str): uri to refresh access token
+ scopes (Optional(Sequence[str])): scopes required by the client application
+
+ Returns:
+ str: The rapt token.
+ Raises:
+ google.auth.exceptions.RefreshError: If reauth failed.
+ """
+ sys.stderr.write("Reauthentication required.\n")
+
+ # Get access token for reauth.
+ access_token, _, _, _ = await _client_async.refresh_grant(
+ request=request,
+ client_id=client_id,
+ client_secret=client_secret,
+ refresh_token=refresh_token,
+ token_uri=token_uri,
+ scopes=[reauth._REAUTH_SCOPE],
+ )
+
+ # Get rapt token from reauth API.
+ rapt_token = await _obtain_rapt(request, access_token, requested_scopes=scopes)
+
+ return rapt_token
+
+
+async def refresh_grant(
+ request,
+ token_uri,
+ refresh_token,
+ client_id,
+ client_secret,
+ scopes=None,
+ rapt_token=None,
+ enable_reauth_refresh=False,
+):
+ """Implements the reauthentication flow.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests. This must be an aiohttp request.
+ token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+ URI.
+ refresh_token (str): The refresh token to use to get a new access
+ token.
+ client_id (str): The OAuth 2.0 application's client ID.
+ client_secret (str): The Oauth 2.0 appliaction's client secret.
+ scopes (Optional(Sequence[str])): Scopes to request. If present, all
+ scopes must be authorized for the refresh token. Useful if refresh
+ token has a wild card scope (e.g.
+ 'https://www.googleapis.com/auth/any-api').
+ rapt_token (Optional(str)): The rapt token for reauth.
+ enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
+ should be used. The default value is False. This option is for
+ gcloud only, other users should use the default value.
+
+ Returns:
+ Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
+ access token, new refresh token, expiration, the additional data
+ returned by the token endpoint, and the rapt token.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+ """
+ body = {
+ "grant_type": _client._REFRESH_GRANT_TYPE,
+ "client_id": client_id,
+ "client_secret": client_secret,
+ "refresh_token": refresh_token,
+ }
+ if scopes:
+ body["scope"] = " ".join(scopes)
+ if rapt_token:
+ body["rapt"] = rapt_token
+
+ response_status_ok, response_data, retryable_error = await _client_async._token_endpoint_request_no_throw(
+ request, token_uri, body
+ )
+ if (
+ not response_status_ok
+ and response_data.get("error") == reauth._REAUTH_NEEDED_ERROR
+ and (
+ response_data.get("error_subtype")
+ == reauth._REAUTH_NEEDED_ERROR_INVALID_RAPT
+ or response_data.get("error_subtype")
+ == reauth._REAUTH_NEEDED_ERROR_RAPT_REQUIRED
+ )
+ ):
+ if not enable_reauth_refresh:
+ raise exceptions.RefreshError(
+ "Reauthentication is needed. Please run `gcloud auth application-default login` to reauthenticate."
+ )
+
+ rapt_token = await get_rapt_token(
+ request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
+ )
+ body["rapt"] = rapt_token
+ (
+ response_status_ok,
+ response_data,
+ retryable_error,
+ ) = await _client_async._token_endpoint_request_no_throw(
+ request, token_uri, body
+ )
+
+ if not response_status_ok:
+ _client._handle_error_response(response_data, retryable_error)
+ refresh_response = _client._handle_refresh_grant_response(
+ response_data, refresh_token
+ )
+ return refresh_response + (rapt_token,)
diff --git a/contrib/python/google-auth/py3/google/oauth2/_service_account_async.py b/contrib/python/google-auth/py3/google/oauth2/_service_account_async.py
new file mode 100644
index 0000000000..cfd315a7ff
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/_service_account_async.py
@@ -0,0 +1,132 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+
+"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
+
+NOTE: This file adds asynchronous refresh methods to both credentials
+classes, and therefore async/await syntax is required when calling this
+method when using service account credentials with asynchronous functionality.
+Otherwise, all other methods are inherited from the regular service account
+credentials file google.oauth2.service_account
+
+"""
+
+from google.auth import _credentials_async as credentials_async
+from google.auth import _helpers
+from google.oauth2 import _client_async
+from google.oauth2 import service_account
+
+
+class Credentials(
+ service_account.Credentials, credentials_async.Scoped, credentials_async.Credentials
+):
+ """Service account credentials
+
+ Usually, you'll create these credentials with one of the helper
+ constructors. To create credentials using a Google service account
+ private key JSON file::
+
+ credentials = _service_account_async.Credentials.from_service_account_file(
+ 'service-account.json')
+
+ Or if you already have the service account file loaded::
+
+ service_account_info = json.load(open('service_account.json'))
+ credentials = _service_account_async.Credentials.from_service_account_info(
+ service_account_info)
+
+ Both helper methods pass on arguments to the constructor, so you can
+ specify additional scopes and a subject if necessary::
+
+ credentials = _service_account_async.Credentials.from_service_account_file(
+ 'service-account.json',
+ scopes=['email'],
+ subject='user@example.com')
+
+ The credentials are considered immutable. If you want to modify the scopes
+ or the subject used for delegation, use :meth:`with_scopes` or
+ :meth:`with_subject`::
+
+ scoped_credentials = credentials.with_scopes(['email'])
+ delegated_credentials = credentials.with_subject(subject)
+
+ To add a quota project, use :meth:`with_quota_project`::
+
+ credentials = credentials.with_quota_project('myproject-123')
+ """
+
+ @_helpers.copy_docstring(credentials_async.Credentials)
+ async def refresh(self, request):
+ assertion = self._make_authorization_grant_assertion()
+ access_token, expiry, _ = await _client_async.jwt_grant(
+ request, self._token_uri, assertion
+ )
+ self.token = access_token
+ self.expiry = expiry
+
+
+class IDTokenCredentials(
+ service_account.IDTokenCredentials,
+ credentials_async.Signing,
+ credentials_async.Credentials,
+):
+ """Open ID Connect ID Token-based service account credentials.
+
+ These credentials are largely similar to :class:`.Credentials`, but instead
+ of using an OAuth 2.0 Access Token as the bearer token, they use an Open
+ ID Connect ID Token as the bearer token. These credentials are useful when
+ communicating to services that require ID Tokens and can not accept access
+ tokens.
+
+ Usually, you'll create these credentials with one of the helper
+ constructors. To create credentials using a Google service account
+ private key JSON file::
+
+ credentials = (
+ _service_account_async.IDTokenCredentials.from_service_account_file(
+ 'service-account.json'))
+
+ Or if you already have the service account file loaded::
+
+ service_account_info = json.load(open('service_account.json'))
+ credentials = (
+ _service_account_async.IDTokenCredentials.from_service_account_info(
+ service_account_info))
+
+ Both helper methods pass on arguments to the constructor, so you can
+ specify additional scopes and a subject if necessary::
+
+ credentials = (
+ _service_account_async.IDTokenCredentials.from_service_account_file(
+ 'service-account.json',
+ scopes=['email'],
+ subject='user@example.com'))
+
+ The credentials are considered immutable. If you want to modify the scopes
+ or the subject used for delegation, use :meth:`with_scopes` or
+ :meth:`with_subject`::
+
+ scoped_credentials = credentials.with_scopes(['email'])
+ delegated_credentials = credentials.with_subject(subject)
+
+ """
+
+ @_helpers.copy_docstring(credentials_async.Credentials)
+ async def refresh(self, request):
+ assertion = self._make_authorization_grant_assertion()
+ access_token, expiry, _ = await _client_async.id_token_jwt_grant(
+ request, self._token_uri, assertion
+ )
+ self.token = access_token
+ self.expiry = expiry
diff --git a/contrib/python/google-auth/py3/google/oauth2/challenges.py b/contrib/python/google-auth/py3/google/oauth2/challenges.py
new file mode 100644
index 0000000000..c55796323b
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/challenges.py
@@ -0,0 +1,203 @@
+# Copyright 2021 Google LLC
+#
+# 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.
+
+""" Challenges for reauthentication.
+"""
+
+import abc
+import base64
+import getpass
+import sys
+
+from google.auth import _helpers
+from google.auth import exceptions
+
+
+REAUTH_ORIGIN = "https://accounts.google.com"
+SAML_CHALLENGE_MESSAGE = (
+ "Please run `gcloud auth login` to complete reauthentication with SAML."
+)
+
+
+def get_user_password(text):
+ """Get password from user.
+
+ Override this function with a different logic if you are using this library
+ outside a CLI.
+
+ Args:
+ text (str): message for the password prompt.
+
+ Returns:
+ str: password string.
+ """
+ return getpass.getpass(text)
+
+
+class ReauthChallenge(metaclass=abc.ABCMeta):
+ """Base class for reauth challenges."""
+
+ @property
+ @abc.abstractmethod
+ def name(self): # pragma: NO COVER
+ """Returns the name of the challenge."""
+ raise NotImplementedError("name property must be implemented")
+
+ @property
+ @abc.abstractmethod
+ def is_locally_eligible(self): # pragma: NO COVER
+ """Returns true if a challenge is supported locally on this machine."""
+ raise NotImplementedError("is_locally_eligible property must be implemented")
+
+ @abc.abstractmethod
+ def obtain_challenge_input(self, metadata): # pragma: NO COVER
+ """Performs logic required to obtain credentials and returns it.
+
+ Args:
+ metadata (Mapping): challenge metadata returned in the 'challenges' field in
+ the initial reauth request. Includes the 'challengeType' field
+ and other challenge-specific fields.
+
+ Returns:
+ response that will be send to the reauth service as the content of
+ the 'proposalResponse' field in the request body. Usually a dict
+ with the keys specific to the challenge. For example,
+ ``{'credential': password}`` for password challenge.
+ """
+ raise NotImplementedError("obtain_challenge_input method must be implemented")
+
+
+class PasswordChallenge(ReauthChallenge):
+ """Challenge that asks for user's password."""
+
+ @property
+ def name(self):
+ return "PASSWORD"
+
+ @property
+ def is_locally_eligible(self):
+ return True
+
+ @_helpers.copy_docstring(ReauthChallenge)
+ def obtain_challenge_input(self, unused_metadata):
+ passwd = get_user_password("Please enter your password:")
+ if not passwd:
+ passwd = " " # avoid the server crashing in case of no password :D
+ return {"credential": passwd}
+
+
+class SecurityKeyChallenge(ReauthChallenge):
+ """Challenge that asks for user's security key touch."""
+
+ @property
+ def name(self):
+ return "SECURITY_KEY"
+
+ @property
+ def is_locally_eligible(self):
+ return True
+
+ @_helpers.copy_docstring(ReauthChallenge)
+ def obtain_challenge_input(self, metadata):
+ try:
+ import pyu2f.convenience.authenticator # type: ignore
+ import pyu2f.errors # type: ignore
+ import pyu2f.model # type: ignore
+ except ImportError:
+ raise exceptions.ReauthFailError(
+ "pyu2f dependency is required to use Security key reauth feature. "
+ "It can be installed via `pip install pyu2f` or `pip install google-auth[reauth]`."
+ )
+ sk = metadata["securityKey"]
+ challenges = sk["challenges"]
+ # Read both 'applicationId' and 'relyingPartyId', if they are the same, use
+ # applicationId, if they are different, use relyingPartyId first and retry
+ # with applicationId
+ application_id = sk["applicationId"]
+ relying_party_id = sk["relyingPartyId"]
+
+ if application_id != relying_party_id:
+ application_parameters = [relying_party_id, application_id]
+ else:
+ application_parameters = [application_id]
+
+ challenge_data = []
+ for c in challenges:
+ kh = c["keyHandle"].encode("ascii")
+ key = pyu2f.model.RegisteredKey(bytearray(base64.urlsafe_b64decode(kh)))
+ challenge = c["challenge"].encode("ascii")
+ challenge = base64.urlsafe_b64decode(challenge)
+ challenge_data.append({"key": key, "challenge": challenge})
+
+ # Track number of tries to suppress error message until all application_parameters
+ # are tried.
+ tries = 0
+ for app_id in application_parameters:
+ try:
+ tries += 1
+ api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator(
+ REAUTH_ORIGIN
+ )
+ response = api.Authenticate(
+ app_id, challenge_data, print_callback=sys.stderr.write
+ )
+ return {"securityKey": response}
+ except pyu2f.errors.U2FError as e:
+ if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE:
+ # Only show error if all app_ids have been tried
+ if tries == len(application_parameters):
+ sys.stderr.write("Ineligible security key.\n")
+ return None
+ continue
+ if e.code == pyu2f.errors.U2FError.TIMEOUT:
+ sys.stderr.write(
+ "Timed out while waiting for security key touch.\n"
+ )
+ else:
+ raise e
+ except pyu2f.errors.PluginError as e:
+ sys.stderr.write("Plugin error: {}.\n".format(e))
+ continue
+ except pyu2f.errors.NoDeviceFoundError:
+ sys.stderr.write("No security key found.\n")
+ return None
+
+
+class SamlChallenge(ReauthChallenge):
+ """Challenge that asks the users to browse to their ID Providers.
+
+ Currently SAML challenge is not supported. When obtaining the challenge
+ input, exception will be raised to instruct the users to run
+ `gcloud auth login` for reauthentication.
+ """
+
+ @property
+ def name(self):
+ return "SAML"
+
+ @property
+ def is_locally_eligible(self):
+ return True
+
+ def obtain_challenge_input(self, metadata):
+ # Magic Arch has not fully supported returning a proper dedirect URL
+ # for programmatic SAML users today. So we error our here and request
+ # users to use gcloud to complete a login.
+ raise exceptions.ReauthSamlChallengeFailError(SAML_CHALLENGE_MESSAGE)
+
+
+AVAILABLE_CHALLENGES = {
+ challenge.name: challenge
+ for challenge in [SecurityKeyChallenge(), PasswordChallenge(), SamlChallenge()]
+}
diff --git a/contrib/python/google-auth/py3/google/oauth2/credentials.py b/contrib/python/google-auth/py3/google/oauth2/credentials.py
new file mode 100644
index 0000000000..4643fdbea6
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/credentials.py
@@ -0,0 +1,545 @@
+# Copyright 2016 Google LLC
+#
+# 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 Credentials.
+
+This module provides credentials based on OAuth 2.0 access and refresh tokens.
+These credentials usually access resources on behalf of a user (resource
+owner).
+
+Specifically, this is intended to use access tokens acquired using the
+`Authorization Code grant`_ and can refresh those tokens using a
+optional `refresh token`_.
+
+Obtaining the initial access and refresh token is outside of the scope of this
+module. Consult `rfc6749 section 4.1`_ for complete details on the
+Authorization Code grant flow.
+
+.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1
+.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6
+.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
+"""
+
+from datetime import datetime
+import io
+import json
+import logging
+import warnings
+
+from google.auth import _cloud_sdk
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.auth import metrics
+from google.oauth2 import reauth
+
+_LOGGER = logging.getLogger(__name__)
+
+
+# The Google OAuth 2.0 token endpoint. Used for authorized user credentials.
+_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
+
+
+class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject):
+ """Credentials using OAuth 2.0 access and refresh tokens.
+
+ The credentials are considered immutable. If you want to modify the
+ quota project, use :meth:`with_quota_project` or ::
+
+ credentials = credentials.with_quota_project('myproject-123')
+
+ Reauth is disabled by default. To enable reauth, set the
+ `enable_reauth_refresh` parameter to True in the constructor. Note that
+ reauth feature is intended for gcloud to use only.
+ If reauth is enabled, `pyu2f` dependency has to be installed in order to use security
+ key reauth feature. Dependency can be installed via `pip install pyu2f` or `pip install
+ google-auth[reauth]`.
+ """
+
+ def __init__(
+ self,
+ token,
+ refresh_token=None,
+ id_token=None,
+ token_uri=None,
+ client_id=None,
+ client_secret=None,
+ scopes=None,
+ default_scopes=None,
+ quota_project_id=None,
+ expiry=None,
+ rapt_token=None,
+ refresh_handler=None,
+ enable_reauth_refresh=False,
+ granted_scopes=None,
+ trust_boundary=None,
+ ):
+ """
+ Args:
+ token (Optional(str)): The OAuth 2.0 access token. Can be None
+ if refresh information is provided.
+ refresh_token (str): The OAuth 2.0 refresh token. If specified,
+ credentials can be refreshed.
+ id_token (str): The Open ID Connect ID Token.
+ token_uri (str): The OAuth 2.0 authorization server's token
+ endpoint URI. Must be specified for refresh, can be left as
+ None if the token can not be refreshed.
+ client_id (str): The OAuth 2.0 client ID. Must be specified for
+ refresh, can be left as None if the token can not be refreshed.
+ client_secret(str): The OAuth 2.0 client secret. Must be specified
+ for refresh, can be left as None if the token can not be
+ refreshed.
+ scopes (Sequence[str]): The scopes used to obtain authorization.
+ This parameter is used by :meth:`has_scopes`. OAuth 2.0
+ credentials can not request additional scopes after
+ authorization. The scopes must be derivable from the refresh
+ token if refresh information is provided (e.g. The refresh
+ token scopes are a superset of this or contain a wild card
+ scope like 'https://www.googleapis.com/auth/any-api').
+ default_scopes (Sequence[str]): Default scopes passed by a
+ Google client library. Use 'scopes' for user-defined scopes.
+ quota_project_id (Optional[str]): The project ID used for quota and billing.
+ This project may be different from the project used to
+ create the credentials.
+ rapt_token (Optional[str]): The reauth Proof Token.
+ refresh_handler (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):
+ A callable which takes in the HTTP request callable and the list of
+ OAuth scopes and when called returns an access token string for the
+ requested scopes and its expiry datetime. This is useful when no
+ refresh tokens are provided and tokens are obtained by calling
+ some external process on demand. It is particularly useful for
+ retrieving downscoped tokens from a token broker.
+ enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
+ should be used. This flag is for gcloud to use only.
+ granted_scopes (Optional[Sequence[str]]): The scopes that were consented/granted by the user.
+ This could be different from the requested scopes and it could be empty if granted
+ and requested scopes were same.
+ """
+ super(Credentials, self).__init__()
+ self.token = token
+ self.expiry = expiry
+ self._refresh_token = refresh_token
+ self._id_token = id_token
+ self._scopes = scopes
+ self._default_scopes = default_scopes
+ self._granted_scopes = granted_scopes
+ self._token_uri = token_uri
+ self._client_id = client_id
+ self._client_secret = client_secret
+ self._quota_project_id = quota_project_id
+ self._rapt_token = rapt_token
+ self.refresh_handler = refresh_handler
+ self._enable_reauth_refresh = enable_reauth_refresh
+ self._trust_boundary = trust_boundary
+
+ def __getstate__(self):
+ """A __getstate__ method must exist for the __setstate__ to be called
+ This is identical to the default implementation.
+ See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
+ """
+ state_dict = self.__dict__.copy()
+ # Remove _refresh_handler function as there are limitations pickling and
+ # unpickling certain callables (lambda, functools.partial instances)
+ # because they need to be importable.
+ # Instead, the refresh_handler setter should be used to repopulate this.
+ del state_dict["_refresh_handler"]
+ return state_dict
+
+ def __setstate__(self, d):
+ """Credentials pickled with older versions of the class do not have
+ all the attributes."""
+ self.token = d.get("token")
+ self.expiry = d.get("expiry")
+ self._refresh_token = d.get("_refresh_token")
+ self._id_token = d.get("_id_token")
+ self._scopes = d.get("_scopes")
+ self._default_scopes = d.get("_default_scopes")
+ self._granted_scopes = d.get("_granted_scopes")
+ self._token_uri = d.get("_token_uri")
+ self._client_id = d.get("_client_id")
+ self._client_secret = d.get("_client_secret")
+ self._quota_project_id = d.get("_quota_project_id")
+ self._rapt_token = d.get("_rapt_token")
+ self._enable_reauth_refresh = d.get("_enable_reauth_refresh")
+ self._trust_boundary = d.get("_trust_boundary")
+ self._universe_domain = d.get("_universe_domain")
+ # The refresh_handler setter should be used to repopulate this.
+ self._refresh_handler = None
+
+ @property
+ def refresh_token(self):
+ """Optional[str]: The OAuth 2.0 refresh token."""
+ return self._refresh_token
+
+ @property
+ def scopes(self):
+ """Optional[str]: The OAuth 2.0 permission scopes."""
+ return self._scopes
+
+ @property
+ def granted_scopes(self):
+ """Optional[Sequence[str]]: The OAuth 2.0 permission scopes that were granted by the user."""
+ return self._granted_scopes
+
+ @property
+ def token_uri(self):
+ """Optional[str]: The OAuth 2.0 authorization server's token endpoint
+ URI."""
+ return self._token_uri
+
+ @property
+ def id_token(self):
+ """Optional[str]: The Open ID Connect ID Token.
+
+ Depending on the authorization server and the scopes requested, this
+ may be populated when credentials are obtained and updated when
+ :meth:`refresh` is called. This token is a JWT. It can be verified
+ and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
+ """
+ return self._id_token
+
+ @property
+ def client_id(self):
+ """Optional[str]: The OAuth 2.0 client ID."""
+ return self._client_id
+
+ @property
+ def client_secret(self):
+ """Optional[str]: The OAuth 2.0 client secret."""
+ return self._client_secret
+
+ @property
+ def requires_scopes(self):
+ """False: OAuth 2.0 credentials have their scopes set when
+ the initial token is requested and can not be changed."""
+ return False
+
+ @property
+ def rapt_token(self):
+ """Optional[str]: The reauth Proof Token."""
+ return self._rapt_token
+
+ @property
+ def refresh_handler(self):
+ """Returns the refresh handler if available.
+
+ Returns:
+ Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]:
+ The current refresh handler.
+ """
+ return self._refresh_handler
+
+ @refresh_handler.setter
+ def refresh_handler(self, value):
+ """Updates the current refresh handler.
+
+ Args:
+ value (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):
+ The updated value of the refresh handler.
+
+ Raises:
+ TypeError: If the value is not a callable or None.
+ """
+ if not callable(value) and value is not None:
+ raise TypeError("The provided refresh_handler is not a callable or None.")
+ self._refresh_handler = value
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+
+ return self.__class__(
+ self.token,
+ refresh_token=self.refresh_token,
+ id_token=self.id_token,
+ token_uri=self.token_uri,
+ client_id=self.client_id,
+ client_secret=self.client_secret,
+ scopes=self.scopes,
+ default_scopes=self.default_scopes,
+ granted_scopes=self.granted_scopes,
+ quota_project_id=quota_project_id,
+ rapt_token=self.rapt_token,
+ enable_reauth_refresh=self._enable_reauth_refresh,
+ trust_boundary=self._trust_boundary,
+ )
+
+ @_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
+ def with_token_uri(self, token_uri):
+
+ return self.__class__(
+ self.token,
+ refresh_token=self.refresh_token,
+ id_token=self.id_token,
+ token_uri=token_uri,
+ client_id=self.client_id,
+ client_secret=self.client_secret,
+ scopes=self.scopes,
+ default_scopes=self.default_scopes,
+ granted_scopes=self.granted_scopes,
+ quota_project_id=self.quota_project_id,
+ rapt_token=self.rapt_token,
+ enable_reauth_refresh=self._enable_reauth_refresh,
+ trust_boundary=self._trust_boundary,
+ )
+
+ def _metric_header_for_usage(self):
+ return metrics.CRED_TYPE_USER
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def refresh(self, request):
+ scopes = self._scopes if self._scopes is not None else self._default_scopes
+ # Use refresh handler if available and no refresh token is
+ # available. This is useful in general when tokens are obtained by calling
+ # some external process on demand. It is particularly useful for retrieving
+ # downscoped tokens from a token broker.
+ if self._refresh_token is None and self.refresh_handler:
+ token, expiry = self.refresh_handler(request, scopes=scopes)
+ # Validate returned data.
+ if not isinstance(token, str):
+ raise exceptions.RefreshError(
+ "The refresh_handler returned token is not a string."
+ )
+ if not isinstance(expiry, datetime):
+ raise exceptions.RefreshError(
+ "The refresh_handler returned expiry is not a datetime object."
+ )
+ if _helpers.utcnow() >= expiry - _helpers.REFRESH_THRESHOLD:
+ raise exceptions.RefreshError(
+ "The credentials returned by the refresh_handler are "
+ "already expired."
+ )
+ self.token = token
+ self.expiry = expiry
+ return
+
+ if (
+ self._refresh_token is None
+ or self._token_uri is None
+ or self._client_id is None
+ or self._client_secret is None
+ ):
+ raise exceptions.RefreshError(
+ "The credentials do not contain the necessary fields need to "
+ "refresh the access token. You must specify refresh_token, "
+ "token_uri, client_id, and client_secret."
+ )
+
+ (
+ access_token,
+ refresh_token,
+ expiry,
+ grant_response,
+ rapt_token,
+ ) = reauth.refresh_grant(
+ request,
+ self._token_uri,
+ self._refresh_token,
+ self._client_id,
+ self._client_secret,
+ scopes=scopes,
+ rapt_token=self._rapt_token,
+ enable_reauth_refresh=self._enable_reauth_refresh,
+ )
+
+ self.token = access_token
+ self.expiry = expiry
+ self._refresh_token = refresh_token
+ self._id_token = grant_response.get("id_token")
+ self._rapt_token = rapt_token
+
+ if scopes and "scope" in grant_response:
+ requested_scopes = frozenset(scopes)
+ self._granted_scopes = grant_response["scope"].split()
+ granted_scopes = frozenset(self._granted_scopes)
+ scopes_requested_but_not_granted = requested_scopes - granted_scopes
+ if scopes_requested_but_not_granted:
+ # User might be presented with unbundled scopes at the time of
+ # consent. So it is a valid scenario to not have all the requested
+ # scopes as part of granted scopes but log a warning in case the
+ # developer wants to debug the scenario.
+ _LOGGER.warning(
+ "Not all requested scopes were granted by the "
+ "authorization server, missing scopes {}.".format(
+ ", ".join(scopes_requested_but_not_granted)
+ )
+ )
+
+ @classmethod
+ def from_authorized_user_info(cls, info, scopes=None):
+ """Creates a Credentials instance from parsed authorized user info.
+
+ Args:
+ info (Mapping[str, str]): The authorized user info in Google
+ format.
+ scopes (Sequence[str]): Optional list of scopes to include in the
+ credentials.
+
+ Returns:
+ google.oauth2.credentials.Credentials: The constructed
+ credentials.
+
+ Raises:
+ ValueError: If the info is not in the expected format.
+ """
+ keys_needed = set(("refresh_token", "client_id", "client_secret"))
+ missing = keys_needed.difference(info.keys())
+
+ if missing:
+ raise ValueError(
+ "Authorized user info was not in the expected format, missing "
+ "fields {}.".format(", ".join(missing))
+ )
+
+ # access token expiry (datetime obj); auto-expire if not saved
+ expiry = info.get("expiry")
+ if expiry:
+ expiry = datetime.strptime(
+ expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
+ )
+ else:
+ expiry = _helpers.utcnow() - _helpers.REFRESH_THRESHOLD
+
+ # process scopes, which needs to be a seq
+ if scopes is None and "scopes" in info:
+ scopes = info.get("scopes")
+ if isinstance(scopes, str):
+ scopes = scopes.split(" ")
+
+ return cls(
+ token=info.get("token"),
+ refresh_token=info.get("refresh_token"),
+ token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides
+ scopes=scopes,
+ client_id=info.get("client_id"),
+ client_secret=info.get("client_secret"),
+ quota_project_id=info.get("quota_project_id"), # may not exist
+ expiry=expiry,
+ rapt_token=info.get("rapt_token"), # may not exist
+ trust_boundary=info.get("trust_boundary"), # may not exist
+ )
+
+ @classmethod
+ def from_authorized_user_file(cls, filename, scopes=None):
+ """Creates a Credentials instance from an authorized user json file.
+
+ Args:
+ filename (str): The path to the authorized user json file.
+ scopes (Sequence[str]): Optional list of scopes to include in the
+ credentials.
+
+ Returns:
+ google.oauth2.credentials.Credentials: The constructed
+ credentials.
+
+ Raises:
+ ValueError: If the file is not in the expected format.
+ """
+ with io.open(filename, "r", encoding="utf-8") as json_file:
+ data = json.load(json_file)
+ return cls.from_authorized_user_info(data, scopes)
+
+ def to_json(self, strip=None):
+ """Utility function that creates a JSON representation of a Credentials
+ object.
+
+ Args:
+ strip (Sequence[str]): Optional list of members to exclude from the
+ generated JSON.
+
+ Returns:
+ str: A JSON representation of this instance. When converted into
+ a dictionary, it can be passed to from_authorized_user_info()
+ to create a new credential instance.
+ """
+ prep = {
+ "token": self.token,
+ "refresh_token": self.refresh_token,
+ "token_uri": self.token_uri,
+ "client_id": self.client_id,
+ "client_secret": self.client_secret,
+ "scopes": self.scopes,
+ "rapt_token": self.rapt_token,
+ }
+ if self.expiry: # flatten expiry timestamp
+ prep["expiry"] = self.expiry.isoformat() + "Z"
+
+ # Remove empty entries (those which are None)
+ prep = {k: v for k, v in prep.items() if v is not None}
+
+ # Remove entries that explicitely need to be removed
+ if strip is not None:
+ prep = {k: v for k, v in prep.items() if k not in strip}
+
+ return json.dumps(prep)
+
+
+class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):
+ """Access token credentials for user account.
+
+ Obtain the access token for a given user account or the current active
+ user account with the ``gcloud auth print-access-token`` command.
+
+ Args:
+ account (Optional[str]): Account to get the access token for. If not
+ specified, the current active account will be used.
+ quota_project_id (Optional[str]): The project ID used for quota
+ and billing.
+ """
+
+ def __init__(self, account=None, quota_project_id=None):
+ warnings.warn(
+ "UserAccessTokenCredentials is deprecated, please use "
+ "google.oauth2.credentials.Credentials instead. To use "
+ "that credential type, simply run "
+ "`gcloud auth application-default login` and let the "
+ "client libraries pick up the application default credentials."
+ )
+ super(UserAccessTokenCredentials, self).__init__()
+ self._account = account
+ self._quota_project_id = quota_project_id
+
+ def with_account(self, account):
+ """Create a new instance with the given account.
+
+ Args:
+ account (str): Account to get the access token for.
+
+ Returns:
+ google.oauth2.credentials.UserAccessTokenCredentials: The created
+ credentials with the given account.
+ """
+ return self.__class__(account=account, quota_project_id=self._quota_project_id)
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ return self.__class__(account=self._account, quota_project_id=quota_project_id)
+
+ def refresh(self, request):
+ """Refreshes the access token.
+
+ Args:
+ request (google.auth.transport.Request): This argument is required
+ by the base class interface but not used in this implementation,
+ so just set it to `None`.
+
+ Raises:
+ google.auth.exceptions.UserAccessTokenError: If the access token
+ refresh failed.
+ """
+ self.token = _cloud_sdk.get_auth_access_token(self._account)
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def before_request(self, request, method, url, headers):
+ self.refresh(request)
+ self.apply(headers)
diff --git a/contrib/python/google-auth/py3/google/oauth2/gdch_credentials.py b/contrib/python/google-auth/py3/google/oauth2/gdch_credentials.py
new file mode 100644
index 0000000000..7410cfc2e0
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/gdch_credentials.py
@@ -0,0 +1,251 @@
+# Copyright 2022 Google LLC
+#
+# 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.
+
+"""Experimental GDCH credentials support.
+"""
+
+import datetime
+
+from google.auth import _helpers
+from google.auth import _service_account_info
+from google.auth import credentials
+from google.auth import exceptions
+from google.auth import jwt
+from google.oauth2 import _client
+
+
+TOKEN_EXCHANGE_TYPE = "urn:ietf:params:oauth:token-type:token-exchange"
+ACCESS_TOKEN_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+SERVICE_ACCOUNT_TOKEN_TYPE = "urn:k8s:params:oauth:token-type:serviceaccount"
+JWT_LIFETIME = datetime.timedelta(seconds=3600) # 1 hour
+
+
+class ServiceAccountCredentials(credentials.Credentials):
+ """Credentials for GDCH (`Google Distributed Cloud Hosted`_) for service
+ account users.
+
+ .. _Google Distributed Cloud Hosted:
+ https://cloud.google.com/blog/topics/hybrid-cloud/\
+ announcing-google-distributed-cloud-edge-and-hosted
+
+ To create a GDCH service account credential, first create a JSON file of
+ the following format::
+
+ {
+ "type": "gdch_service_account",
+ "format_version": "1",
+ "project": "<project name>",
+ "private_key_id": "<key id>",
+ "private_key": "-----BEGIN EC PRIVATE KEY-----\n<key bytes>\n-----END EC PRIVATE KEY-----\n",
+ "name": "<service identity name>",
+ "ca_cert_path": "<CA cert path>",
+ "token_uri": "https://service-identity.<Domain>/authenticate"
+ }
+
+ The "format_version" field stands for the format of the JSON file. For now
+ it is always "1". The `private_key_id` and `private_key` is used for signing.
+ The `ca_cert_path` is used for token server TLS certificate verification.
+
+ After the JSON file is created, set `GOOGLE_APPLICATION_CREDENTIALS` environment
+ variable to the JSON file path, then use the following code to create the
+ credential::
+
+ import google.auth
+
+ credential, _ = google.auth.default()
+ credential = credential.with_gdch_audience("<the audience>")
+
+ We can also create the credential directly::
+
+ from google.oauth import gdch_credentials
+
+ credential = gdch_credentials.ServiceAccountCredentials.from_service_account_file("<the json file path>")
+ credential = credential.with_gdch_audience("<the audience>")
+
+ The token is obtained in the following way. This class first creates a
+ self signed JWT. It uses the `name` value as the `iss` and `sub` claim, and
+ the `token_uri` as the `aud` claim, and signs the JWT with the `private_key`.
+ It then sends the JWT to the `token_uri` to exchange a final token for
+ `audience`.
+ """
+
+ def __init__(
+ self, signer, service_identity_name, project, audience, token_uri, ca_cert_path
+ ):
+ """
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ service_identity_name (str): The service identity name. It will be
+ used as the `iss` and `sub` claim in the self signed JWT.
+ project (str): The project.
+ audience (str): The audience for the final token.
+ token_uri (str): The token server uri.
+ ca_cert_path (str): The CA cert path for token server side TLS
+ certificate verification. If the token server uses well known
+ CA, then this parameter can be `None`.
+ """
+ super(ServiceAccountCredentials, self).__init__()
+ self._signer = signer
+ self._service_identity_name = service_identity_name
+ self._project = project
+ self._audience = audience
+ self._token_uri = token_uri
+ self._ca_cert_path = ca_cert_path
+
+ def _create_jwt(self):
+ now = _helpers.utcnow()
+ expiry = now + JWT_LIFETIME
+ iss_sub_value = "system:serviceaccount:{}:{}".format(
+ self._project, self._service_identity_name
+ )
+
+ payload = {
+ "iss": iss_sub_value,
+ "sub": iss_sub_value,
+ "aud": self._token_uri,
+ "iat": _helpers.datetime_to_secs(now),
+ "exp": _helpers.datetime_to_secs(expiry),
+ }
+
+ return _helpers.from_bytes(jwt.encode(self._signer, payload))
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def refresh(self, request):
+ import google.auth.transport.requests
+
+ if not isinstance(request, google.auth.transport.requests.Request):
+ raise exceptions.RefreshError(
+ "For GDCH service account credentials, request must be a google.auth.transport.requests.Request object"
+ )
+
+ # Create a self signed JWT, and do token exchange.
+ jwt_token = self._create_jwt()
+ request_body = {
+ "grant_type": TOKEN_EXCHANGE_TYPE,
+ "audience": self._audience,
+ "requested_token_type": ACCESS_TOKEN_TOKEN_TYPE,
+ "subject_token": jwt_token,
+ "subject_token_type": SERVICE_ACCOUNT_TOKEN_TYPE,
+ }
+ response_data = _client._token_endpoint_request(
+ request,
+ self._token_uri,
+ request_body,
+ access_token=None,
+ use_json=True,
+ verify=self._ca_cert_path,
+ )
+
+ self.token, _, self.expiry, _ = _client._handle_refresh_grant_response(
+ response_data, None
+ )
+
+ def with_gdch_audience(self, audience):
+ """Create a copy of GDCH credentials with the specified audience.
+
+ Args:
+ audience (str): The intended audience for GDCH credentials.
+ """
+ return self.__class__(
+ self._signer,
+ self._service_identity_name,
+ self._project,
+ audience,
+ self._token_uri,
+ self._ca_cert_path,
+ )
+
+ @classmethod
+ def _from_signer_and_info(cls, signer, info):
+ """Creates a Credentials instance from a signer and service account
+ info.
+
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ info (Mapping[str, str]): The service account info.
+
+ Returns:
+ google.oauth2.gdch_credentials.ServiceAccountCredentials: The constructed
+ credentials.
+
+ Raises:
+ ValueError: If the info is not in the expected format.
+ """
+ if info["format_version"] != "1":
+ raise ValueError("Only format version 1 is supported")
+
+ return cls(
+ signer,
+ info["name"], # service_identity_name
+ info["project"],
+ None, # audience
+ info["token_uri"],
+ info.get("ca_cert_path", None),
+ )
+
+ @classmethod
+ def from_service_account_info(cls, info):
+ """Creates a Credentials instance from parsed service account info.
+
+ Args:
+ info (Mapping[str, str]): The service account info in Google
+ format.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.oauth2.gdch_credentials.ServiceAccountCredentials: The constructed
+ credentials.
+
+ Raises:
+ ValueError: If the info is not in the expected format.
+ """
+ signer = _service_account_info.from_dict(
+ info,
+ require=[
+ "format_version",
+ "private_key_id",
+ "private_key",
+ "name",
+ "project",
+ "token_uri",
+ ],
+ use_rsa_signer=False,
+ )
+ return cls._from_signer_and_info(signer, info)
+
+ @classmethod
+ def from_service_account_file(cls, filename):
+ """Creates a Credentials instance from a service account json file.
+
+ Args:
+ filename (str): The path to the service account json file.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.oauth2.gdch_credentials.ServiceAccountCredentials: The constructed
+ credentials.
+ """
+ info, signer = _service_account_info.from_filename(
+ filename,
+ require=[
+ "format_version",
+ "private_key_id",
+ "private_key",
+ "name",
+ "project",
+ "token_uri",
+ ],
+ use_rsa_signer=False,
+ )
+ return cls._from_signer_and_info(signer, info)
diff --git a/contrib/python/google-auth/py3/google/oauth2/id_token.py b/contrib/python/google-auth/py3/google/oauth2/id_token.py
new file mode 100644
index 0000000000..2b1abec2b4
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/id_token.py
@@ -0,0 +1,339 @@
+# Copyright 2016 Google LLC
+#
+# 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 ID Token helpers.
+
+Provides support for verifying `OpenID Connect ID Tokens`_, especially ones
+generated by Google infrastructure.
+
+To parse and verify an ID Token issued by Google's OAuth 2.0 authorization
+server use :func:`verify_oauth2_token`. To verify an ID Token issued by
+Firebase, use :func:`verify_firebase_token`.
+
+A general purpose ID Token verifier is available as :func:`verify_token`.
+
+Example::
+
+ from google.oauth2 import id_token
+ from google.auth.transport import requests
+
+ request = requests.Request()
+
+ id_info = id_token.verify_oauth2_token(
+ token, request, 'my-client-id.example.com')
+
+ userid = id_info['sub']
+
+By default, this will re-fetch certificates for each verification. Because
+Google's public keys are only changed infrequently (on the order of once per
+day), you may wish to take advantage of caching to reduce latency and the
+potential for network errors. This can be accomplished using an external
+library like `CacheControl`_ to create a cache-aware
+:class:`google.auth.transport.Request`::
+
+ import cachecontrol
+ import google.auth.transport.requests
+ import requests
+
+ session = requests.session()
+ cached_session = cachecontrol.CacheControl(session)
+ request = google.auth.transport.requests.Request(session=cached_session)
+
+.. _OpenID Connect ID Tokens:
+ http://openid.net/specs/openid-connect-core-1_0.html#IDToken
+.. _CacheControl: https://cachecontrol.readthedocs.io
+"""
+
+import http.client as http_client
+import json
+import os
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import jwt
+import google.auth.transport.requests
+
+
+# The URL that provides public certificates for verifying ID tokens issued
+# by Google's OAuth 2.0 authorization server.
+_GOOGLE_OAUTH2_CERTS_URL = "https://www.googleapis.com/oauth2/v1/certs"
+
+# The URL that provides public certificates for verifying ID tokens issued
+# by Firebase and the Google APIs infrastructure
+_GOOGLE_APIS_CERTS_URL = (
+ "https://www.googleapis.com/robot/v1/metadata/x509"
+ "/securetoken@system.gserviceaccount.com"
+)
+
+_GOOGLE_ISSUERS = ["accounts.google.com", "https://accounts.google.com"]
+
+
+def _fetch_certs(request, certs_url):
+ """Fetches certificates.
+
+ Google-style cerificate endpoints return JSON in the format of
+ ``{'key id': 'x509 certificate'}``.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ certs_url (str): The certificate endpoint URL.
+
+ Returns:
+ Mapping[str, str]: A mapping of public key ID to x.509 certificate
+ data.
+ """
+ response = request(certs_url, method="GET")
+
+ if response.status != http_client.OK:
+ raise exceptions.TransportError(
+ "Could not fetch certificates at {}".format(certs_url)
+ )
+
+ return json.loads(response.data.decode("utf-8"))
+
+
+def verify_token(
+ id_token,
+ request,
+ audience=None,
+ certs_url=_GOOGLE_OAUTH2_CERTS_URL,
+ clock_skew_in_seconds=0,
+):
+ """Verifies an ID token and returns the decoded token.
+
+ Args:
+ id_token (Union[str, bytes]): The encoded token.
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ audience (str or list): The audience or audiences that this token is
+ intended for. If None then the audience is not verified.
+ certs_url (str): The URL that specifies the certificates to use to
+ verify the token. This URL should return JSON in the format of
+ ``{'key id': 'x509 certificate'}``.
+ clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+ validation.
+
+ Returns:
+ Mapping[str, Any]: The decoded token.
+ """
+ certs = _fetch_certs(request, certs_url)
+
+ return jwt.decode(
+ id_token,
+ certs=certs,
+ audience=audience,
+ clock_skew_in_seconds=clock_skew_in_seconds,
+ )
+
+
+def verify_oauth2_token(id_token, request, audience=None, clock_skew_in_seconds=0):
+ """Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
+
+ Args:
+ id_token (Union[str, bytes]): The encoded token.
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ audience (str): The audience that this token is intended for. This is
+ typically your application's OAuth 2.0 client ID. If None then the
+ audience is not verified.
+ clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+ validation.
+
+ Returns:
+ Mapping[str, Any]: The decoded token.
+
+ Raises:
+ exceptions.GoogleAuthError: If the issuer is invalid.
+ ValueError: If token verification fails
+ """
+ idinfo = verify_token(
+ id_token,
+ request,
+ audience=audience,
+ certs_url=_GOOGLE_OAUTH2_CERTS_URL,
+ clock_skew_in_seconds=clock_skew_in_seconds,
+ )
+
+ if idinfo["iss"] not in _GOOGLE_ISSUERS:
+ raise exceptions.GoogleAuthError(
+ "Wrong issuer. 'iss' should be one of the following: {}".format(
+ _GOOGLE_ISSUERS
+ )
+ )
+
+ return idinfo
+
+
+def verify_firebase_token(id_token, request, audience=None, clock_skew_in_seconds=0):
+ """Verifies an ID Token issued by Firebase Authentication.
+
+ Args:
+ id_token (Union[str, bytes]): The encoded token.
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ audience (str): The audience that this token is intended for. This is
+ typically your Firebase application ID. If None then the audience
+ is not verified.
+ clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+ validation.
+
+ Returns:
+ Mapping[str, Any]: The decoded token.
+ """
+ return verify_token(
+ id_token,
+ request,
+ audience=audience,
+ certs_url=_GOOGLE_APIS_CERTS_URL,
+ clock_skew_in_seconds=clock_skew_in_seconds,
+ )
+
+
+def fetch_id_token_credentials(audience, request=None):
+ """Create the ID Token credentials from the current environment.
+
+ This function acquires ID token from the environment in the following order.
+ See https://google.aip.dev/auth/4110.
+
+ 1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+ to the path of a valid service account JSON file, then ID token is
+ acquired using this service account credentials.
+ 2. If the application is running in Compute Engine, App Engine or Cloud Run,
+ then the ID token are obtained from the metadata server.
+ 3. If metadata server doesn't exist and no valid service account credentials
+ are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
+ be raised.
+
+ Example::
+
+ import google.oauth2.id_token
+ import google.auth.transport.requests
+
+ request = google.auth.transport.requests.Request()
+ target_audience = "https://pubsub.googleapis.com"
+
+ # Create ID token credentials.
+ credentials = google.oauth2.id_token.fetch_id_token_credentials(target_audience, request=request)
+
+ # Refresh the credential to obtain an ID token.
+ credentials.refresh(request)
+
+ id_token = credentials.token
+ id_token_expiry = credentials.expiry
+
+ Args:
+ audience (str): The audience that this ID token is intended for.
+ request (Optional[google.auth.transport.Request]): A callable used to make
+ HTTP requests. A request object will be created if not provided.
+
+ Returns:
+ google.auth.credentials.Credentials: The ID token credentials.
+
+ Raises:
+ ~google.auth.exceptions.DefaultCredentialsError:
+ If metadata server doesn't exist and no valid service account
+ credentials are found.
+ """
+ # 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
+ # variable.
+ credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
+ if credentials_filename:
+ if not (
+ os.path.exists(credentials_filename)
+ and os.path.isfile(credentials_filename)
+ ):
+ raise exceptions.DefaultCredentialsError(
+ "GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
+ )
+
+ try:
+ with open(credentials_filename, "r") as f:
+ from google.oauth2 import service_account
+
+ info = json.load(f)
+ if info.get("type") == "service_account":
+ return service_account.IDTokenCredentials.from_service_account_info(
+ info, target_audience=audience
+ )
+ except ValueError as caught_exc:
+ new_exc = exceptions.DefaultCredentialsError(
+ "GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
+ caught_exc,
+ )
+ raise new_exc from caught_exc
+
+ # 2. Try to fetch ID token from metada server if it exists. The code
+ # works for GAE and Cloud Run metadata server as well.
+ try:
+ from google.auth import compute_engine
+ from google.auth.compute_engine import _metadata
+
+ # Create a request object if not provided.
+ if not request:
+ request = google.auth.transport.requests.Request()
+
+ if _metadata.ping(request):
+ return compute_engine.IDTokenCredentials(
+ request, audience, use_metadata_identity_endpoint=True
+ )
+ except (ImportError, exceptions.TransportError):
+ pass
+
+ raise exceptions.DefaultCredentialsError(
+ "Neither metadata server or valid service account credentials are found."
+ )
+
+
+def fetch_id_token(request, audience):
+ """Fetch the ID Token from the current environment.
+
+ This function acquires ID token from the environment in the following order.
+ See https://google.aip.dev/auth/4110.
+
+ 1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+ to the path of a valid service account JSON file, then ID token is
+ acquired using this service account credentials.
+ 2. If the application is running in Compute Engine, App Engine or Cloud Run,
+ then the ID token are obtained from the metadata server.
+ 3. If metadata server doesn't exist and no valid service account credentials
+ are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
+ be raised.
+
+ Example::
+
+ import google.oauth2.id_token
+ import google.auth.transport.requests
+
+ request = google.auth.transport.requests.Request()
+ target_audience = "https://pubsub.googleapis.com"
+
+ id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ audience (str): The audience that this ID token is intended for.
+
+ Returns:
+ str: The ID token.
+
+ Raises:
+ ~google.auth.exceptions.DefaultCredentialsError:
+ If metadata server doesn't exist and no valid service account
+ credentials are found.
+ """
+ id_token_credentials = fetch_id_token_credentials(audience, request=request)
+ id_token_credentials.refresh(request)
+ return id_token_credentials.token
diff --git a/contrib/python/google-auth/py3/google/oauth2/reauth.py b/contrib/python/google-auth/py3/google/oauth2/reauth.py
new file mode 100644
index 0000000000..5870347739
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/reauth.py
@@ -0,0 +1,368 @@
+# Copyright 2021 Google LLC
+#
+# 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 module that provides functions for handling rapt authentication.
+
+Reauth is a process of obtaining additional authentication (such as password,
+security token, etc.) while refreshing OAuth 2.0 credentials for a user.
+
+Credentials that use the Reauth flow must have the reauth scope,
+``https://www.googleapis.com/auth/accounts.reauth``.
+
+This module provides a high-level function for executing the Reauth process,
+:func:`refresh_grant`, and lower-level helpers for doing the individual
+steps of the reauth process.
+
+Those steps are:
+
+1. Obtaining a list of challenges from the reauth server.
+2. Running through each challenge and sending the result back to the reauth
+ server.
+3. Refreshing the access token using the returned rapt token.
+"""
+
+import sys
+
+from google.auth import exceptions
+from google.auth import metrics
+from google.oauth2 import _client
+from google.oauth2 import challenges
+
+
+_REAUTH_SCOPE = "https://www.googleapis.com/auth/accounts.reauth"
+_REAUTH_API = "https://reauth.googleapis.com/v2/sessions"
+
+_REAUTH_NEEDED_ERROR = "invalid_grant"
+_REAUTH_NEEDED_ERROR_INVALID_RAPT = "invalid_rapt"
+_REAUTH_NEEDED_ERROR_RAPT_REQUIRED = "rapt_required"
+
+_AUTHENTICATED = "AUTHENTICATED"
+_CHALLENGE_REQUIRED = "CHALLENGE_REQUIRED"
+_CHALLENGE_PENDING = "CHALLENGE_PENDING"
+
+
+# Override this global variable to set custom max number of rounds of reauth
+# challenges should be run.
+RUN_CHALLENGE_RETRY_LIMIT = 5
+
+
+def is_interactive():
+ """Check if we are in an interractive environment.
+
+ Override this function with a different logic if you are using this library
+ outside a CLI.
+
+ If the rapt token needs refreshing, the user needs to answer the challenges.
+ If the user is not in an interractive environment, the challenges can not
+ be answered and we just wait for timeout for no reason.
+
+ Returns:
+ bool: True if is interactive environment, False otherwise.
+ """
+
+ return sys.stdin.isatty()
+
+
+def _get_challenges(
+ request, supported_challenge_types, access_token, requested_scopes=None
+):
+ """Does initial request to reauth API to get the challenges.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ supported_challenge_types (Sequence[str]): list of challenge names
+ supported by the manager.
+ access_token (str): Access token with reauth scopes.
+ requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.
+
+ Returns:
+ dict: The response from the reauth API.
+ """
+ body = {"supportedChallengeTypes": supported_challenge_types}
+ if requested_scopes:
+ body["oauthScopesForDomainPolicyLookup"] = requested_scopes
+ metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_start()}
+
+ return _client._token_endpoint_request(
+ request,
+ _REAUTH_API + ":start",
+ body,
+ access_token=access_token,
+ use_json=True,
+ headers=metrics_header,
+ )
+
+
+def _send_challenge_result(
+ request, session_id, challenge_id, client_input, access_token
+):
+ """Attempt to refresh access token by sending next challenge result.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ session_id (str): session id returned by the initial reauth call.
+ challenge_id (str): challenge id returned by the initial reauth call.
+ client_input: dict with a challenge-specific client input. For example:
+ ``{'credential': password}`` for password challenge.
+ access_token (str): Access token with reauth scopes.
+
+ Returns:
+ dict: The response from the reauth API.
+ """
+ body = {
+ "sessionId": session_id,
+ "challengeId": challenge_id,
+ "action": "RESPOND",
+ "proposalResponse": client_input,
+ }
+ metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_continue()}
+
+ return _client._token_endpoint_request(
+ request,
+ _REAUTH_API + "/{}:continue".format(session_id),
+ body,
+ access_token=access_token,
+ use_json=True,
+ headers=metrics_header,
+ )
+
+
+def _run_next_challenge(msg, request, access_token):
+ """Get the next challenge from msg and run it.
+
+ Args:
+ msg (dict): Reauth API response body (either from the initial request to
+ https://reauth.googleapis.com/v2/sessions:start or from sending the
+ previous challenge response to
+ https://reauth.googleapis.com/v2/sessions/id:continue)
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ access_token (str): reauth access token
+
+ Returns:
+ dict: The response from the reauth API.
+
+ Raises:
+ google.auth.exceptions.ReauthError: if reauth failed.
+ """
+ for challenge in msg["challenges"]:
+ if challenge["status"] != "READY":
+ # Skip non-activated challenges.
+ continue
+ c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)
+ if not c:
+ raise exceptions.ReauthFailError(
+ "Unsupported challenge type {0}. Supported types: {1}".format(
+ challenge["challengeType"],
+ ",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),
+ )
+ )
+ if not c.is_locally_eligible:
+ raise exceptions.ReauthFailError(
+ "Challenge {0} is not locally eligible".format(
+ challenge["challengeType"]
+ )
+ )
+ client_input = c.obtain_challenge_input(challenge)
+ if not client_input:
+ return None
+ return _send_challenge_result(
+ request,
+ msg["sessionId"],
+ challenge["challengeId"],
+ client_input,
+ access_token,
+ )
+ return None
+
+
+def _obtain_rapt(request, access_token, requested_scopes):
+ """Given an http request method and reauth access token, get rapt token.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ access_token (str): reauth access token
+ requested_scopes (Sequence[str]): scopes required by the client application
+
+ Returns:
+ str: The rapt token.
+
+ Raises:
+ google.auth.exceptions.ReauthError: if reauth failed
+ """
+ msg = _get_challenges(
+ request,
+ list(challenges.AVAILABLE_CHALLENGES.keys()),
+ access_token,
+ requested_scopes,
+ )
+
+ if msg["status"] == _AUTHENTICATED:
+ return msg["encodedProofOfReauthToken"]
+
+ for _ in range(0, RUN_CHALLENGE_RETRY_LIMIT):
+ if not (
+ msg["status"] == _CHALLENGE_REQUIRED or msg["status"] == _CHALLENGE_PENDING
+ ):
+ raise exceptions.ReauthFailError(
+ "Reauthentication challenge failed due to API error: {}".format(
+ msg["status"]
+ )
+ )
+
+ if not is_interactive():
+ raise exceptions.ReauthFailError(
+ "Reauthentication challenge could not be answered because you are not"
+ " in an interactive session."
+ )
+
+ msg = _run_next_challenge(msg, request, access_token)
+
+ if not msg:
+ raise exceptions.ReauthFailError("Failed to obtain rapt token.")
+ if msg["status"] == _AUTHENTICATED:
+ return msg["encodedProofOfReauthToken"]
+
+ # If we got here it means we didn't get authenticated.
+ raise exceptions.ReauthFailError("Failed to obtain rapt token.")
+
+
+def get_rapt_token(
+ request, client_id, client_secret, refresh_token, token_uri, scopes=None
+):
+ """Given an http request method and refresh_token, get rapt token.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ client_id (str): client id to get access token for reauth scope.
+ client_secret (str): client secret for the client_id
+ refresh_token (str): refresh token to refresh access token
+ token_uri (str): uri to refresh access token
+ scopes (Optional(Sequence[str])): scopes required by the client application
+
+ Returns:
+ str: The rapt token.
+ Raises:
+ google.auth.exceptions.RefreshError: If reauth failed.
+ """
+ sys.stderr.write("Reauthentication required.\n")
+
+ # Get access token for reauth.
+ access_token, _, _, _ = _client.refresh_grant(
+ request=request,
+ client_id=client_id,
+ client_secret=client_secret,
+ refresh_token=refresh_token,
+ token_uri=token_uri,
+ scopes=[_REAUTH_SCOPE],
+ )
+
+ # Get rapt token from reauth API.
+ rapt_token = _obtain_rapt(request, access_token, requested_scopes=scopes)
+
+ return rapt_token
+
+
+def refresh_grant(
+ request,
+ token_uri,
+ refresh_token,
+ client_id,
+ client_secret,
+ scopes=None,
+ rapt_token=None,
+ enable_reauth_refresh=False,
+):
+ """Implements the reauthentication flow.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+ URI.
+ refresh_token (str): The refresh token to use to get a new access
+ token.
+ client_id (str): The OAuth 2.0 application's client ID.
+ client_secret (str): The Oauth 2.0 appliaction's client secret.
+ scopes (Optional(Sequence[str])): Scopes to request. If present, all
+ scopes must be authorized for the refresh token. Useful if refresh
+ token has a wild card scope (e.g.
+ 'https://www.googleapis.com/auth/any-api').
+ rapt_token (Optional(str)): The rapt token for reauth.
+ enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
+ should be used. The default value is False. This option is for
+ gcloud only, other users should use the default value.
+
+ Returns:
+ Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
+ access token, new refresh token, expiration, the additional data
+ returned by the token endpoint, and the rapt token.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the token endpoint returned
+ an error.
+ """
+ body = {
+ "grant_type": _client._REFRESH_GRANT_TYPE,
+ "client_id": client_id,
+ "client_secret": client_secret,
+ "refresh_token": refresh_token,
+ }
+ if scopes:
+ body["scope"] = " ".join(scopes)
+ if rapt_token:
+ body["rapt"] = rapt_token
+ metrics_header = {metrics.API_CLIENT_HEADER: metrics.token_request_user()}
+
+ response_status_ok, response_data, retryable_error = _client._token_endpoint_request_no_throw(
+ request, token_uri, body, headers=metrics_header
+ )
+
+ if not response_status_ok and isinstance(response_data, str):
+ raise exceptions.RefreshError(response_data, retryable=False)
+
+ if (
+ not response_status_ok
+ and response_data.get("error") == _REAUTH_NEEDED_ERROR
+ and (
+ response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_INVALID_RAPT
+ or response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED
+ )
+ ):
+ if not enable_reauth_refresh:
+ raise exceptions.RefreshError(
+ "Reauthentication is needed. Please run `gcloud auth application-default login` to reauthenticate."
+ )
+
+ rapt_token = get_rapt_token(
+ request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
+ )
+ body["rapt"] = rapt_token
+ (
+ response_status_ok,
+ response_data,
+ retryable_error,
+ ) = _client._token_endpoint_request_no_throw(
+ request, token_uri, body, headers=metrics_header
+ )
+
+ if not response_status_ok:
+ _client._handle_error_response(response_data, retryable_error)
+ return _client._handle_refresh_grant_response(response_data, refresh_token) + (
+ rapt_token,
+ )
diff --git a/contrib/python/google-auth/py3/google/oauth2/service_account.py b/contrib/python/google-auth/py3/google/oauth2/service_account.py
new file mode 100644
index 0000000000..e08899f8e5
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/service_account.py
@@ -0,0 +1,819 @@
+# Copyright 2016 Google LLC
+#
+# 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.
+
+"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
+
+This module implements the JWT Profile for OAuth 2.0 Authorization Grants
+as defined by `RFC 7523`_ with particular support for how this RFC is
+implemented in Google's infrastructure. Google refers to these credentials
+as *Service Accounts*.
+
+Service accounts are used for server-to-server communication, such as
+interactions between a web application server and a Google service. The
+service account belongs to your application instead of to an individual end
+user. In contrast to other OAuth 2.0 profiles, no users are involved and your
+application "acts" as the service account.
+
+Typically an application uses a service account when the application uses
+Google APIs to work with its own data rather than a user's data. For example,
+an application that uses Google Cloud Datastore for data persistence would use
+a service account to authenticate its calls to the Google Cloud Datastore API.
+However, an application that needs to access a user's Drive documents would
+use the normal OAuth 2.0 profile.
+
+Additionally, Google Apps domain administrators can grant service accounts
+`domain-wide delegation`_ authority to access user data on behalf of users in
+the domain.
+
+This profile uses a JWT to acquire an OAuth 2.0 access token. The JWT is used
+in place of the usual authorization token returned during the standard
+OAuth 2.0 Authorization Code grant. The JWT is only used for this purpose, as
+the acquired access token is used as the bearer token when making requests
+using these credentials.
+
+This profile differs from normal OAuth 2.0 profile because no user consent
+step is required. The use of the private key allows this profile to assert
+identity directly.
+
+This profile also differs from the :mod:`google.auth.jwt` authentication
+because the JWT credentials use the JWT directly as the bearer token. This
+profile instead only uses the JWT to obtain an OAuth 2.0 access token. The
+obtained OAuth 2.0 access token is used as the bearer token.
+
+Domain-wide delegation
+----------------------
+
+Domain-wide delegation allows a service account to access user data on
+behalf of any user in a Google Apps domain without consent from the user.
+For example, an application that uses the Google Calendar API to add events to
+the calendars of all users in a Google Apps domain would use a service account
+to access the Google Calendar API on behalf of users.
+
+The Google Apps administrator must explicitly authorize the service account to
+do this. This authorization step is referred to as "delegating domain-wide
+authority" to a service account.
+
+You can use domain-wise delegation by creating a set of credentials with a
+specific subject using :meth:`~Credentials.with_subject`.
+
+.. _RFC 7523: https://tools.ietf.org/html/rfc7523
+"""
+
+import copy
+import datetime
+
+from google.auth import _helpers
+from google.auth import _service_account_info
+from google.auth import credentials
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth import metrics
+from google.oauth2 import _client
+
+_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
+_DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
+_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
+
+
+class Credentials(
+ credentials.Signing,
+ credentials.Scoped,
+ credentials.CredentialsWithQuotaProject,
+ credentials.CredentialsWithTokenUri,
+):
+ """Service account credentials
+
+ Usually, you'll create these credentials with one of the helper
+ constructors. To create credentials using a Google service account
+ private key JSON file::
+
+ credentials = service_account.Credentials.from_service_account_file(
+ 'service-account.json')
+
+ Or if you already have the service account file loaded::
+
+ service_account_info = json.load(open('service_account.json'))
+ credentials = service_account.Credentials.from_service_account_info(
+ service_account_info)
+
+ Both helper methods pass on arguments to the constructor, so you can
+ specify additional scopes and a subject if necessary::
+
+ credentials = service_account.Credentials.from_service_account_file(
+ 'service-account.json',
+ scopes=['email'],
+ subject='user@example.com')
+
+ The credentials are considered immutable. If you want to modify the scopes
+ or the subject used for delegation, use :meth:`with_scopes` or
+ :meth:`with_subject`::
+
+ scoped_credentials = credentials.with_scopes(['email'])
+ delegated_credentials = credentials.with_subject(subject)
+
+ To add a quota project, use :meth:`with_quota_project`::
+
+ credentials = credentials.with_quota_project('myproject-123')
+ """
+
+ def __init__(
+ self,
+ signer,
+ service_account_email,
+ token_uri,
+ scopes=None,
+ default_scopes=None,
+ subject=None,
+ project_id=None,
+ quota_project_id=None,
+ additional_claims=None,
+ always_use_jwt_access=False,
+ universe_domain=_DEFAULT_UNIVERSE_DOMAIN,
+ trust_boundary=None,
+ ):
+ """
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ service_account_email (str): The service account's email.
+ scopes (Sequence[str]): User-defined scopes to request during the
+ authorization grant.
+ default_scopes (Sequence[str]): Default scopes passed by a
+ Google client library. Use 'scopes' for user-defined scopes.
+ token_uri (str): The OAuth 2.0 Token URI.
+ subject (str): For domain-wide delegation, the email address of the
+ user to for which to request delegated access.
+ project_id (str): Project ID associated with the service account
+ credential.
+ quota_project_id (Optional[str]): The project ID used for quota and
+ billing.
+ additional_claims (Mapping[str, str]): Any additional claims for
+ the JWT assertion used in the authorization grant.
+ always_use_jwt_access (Optional[bool]): Whether self signed JWT should
+ be always used.
+ universe_domain (str): The universe domain. The default
+ universe domain is googleapis.com. For default value self
+ signed jwt is used for token refresh.
+ trust_boundary (str): String representation of trust boundary meta.
+
+ .. note:: Typically one of the helper constructors
+ :meth:`from_service_account_file` or
+ :meth:`from_service_account_info` are used instead of calling the
+ constructor directly.
+ """
+ super(Credentials, self).__init__()
+
+ self._scopes = scopes
+ self._default_scopes = default_scopes
+ self._signer = signer
+ self._service_account_email = service_account_email
+ self._subject = subject
+ self._project_id = project_id
+ self._quota_project_id = quota_project_id
+ self._token_uri = token_uri
+ self._always_use_jwt_access = always_use_jwt_access
+ if not universe_domain:
+ self._universe_domain = _DEFAULT_UNIVERSE_DOMAIN
+ else:
+ self._universe_domain = universe_domain
+
+ if universe_domain != _DEFAULT_UNIVERSE_DOMAIN:
+ self._always_use_jwt_access = True
+
+ self._jwt_credentials = None
+
+ if additional_claims is not None:
+ self._additional_claims = additional_claims
+ else:
+ self._additional_claims = {}
+ self._trust_boundary = "0"
+
+ @classmethod
+ def _from_signer_and_info(cls, signer, info, **kwargs):
+ """Creates a Credentials instance from a signer and service account
+ info.
+
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ info (Mapping[str, str]): The service account info.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.jwt.Credentials: The constructed credentials.
+
+ Raises:
+ ValueError: If the info is not in the expected format.
+ """
+ return cls(
+ signer,
+ service_account_email=info["client_email"],
+ token_uri=info["token_uri"],
+ project_id=info.get("project_id"),
+ universe_domain=info.get("universe_domain", _DEFAULT_UNIVERSE_DOMAIN),
+ trust_boundary=info.get("trust_boundary"),
+ **kwargs
+ )
+
+ @classmethod
+ def from_service_account_info(cls, info, **kwargs):
+ """Creates a Credentials instance from parsed service account info.
+
+ Args:
+ info (Mapping[str, str]): The service account info in Google
+ format.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.service_account.Credentials: The constructed
+ credentials.
+
+ Raises:
+ ValueError: If the info is not in the expected format.
+ """
+ signer = _service_account_info.from_dict(
+ info, require=["client_email", "token_uri"]
+ )
+ return cls._from_signer_and_info(signer, info, **kwargs)
+
+ @classmethod
+ def from_service_account_file(cls, filename, **kwargs):
+ """Creates a Credentials instance from a service account json file.
+
+ Args:
+ filename (str): The path to the service account json file.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.service_account.Credentials: The constructed
+ credentials.
+ """
+ info, signer = _service_account_info.from_filename(
+ filename, require=["client_email", "token_uri"]
+ )
+ return cls._from_signer_and_info(signer, info, **kwargs)
+
+ @property
+ def service_account_email(self):
+ """The service account email."""
+ return self._service_account_email
+
+ @property
+ def project_id(self):
+ """Project ID associated with this credential."""
+ return self._project_id
+
+ @property
+ def requires_scopes(self):
+ """Checks if the credentials requires scopes.
+
+ Returns:
+ bool: True if there are no scopes set otherwise False.
+ """
+ return True if not self._scopes else False
+
+ def _make_copy(self):
+ cred = self.__class__(
+ self._signer,
+ service_account_email=self._service_account_email,
+ scopes=copy.copy(self._scopes),
+ default_scopes=copy.copy(self._default_scopes),
+ token_uri=self._token_uri,
+ subject=self._subject,
+ project_id=self._project_id,
+ quota_project_id=self._quota_project_id,
+ additional_claims=self._additional_claims.copy(),
+ always_use_jwt_access=self._always_use_jwt_access,
+ universe_domain=self._universe_domain,
+ )
+ return cred
+
+ @_helpers.copy_docstring(credentials.Scoped)
+ def with_scopes(self, scopes, default_scopes=None):
+ cred = self._make_copy()
+ cred._scopes = scopes
+ cred._default_scopes = default_scopes
+ return cred
+
+ def with_always_use_jwt_access(self, always_use_jwt_access):
+ """Create a copy of these credentials with the specified always_use_jwt_access value.
+
+ Args:
+ always_use_jwt_access (bool): Whether always use self signed JWT or not.
+
+ Returns:
+ google.auth.service_account.Credentials: A new credentials
+ instance.
+ Raises:
+ google.auth.exceptions.InvalidValue: If the universe domain is not
+ default and always_use_jwt_access is False.
+ """
+ cred = self._make_copy()
+ if (
+ cred._universe_domain != _DEFAULT_UNIVERSE_DOMAIN
+ and not always_use_jwt_access
+ ):
+ raise exceptions.InvalidValue(
+ "always_use_jwt_access should be True for non-default universe domain"
+ )
+ cred._always_use_jwt_access = always_use_jwt_access
+ return cred
+
+ def with_subject(self, subject):
+ """Create a copy of these credentials with the specified subject.
+
+ Args:
+ subject (str): The subject claim.
+
+ Returns:
+ google.auth.service_account.Credentials: A new credentials
+ instance.
+ """
+ cred = self._make_copy()
+ cred._subject = subject
+ return cred
+
+ def with_claims(self, additional_claims):
+ """Returns a copy of these credentials with modified claims.
+
+ Args:
+ additional_claims (Mapping[str, str]): Any additional claims for
+ the JWT payload. This will be merged with the current
+ additional claims.
+
+ Returns:
+ google.auth.service_account.Credentials: A new credentials
+ instance.
+ """
+ new_additional_claims = copy.deepcopy(self._additional_claims)
+ new_additional_claims.update(additional_claims or {})
+ cred = self._make_copy()
+ cred._additional_claims = new_additional_claims
+ return cred
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ cred = self._make_copy()
+ cred._quota_project_id = quota_project_id
+ return cred
+
+ @_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
+ def with_token_uri(self, token_uri):
+ cred = self._make_copy()
+ cred._token_uri = token_uri
+ return cred
+
+ def _make_authorization_grant_assertion(self):
+ """Create the OAuth 2.0 assertion.
+
+ This assertion is used during the OAuth 2.0 grant to acquire an
+ access token.
+
+ Returns:
+ bytes: The authorization grant assertion.
+ """
+ now = _helpers.utcnow()
+ lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
+ expiry = now + lifetime
+
+ payload = {
+ "iat": _helpers.datetime_to_secs(now),
+ "exp": _helpers.datetime_to_secs(expiry),
+ # The issuer must be the service account email.
+ "iss": self._service_account_email,
+ # The audience must be the auth token endpoint's URI
+ "aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
+ "scope": _helpers.scopes_to_string(self._scopes or ()),
+ }
+
+ payload.update(self._additional_claims)
+
+ # The subject can be a user email for domain-wide delegation.
+ if self._subject:
+ payload.setdefault("sub", self._subject)
+
+ token = jwt.encode(self._signer, payload)
+
+ return token
+
+ def _use_self_signed_jwt(self):
+ # Since domain wide delegation doesn't work with self signed JWT. If
+ # subject exists, then we should not use self signed JWT.
+ return self._subject is None and self._jwt_credentials is not None
+
+ def _metric_header_for_usage(self):
+ if self._use_self_signed_jwt():
+ return metrics.CRED_TYPE_SA_JWT
+ return metrics.CRED_TYPE_SA_ASSERTION
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def refresh(self, request):
+ if (
+ self._universe_domain != _DEFAULT_UNIVERSE_DOMAIN
+ and not self._jwt_credentials
+ ):
+ raise exceptions.RefreshError(
+ "self._jwt_credentials is missing for non-default universe domain"
+ )
+ if self._universe_domain != _DEFAULT_UNIVERSE_DOMAIN and self._subject:
+ raise exceptions.RefreshError(
+ "domain wide delegation is not supported for non-default universe domain"
+ )
+
+ if self._use_self_signed_jwt():
+ self._jwt_credentials.refresh(request)
+ self.token = self._jwt_credentials.token.decode()
+ self.expiry = self._jwt_credentials.expiry
+ else:
+ assertion = self._make_authorization_grant_assertion()
+ access_token, expiry, _ = _client.jwt_grant(
+ request, self._token_uri, assertion
+ )
+ self.token = access_token
+ self.expiry = expiry
+
+ def _create_self_signed_jwt(self, audience):
+ """Create a self-signed JWT from the credentials if requirements are met.
+
+ Args:
+ audience (str): The service URL. ``https://[API_ENDPOINT]/``
+ """
+ # https://google.aip.dev/auth/4111
+ if self._always_use_jwt_access:
+ if self._scopes:
+ additional_claims = {"scope": " ".join(self._scopes)}
+ if (
+ self._jwt_credentials is None
+ or self._jwt_credentials.additional_claims != additional_claims
+ ):
+ self._jwt_credentials = jwt.Credentials.from_signing_credentials(
+ self, None, additional_claims=additional_claims
+ )
+ elif audience:
+ if (
+ self._jwt_credentials is None
+ or self._jwt_credentials._audience != audience
+ ):
+
+ self._jwt_credentials = jwt.Credentials.from_signing_credentials(
+ self, audience
+ )
+ elif self._default_scopes:
+ additional_claims = {"scope": " ".join(self._default_scopes)}
+ if (
+ self._jwt_credentials is None
+ or additional_claims != self._jwt_credentials.additional_claims
+ ):
+ self._jwt_credentials = jwt.Credentials.from_signing_credentials(
+ self, None, additional_claims=additional_claims
+ )
+ elif not self._scopes and audience:
+ self._jwt_credentials = jwt.Credentials.from_signing_credentials(
+ self, audience
+ )
+
+ @_helpers.copy_docstring(credentials.Signing)
+ def sign_bytes(self, message):
+ return self._signer.sign(message)
+
+ @property # type: ignore
+ @_helpers.copy_docstring(credentials.Signing)
+ def signer(self):
+ return self._signer
+
+ @property # type: ignore
+ @_helpers.copy_docstring(credentials.Signing)
+ def signer_email(self):
+ return self._service_account_email
+
+
+class IDTokenCredentials(
+ credentials.Signing,
+ credentials.CredentialsWithQuotaProject,
+ credentials.CredentialsWithTokenUri,
+):
+ """Open ID Connect ID Token-based service account credentials.
+
+ These credentials are largely similar to :class:`.Credentials`, but instead
+ of using an OAuth 2.0 Access Token as the bearer token, they use an Open
+ ID Connect ID Token as the bearer token. These credentials are useful when
+ communicating to services that require ID Tokens and can not accept access
+ tokens.
+
+ Usually, you'll create these credentials with one of the helper
+ constructors. To create credentials using a Google service account
+ private key JSON file::
+
+ credentials = (
+ service_account.IDTokenCredentials.from_service_account_file(
+ 'service-account.json'))
+
+
+ Or if you already have the service account file loaded::
+
+ service_account_info = json.load(open('service_account.json'))
+ credentials = (
+ service_account.IDTokenCredentials.from_service_account_info(
+ service_account_info))
+
+
+ Both helper methods pass on arguments to the constructor, so you can
+ specify additional scopes and a subject if necessary::
+
+ credentials = (
+ service_account.IDTokenCredentials.from_service_account_file(
+ 'service-account.json',
+ scopes=['email'],
+ subject='user@example.com'))
+
+
+ The credentials are considered immutable. If you want to modify the scopes
+ or the subject used for delegation, use :meth:`with_scopes` or
+ :meth:`with_subject`::
+
+ scoped_credentials = credentials.with_scopes(['email'])
+ delegated_credentials = credentials.with_subject(subject)
+
+ """
+
+ def __init__(
+ self,
+ signer,
+ service_account_email,
+ token_uri,
+ target_audience,
+ additional_claims=None,
+ quota_project_id=None,
+ universe_domain=_DEFAULT_UNIVERSE_DOMAIN,
+ ):
+ """
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ service_account_email (str): The service account's email.
+ token_uri (str): The OAuth 2.0 Token URI.
+ target_audience (str): The intended audience for these credentials,
+ used when requesting the ID Token. The ID Token's ``aud`` claim
+ will be set to this string.
+ additional_claims (Mapping[str, str]): Any additional claims for
+ the JWT assertion used in the authorization grant.
+ quota_project_id (Optional[str]): The project ID used for quota and billing.
+ universe_domain (str): The universe domain. The default
+ universe domain is googleapis.com. For default value IAM ID
+ token endponint is used for token refresh. Note that
+ iam.serviceAccountTokenCreator role is required to use the IAM
+ endpoint.
+ .. note:: Typically one of the helper constructors
+ :meth:`from_service_account_file` or
+ :meth:`from_service_account_info` are used instead of calling the
+ constructor directly.
+ """
+ super(IDTokenCredentials, self).__init__()
+ self._signer = signer
+ self._service_account_email = service_account_email
+ self._token_uri = token_uri
+ self._target_audience = target_audience
+ self._quota_project_id = quota_project_id
+ self._use_iam_endpoint = False
+
+ if not universe_domain:
+ self._universe_domain = _DEFAULT_UNIVERSE_DOMAIN
+ else:
+ self._universe_domain = universe_domain
+
+ if universe_domain != _DEFAULT_UNIVERSE_DOMAIN:
+ self._use_iam_endpoint = True
+
+ if additional_claims is not None:
+ self._additional_claims = additional_claims
+ else:
+ self._additional_claims = {}
+
+ @classmethod
+ def _from_signer_and_info(cls, signer, info, **kwargs):
+ """Creates a credentials instance from a signer and service account
+ info.
+
+ Args:
+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+ info (Mapping[str, str]): The service account info.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.jwt.IDTokenCredentials: The constructed credentials.
+
+ Raises:
+ ValueError: If the info is not in the expected format.
+ """
+ kwargs.setdefault("service_account_email", info["client_email"])
+ kwargs.setdefault("token_uri", info["token_uri"])
+ if "universe_domain" in info:
+ kwargs["universe_domain"] = info["universe_domain"]
+ return cls(signer, **kwargs)
+
+ @classmethod
+ def from_service_account_info(cls, info, **kwargs):
+ """Creates a credentials instance from parsed service account info.
+
+ Args:
+ info (Mapping[str, str]): The service account info in Google
+ format.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.service_account.IDTokenCredentials: The constructed
+ credentials.
+
+ Raises:
+ ValueError: If the info is not in the expected format.
+ """
+ signer = _service_account_info.from_dict(
+ info, require=["client_email", "token_uri"]
+ )
+ return cls._from_signer_and_info(signer, info, **kwargs)
+
+ @classmethod
+ def from_service_account_file(cls, filename, **kwargs):
+ """Creates a credentials instance from a service account json file.
+
+ Args:
+ filename (str): The path to the service account json file.
+ kwargs: Additional arguments to pass to the constructor.
+
+ Returns:
+ google.auth.service_account.IDTokenCredentials: The constructed
+ credentials.
+ """
+ info, signer = _service_account_info.from_filename(
+ filename, require=["client_email", "token_uri"]
+ )
+ return cls._from_signer_and_info(signer, info, **kwargs)
+
+ def _make_copy(self):
+ cred = self.__class__(
+ self._signer,
+ service_account_email=self._service_account_email,
+ token_uri=self._token_uri,
+ target_audience=self._target_audience,
+ additional_claims=self._additional_claims.copy(),
+ quota_project_id=self.quota_project_id,
+ universe_domain=self._universe_domain,
+ )
+ # _use_iam_endpoint is not exposed in the constructor
+ cred._use_iam_endpoint = self._use_iam_endpoint
+ return cred
+
+ def with_target_audience(self, target_audience):
+ """Create a copy of these credentials with the specified target
+ audience.
+
+ Args:
+ target_audience (str): The intended audience for these credentials,
+ used when requesting the ID Token.
+
+ Returns:
+ google.auth.service_account.IDTokenCredentials: A new credentials
+ instance.
+ """
+ cred = self._make_copy()
+ cred._target_audience = target_audience
+ return cred
+
+ def _with_use_iam_endpoint(self, use_iam_endpoint):
+ """Create a copy of these credentials with the use_iam_endpoint value.
+
+ Args:
+ use_iam_endpoint (bool): If True, IAM generateIdToken endpoint will
+ be used instead of the token_uri. Note that
+ iam.serviceAccountTokenCreator role is required to use the IAM
+ endpoint. The default value is False. This feature is currently
+ experimental and subject to change without notice.
+
+ Returns:
+ google.auth.service_account.IDTokenCredentials: A new credentials
+ instance.
+ Raises:
+ google.auth.exceptions.InvalidValue: If the universe domain is not
+ default and use_iam_endpoint is False.
+ """
+ cred = self._make_copy()
+ if cred._universe_domain != _DEFAULT_UNIVERSE_DOMAIN and not use_iam_endpoint:
+ raise exceptions.InvalidValue(
+ "use_iam_endpoint should be True for non-default universe domain"
+ )
+ cred._use_iam_endpoint = use_iam_endpoint
+ return cred
+
+ @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+ def with_quota_project(self, quota_project_id):
+ cred = self._make_copy()
+ cred._quota_project_id = quota_project_id
+ return cred
+
+ @_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
+ def with_token_uri(self, token_uri):
+ cred = self._make_copy()
+ cred._token_uri = token_uri
+ return cred
+
+ def _make_authorization_grant_assertion(self):
+ """Create the OAuth 2.0 assertion.
+
+ This assertion is used during the OAuth 2.0 grant to acquire an
+ ID token.
+
+ Returns:
+ bytes: The authorization grant assertion.
+ """
+ now = _helpers.utcnow()
+ lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
+ expiry = now + lifetime
+
+ payload = {
+ "iat": _helpers.datetime_to_secs(now),
+ "exp": _helpers.datetime_to_secs(expiry),
+ # The issuer must be the service account email.
+ "iss": self.service_account_email,
+ # The audience must be the auth token endpoint's URI
+ "aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
+ # The target audience specifies which service the ID token is
+ # intended for.
+ "target_audience": self._target_audience,
+ }
+
+ payload.update(self._additional_claims)
+
+ token = jwt.encode(self._signer, payload)
+
+ return token
+
+ def _refresh_with_iam_endpoint(self, request):
+ """Use IAM generateIdToken endpoint to obtain an ID token.
+
+ It works as follows:
+
+ 1. First we create a self signed jwt with
+ https://www.googleapis.com/auth/iam being the scope.
+
+ 2. Next we use the self signed jwt as the access token, and make a POST
+ request to IAM generateIdToken endpoint. The request body is:
+ {
+ "audience": self._target_audience,
+ "includeEmail": "true",
+ "useEmailAzp": "true",
+ }
+
+ If the request is succesfully, it will return {"token":"the ID token"},
+ and we can extract the ID token and compute its expiry.
+ """
+ jwt_credentials = jwt.Credentials.from_signing_credentials(
+ self,
+ None,
+ additional_claims={"scope": "https://www.googleapis.com/auth/iam"},
+ )
+ jwt_credentials.refresh(request)
+ self.token, self.expiry = _client.call_iam_generate_id_token_endpoint(
+ request,
+ self.signer_email,
+ self._target_audience,
+ jwt_credentials.token.decode(),
+ )
+
+ @_helpers.copy_docstring(credentials.Credentials)
+ def refresh(self, request):
+ if self._use_iam_endpoint:
+ self._refresh_with_iam_endpoint(request)
+ else:
+ assertion = self._make_authorization_grant_assertion()
+ access_token, expiry, _ = _client.id_token_jwt_grant(
+ request, self._token_uri, assertion
+ )
+ self.token = access_token
+ self.expiry = expiry
+
+ @property
+ def service_account_email(self):
+ """The service account email."""
+ return self._service_account_email
+
+ @_helpers.copy_docstring(credentials.Signing)
+ def sign_bytes(self, message):
+ return self._signer.sign(message)
+
+ @property # type: ignore
+ @_helpers.copy_docstring(credentials.Signing)
+ def signer(self):
+ return self._signer
+
+ @property # type: ignore
+ @_helpers.copy_docstring(credentials.Signing)
+ def signer_email(self):
+ return self._service_account_email
diff --git a/contrib/python/google-auth/py3/google/oauth2/sts.py b/contrib/python/google-auth/py3/google/oauth2/sts.py
new file mode 100644
index 0000000000..ad3962735f
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/sts.py
@@ -0,0 +1,176 @@
+# Copyright 2020 Google LLC
+#
+# 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 Token Exchange Spec.
+
+This module defines a token exchange utility based on the `OAuth 2.0 Token
+Exchange`_ spec. This will be mainly used to exchange external credentials
+for GCP access tokens in workload identity pools to access Google APIs.
+
+The implementation will support various types of client authentication as
+allowed in the spec.
+
+A deviation on the spec will be for additional Google specific options that
+cannot be easily mapped to parameters defined in the RFC.
+
+The returned dictionary response will be based on the `rfc8693 section 2.2.1`_
+spec JSON response.
+
+.. _OAuth 2.0 Token Exchange: https://tools.ietf.org/html/rfc8693
+.. _rfc8693 section 2.2.1: https://tools.ietf.org/html/rfc8693#section-2.2.1
+"""
+
+import http.client as http_client
+import json
+import urllib
+
+from google.oauth2 import utils
+
+
+_URLENCODED_HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
+
+
+class Client(utils.OAuthClientAuthHandler):
+ """Implements the OAuth 2.0 token exchange spec based on
+ https://tools.ietf.org/html/rfc8693.
+ """
+
+ def __init__(self, token_exchange_endpoint, client_authentication=None):
+ """Initializes an STS client instance.
+
+ Args:
+ token_exchange_endpoint (str): The token exchange endpoint.
+ client_authentication (Optional(google.oauth2.oauth2_utils.ClientAuthentication)):
+ The optional OAuth client authentication credentials if available.
+ """
+ super(Client, self).__init__(client_authentication)
+ self._token_exchange_endpoint = token_exchange_endpoint
+
+ def _make_request(self, request, headers, request_body):
+ # Initialize request headers.
+ request_headers = _URLENCODED_HEADERS.copy()
+
+ # Inject additional headers.
+ if headers:
+ for k, v in dict(headers).items():
+ request_headers[k] = v
+
+ # Apply OAuth client authentication.
+ self.apply_client_authentication_options(request_headers, request_body)
+
+ # Execute request.
+ response = request(
+ url=self._token_exchange_endpoint,
+ method="POST",
+ headers=request_headers,
+ body=urllib.parse.urlencode(request_body).encode("utf-8"),
+ )
+
+ response_body = (
+ response.data.decode("utf-8")
+ if hasattr(response.data, "decode")
+ else response.data
+ )
+
+ # If non-200 response received, translate to OAuthError exception.
+ if response.status != http_client.OK:
+ utils.handle_error_response(response_body)
+
+ response_data = json.loads(response_body)
+
+ # Return successful response.
+ return response_data
+
+ def exchange_token(
+ self,
+ request,
+ grant_type,
+ subject_token,
+ subject_token_type,
+ resource=None,
+ audience=None,
+ scopes=None,
+ requested_token_type=None,
+ actor_token=None,
+ actor_token_type=None,
+ additional_options=None,
+ additional_headers=None,
+ ):
+ """Exchanges the provided token for another type of token based on the
+ rfc8693 spec.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ grant_type (str): The OAuth 2.0 token exchange grant type.
+ subject_token (str): The OAuth 2.0 token exchange subject token.
+ subject_token_type (str): The OAuth 2.0 token exchange subject token type.
+ resource (Optional[str]): The optional OAuth 2.0 token exchange resource field.
+ audience (Optional[str]): The optional OAuth 2.0 token exchange audience field.
+ scopes (Optional[Sequence[str]]): The optional list of scopes to use.
+ requested_token_type (Optional[str]): The optional OAuth 2.0 token exchange requested
+ token type.
+ actor_token (Optional[str]): The optional OAuth 2.0 token exchange actor token.
+ actor_token_type (Optional[str]): The optional OAuth 2.0 token exchange actor token type.
+ additional_options (Optional[Mapping[str, str]]): The optional additional
+ non-standard Google specific options.
+ additional_headers (Optional[Mapping[str, str]]): The optional additional
+ headers to pass to the token exchange endpoint.
+
+ Returns:
+ Mapping[str, str]: The token exchange JSON-decoded response data containing
+ the requested token and its expiration time.
+
+ Raises:
+ google.auth.exceptions.OAuthError: If the token endpoint returned
+ an error.
+ """
+ # Initialize request body.
+ request_body = {
+ "grant_type": grant_type,
+ "resource": resource,
+ "audience": audience,
+ "scope": " ".join(scopes or []),
+ "requested_token_type": requested_token_type,
+ "subject_token": subject_token,
+ "subject_token_type": subject_token_type,
+ "actor_token": actor_token,
+ "actor_token_type": actor_token_type,
+ "options": None,
+ }
+ # Add additional non-standard options.
+ if additional_options:
+ request_body["options"] = urllib.parse.quote(json.dumps(additional_options))
+ # Remove empty fields in request body.
+ for k, v in dict(request_body).items():
+ if v is None or v == "":
+ del request_body[k]
+
+ return self._make_request(request, additional_headers, request_body)
+
+ def refresh_token(self, request, refresh_token):
+ """Exchanges a refresh token for an access token based on the
+ RFC6749 spec.
+
+ Args:
+ request (google.auth.transport.Request): A callable used to make
+ HTTP requests.
+ subject_token (str): The OAuth 2.0 refresh token.
+ """
+
+ return self._make_request(
+ request,
+ None,
+ {"grant_type": "refresh_token", "refresh_token": refresh_token},
+ )
diff --git a/contrib/python/google-auth/py3/google/oauth2/utils.py b/contrib/python/google-auth/py3/google/oauth2/utils.py
new file mode 100644
index 0000000000..d72ff19166
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/utils.py
@@ -0,0 +1,168 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+
+This module provides implementations for various OAuth 2.0 utilities.
+This includes `OAuth error handling`_ and
+`Client authentication for OAuth flows`_.
+
+OAuth error handling
+--------------------
+This will define interfaces for handling OAuth related error responses as
+stated in `RFC 6749 section 5.2`_.
+This will include a common function to convert these HTTP error responses to a
+:class:`google.auth.exceptions.OAuthError` exception.
+
+
+Client authentication for OAuth flows
+-------------------------------------
+We introduce an interface for defining client authentication credentials based
+on `RFC 6749 section 2.3.1`_. This will expose the following
+capabilities:
+
+ * Ability to support basic authentication via request header.
+ * Ability to support bearer token authentication via request header.
+ * Ability to support client ID / secret authentication via request body.
+
+.. _RFC 6749 section 2.3.1: https://tools.ietf.org/html/rfc6749#section-2.3.1
+.. _RFC 6749 section 5.2: https://tools.ietf.org/html/rfc6749#section-5.2
+"""
+
+import abc
+import base64
+import enum
+import json
+
+from google.auth import exceptions
+
+
+# OAuth client authentication based on
+# https://tools.ietf.org/html/rfc6749#section-2.3.
+class ClientAuthType(enum.Enum):
+ basic = 1
+ request_body = 2
+
+
+class ClientAuthentication(object):
+ """Defines the client authentication credentials for basic and request-body
+ types based on https://tools.ietf.org/html/rfc6749#section-2.3.1.
+ """
+
+ def __init__(self, client_auth_type, client_id, client_secret=None):
+ """Instantiates a client authentication object containing the client ID
+ and secret credentials for basic and response-body auth.
+
+ Args:
+ client_auth_type (google.oauth2.oauth_utils.ClientAuthType): The
+ client authentication type.
+ client_id (str): The client ID.
+ client_secret (Optional[str]): The client secret.
+ """
+ self.client_auth_type = client_auth_type
+ self.client_id = client_id
+ self.client_secret = client_secret
+
+
+class OAuthClientAuthHandler(metaclass=abc.ABCMeta):
+ """Abstract class for handling client authentication in OAuth-based
+ operations.
+ """
+
+ def __init__(self, client_authentication=None):
+ """Instantiates an OAuth client authentication handler.
+
+ Args:
+ client_authentication (Optional[google.oauth2.utils.ClientAuthentication]):
+ The OAuth client authentication credentials if available.
+ """
+ super(OAuthClientAuthHandler, self).__init__()
+ self._client_authentication = client_authentication
+
+ def apply_client_authentication_options(
+ self, headers, request_body=None, bearer_token=None
+ ):
+ """Applies client authentication on the OAuth request's headers or POST
+ body.
+
+ Args:
+ headers (Mapping[str, str]): The HTTP request header.
+ request_body (Optional[Mapping[str, str]]): The HTTP request body
+ dictionary. For requests that do not support request body, this
+ is None and will be ignored.
+ bearer_token (Optional[str]): The optional bearer token.
+ """
+ # Inject authenticated header.
+ self._inject_authenticated_headers(headers, bearer_token)
+ # Inject authenticated request body.
+ if bearer_token is None:
+ self._inject_authenticated_request_body(request_body)
+
+ def _inject_authenticated_headers(self, headers, bearer_token=None):
+ if bearer_token is not None:
+ headers["Authorization"] = "Bearer %s" % bearer_token
+ elif (
+ self._client_authentication is not None
+ and self._client_authentication.client_auth_type is ClientAuthType.basic
+ ):
+ username = self._client_authentication.client_id
+ password = self._client_authentication.client_secret or ""
+
+ credentials = base64.b64encode(
+ ("%s:%s" % (username, password)).encode()
+ ).decode()
+ headers["Authorization"] = "Basic %s" % credentials
+
+ def _inject_authenticated_request_body(self, request_body):
+ if (
+ self._client_authentication is not None
+ and self._client_authentication.client_auth_type
+ is ClientAuthType.request_body
+ ):
+ if request_body is None:
+ raise exceptions.OAuthError(
+ "HTTP request does not support request-body"
+ )
+ else:
+ request_body["client_id"] = self._client_authentication.client_id
+ request_body["client_secret"] = (
+ self._client_authentication.client_secret or ""
+ )
+
+
+def handle_error_response(response_body):
+ """Translates an error response from an OAuth operation into an
+ OAuthError exception.
+
+ Args:
+ response_body (str): The decoded response data.
+
+ Raises:
+ google.auth.exceptions.OAuthError
+ """
+ try:
+ error_components = []
+ error_data = json.loads(response_body)
+
+ error_components.append("Error code {}".format(error_data["error"]))
+ if "error_description" in error_data:
+ error_components.append(": {}".format(error_data["error_description"]))
+ if "error_uri" in error_data:
+ error_components.append(" - {}".format(error_data["error_uri"]))
+ error_details = "".join(error_components)
+ # If no details could be extracted, use the response data.
+ except (KeyError, ValueError):
+ error_details = response_body
+
+ raise exceptions.OAuthError(error_details, response_body)
diff --git a/contrib/python/google-auth/py3/tests/__init__.py b/contrib/python/google-auth/py3/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/__init__.py
diff --git a/contrib/python/google-auth/py3/tests/compute_engine/__init__.py b/contrib/python/google-auth/py3/tests/compute_engine/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/compute_engine/__init__.py
diff --git a/contrib/python/google-auth/py3/tests/compute_engine/data/smbios_product_name b/contrib/python/google-auth/py3/tests/compute_engine/data/smbios_product_name
new file mode 100644
index 0000000000..2ca735d9b3
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/compute_engine/data/smbios_product_name
@@ -0,0 +1 @@
+Google Compute Engine
diff --git a/contrib/python/google-auth/py3/tests/compute_engine/data/smbios_product_name_non_google b/contrib/python/google-auth/py3/tests/compute_engine/data/smbios_product_name_non_google
new file mode 100644
index 0000000000..9fd177038e
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/compute_engine/data/smbios_product_name_non_google
@@ -0,0 +1 @@
+ABC Compute Engine
diff --git a/contrib/python/google-auth/py3/tests/compute_engine/test__metadata.py b/contrib/python/google-auth/py3/tests/compute_engine/test__metadata.py
new file mode 100644
index 0000000000..ddf84596af
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/compute_engine/test__metadata.py
@@ -0,0 +1,450 @@
+# Copyright 2016 Google LLC
+#
+# 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 datetime
+import http.client as http_client
+import importlib
+import json
+import os
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+from google.auth.compute_engine import _metadata
+
+PATH = "instance/service-accounts/default"
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
+SMBIOS_PRODUCT_NAME_FILE = os.path.join(DATA_DIR, "smbios_product_name")
+SMBIOS_PRODUCT_NAME_NONEXISTENT_FILE = os.path.join(
+ DATA_DIR, "smbios_product_name_nonexistent"
+)
+SMBIOS_PRODUCT_NAME_NON_GOOGLE = os.path.join(
+ DATA_DIR, "smbios_product_name_non_google"
+)
+
+ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
+ "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/mds"
+)
+MDS_PING_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 auth-request-type/mds"
+MDS_PING_REQUEST_HEADER = {
+ "metadata-flavor": "Google",
+ "x-goog-api-client": MDS_PING_METRICS_HEADER_VALUE,
+}
+
+
+def make_request(data, status=http_client.OK, headers=None, retry=False):
+ response = mock.create_autospec(transport.Response, instance=True)
+ response.status = status
+ response.data = _helpers.to_bytes(data)
+ response.headers = headers or {}
+
+ request = mock.create_autospec(transport.Request)
+ if retry:
+ request.side_effect = [exceptions.TransportError(), response]
+ else:
+ request.return_value = response
+
+ return request
+
+
+def test_detect_gce_residency_linux_success():
+ _metadata._GCE_PRODUCT_NAME_FILE = SMBIOS_PRODUCT_NAME_FILE
+ assert _metadata.detect_gce_residency_linux()
+
+
+def test_detect_gce_residency_linux_non_google():
+ _metadata._GCE_PRODUCT_NAME_FILE = SMBIOS_PRODUCT_NAME_NON_GOOGLE
+ assert not _metadata.detect_gce_residency_linux()
+
+
+def test_detect_gce_residency_linux_nonexistent():
+ _metadata._GCE_PRODUCT_NAME_FILE = SMBIOS_PRODUCT_NAME_NONEXISTENT_FILE
+ assert not _metadata.detect_gce_residency_linux()
+
+
+def test_is_on_gce_ping_success():
+ request = make_request("", headers=_metadata._METADATA_HEADERS)
+ assert _metadata.is_on_gce(request)
+
+
+@mock.patch("os.name", new="nt")
+def test_is_on_gce_windows_success():
+ request = make_request("", headers={_metadata._METADATA_FLAVOR_HEADER: "meep"})
+ assert not _metadata.is_on_gce(request)
+
+
+@mock.patch("os.name", new="posix")
+def test_is_on_gce_linux_success():
+ request = make_request("", headers={_metadata._METADATA_FLAVOR_HEADER: "meep"})
+ _metadata._GCE_PRODUCT_NAME_FILE = SMBIOS_PRODUCT_NAME_FILE
+ assert _metadata.is_on_gce(request)
+
+
+@mock.patch("google.auth.metrics.mds_ping", return_value=MDS_PING_METRICS_HEADER_VALUE)
+def test_ping_success(mock_metrics_header_value):
+ request = make_request("", headers=_metadata._METADATA_HEADERS)
+
+ assert _metadata.ping(request)
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_IP_ROOT,
+ headers=MDS_PING_REQUEST_HEADER,
+ timeout=_metadata._METADATA_DEFAULT_TIMEOUT,
+ )
+
+
+@mock.patch("google.auth.metrics.mds_ping", return_value=MDS_PING_METRICS_HEADER_VALUE)
+def test_ping_success_retry(mock_metrics_header_value):
+ request = make_request("", headers=_metadata._METADATA_HEADERS, retry=True)
+
+ assert _metadata.ping(request)
+
+ request.assert_called_with(
+ method="GET",
+ url=_metadata._METADATA_IP_ROOT,
+ headers=MDS_PING_REQUEST_HEADER,
+ timeout=_metadata._METADATA_DEFAULT_TIMEOUT,
+ )
+ assert request.call_count == 2
+
+
+def test_ping_failure_bad_flavor():
+ request = make_request("", headers={_metadata._METADATA_FLAVOR_HEADER: "meep"})
+
+ assert not _metadata.ping(request)
+
+
+def test_ping_failure_connection_failed():
+ request = make_request("")
+ request.side_effect = exceptions.TransportError()
+
+ assert not _metadata.ping(request)
+
+
+@mock.patch("google.auth.metrics.mds_ping", return_value=MDS_PING_METRICS_HEADER_VALUE)
+def _test_ping_success_custom_root(mock_metrics_header_value):
+ request = make_request("", headers=_metadata._METADATA_HEADERS)
+
+ fake_ip = "1.2.3.4"
+ os.environ[environment_vars.GCE_METADATA_IP] = fake_ip
+ importlib.reload(_metadata)
+
+ try:
+ assert _metadata.ping(request)
+ finally:
+ del os.environ[environment_vars.GCE_METADATA_IP]
+ importlib.reload(_metadata)
+
+ request.assert_called_once_with(
+ method="GET",
+ url="http://" + fake_ip,
+ headers=MDS_PING_REQUEST_HEADER,
+ timeout=_metadata._METADATA_DEFAULT_TIMEOUT,
+ )
+
+
+def test_get_success_json():
+ key, value = "foo", "bar"
+
+ data = json.dumps({key: value})
+ request = make_request(data, headers={"content-type": "application/json"})
+
+ result = _metadata.get(request, PATH)
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH,
+ headers=_metadata._METADATA_HEADERS,
+ )
+ assert result[key] == value
+
+
+def test_get_success_retry():
+ key, value = "foo", "bar"
+
+ data = json.dumps({key: value})
+ request = make_request(
+ data, headers={"content-type": "application/json"}, retry=True
+ )
+
+ result = _metadata.get(request, PATH)
+
+ request.assert_called_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH,
+ headers=_metadata._METADATA_HEADERS,
+ )
+ assert request.call_count == 2
+ assert result[key] == value
+
+
+def test_get_success_text():
+ data = "foobar"
+ request = make_request(data, headers={"content-type": "text/plain"})
+
+ result = _metadata.get(request, PATH)
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH,
+ headers=_metadata._METADATA_HEADERS,
+ )
+ assert result == data
+
+
+def test_get_success_params():
+ data = "foobar"
+ request = make_request(data, headers={"content-type": "text/plain"})
+ params = {"recursive": "true"}
+
+ result = _metadata.get(request, PATH, params=params)
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH + "?recursive=true",
+ headers=_metadata._METADATA_HEADERS,
+ )
+ assert result == data
+
+
+def test_get_success_recursive_and_params():
+ data = "foobar"
+ request = make_request(data, headers={"content-type": "text/plain"})
+ params = {"recursive": "false"}
+ result = _metadata.get(request, PATH, recursive=True, params=params)
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH + "?recursive=true",
+ headers=_metadata._METADATA_HEADERS,
+ )
+ assert result == data
+
+
+def test_get_success_recursive():
+ data = "foobar"
+ request = make_request(data, headers={"content-type": "text/plain"})
+
+ result = _metadata.get(request, PATH, recursive=True)
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH + "?recursive=true",
+ headers=_metadata._METADATA_HEADERS,
+ )
+ assert result == data
+
+
+def _test_get_success_custom_root_new_variable():
+ request = make_request("{}", headers={"content-type": "application/json"})
+
+ fake_root = "another.metadata.service"
+ os.environ[environment_vars.GCE_METADATA_HOST] = fake_root
+ importlib.reload(_metadata)
+
+ try:
+ _metadata.get(request, PATH)
+ finally:
+ del os.environ[environment_vars.GCE_METADATA_HOST]
+ importlib.reload(_metadata)
+
+ request.assert_called_once_with(
+ method="GET",
+ url="http://{}/computeMetadata/v1/{}".format(fake_root, PATH),
+ headers=_metadata._METADATA_HEADERS,
+ )
+
+
+def _test_get_success_custom_root_old_variable():
+ request = make_request("{}", headers={"content-type": "application/json"})
+
+ fake_root = "another.metadata.service"
+ os.environ[environment_vars.GCE_METADATA_ROOT] = fake_root
+ importlib.reload(_metadata)
+
+ try:
+ _metadata.get(request, PATH)
+ finally:
+ del os.environ[environment_vars.GCE_METADATA_ROOT]
+ importlib.reload(_metadata)
+
+ request.assert_called_once_with(
+ method="GET",
+ url="http://{}/computeMetadata/v1/{}".format(fake_root, PATH),
+ headers=_metadata._METADATA_HEADERS,
+ )
+
+
+def test_get_failure():
+ request = make_request("Metadata error", status=http_client.NOT_FOUND)
+
+ with pytest.raises(exceptions.TransportError) as excinfo:
+ _metadata.get(request, PATH)
+
+ assert excinfo.match(r"Metadata error")
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH,
+ headers=_metadata._METADATA_HEADERS,
+ )
+
+
+def test_get_failure_connection_failed():
+ request = make_request("")
+ request.side_effect = exceptions.TransportError()
+
+ with pytest.raises(exceptions.TransportError) as excinfo:
+ _metadata.get(request, PATH)
+
+ assert excinfo.match(r"Compute Engine Metadata server unavailable")
+
+ request.assert_called_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH,
+ headers=_metadata._METADATA_HEADERS,
+ )
+ assert request.call_count == 5
+
+
+def test_get_failure_bad_json():
+ request = make_request("{", headers={"content-type": "application/json"})
+
+ with pytest.raises(exceptions.TransportError) as excinfo:
+ _metadata.get(request, PATH)
+
+ assert excinfo.match(r"invalid JSON")
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH,
+ headers=_metadata._METADATA_HEADERS,
+ )
+
+
+def test_get_project_id():
+ project = "example-project"
+ request = make_request(project, headers={"content-type": "text/plain"})
+
+ project_id = _metadata.get_project_id(request)
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + "project/project-id",
+ headers=_metadata._METADATA_HEADERS,
+ )
+ assert project_id == project
+
+
+@mock.patch(
+ "google.auth.metrics.token_request_access_token_mds",
+ return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+)
+@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_get_service_account_token(utcnow, mock_metrics_header_value):
+ ttl = 500
+ request = make_request(
+ json.dumps({"access_token": "token", "expires_in": ttl}),
+ headers={"content-type": "application/json"},
+ )
+
+ token, expiry = _metadata.get_service_account_token(request)
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH + "/token",
+ headers={
+ "metadata-flavor": "Google",
+ "x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ },
+ )
+ assert token == "token"
+ assert expiry == utcnow() + datetime.timedelta(seconds=ttl)
+
+
+@mock.patch(
+ "google.auth.metrics.token_request_access_token_mds",
+ return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+)
+@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_get_service_account_token_with_scopes_list(utcnow, mock_metrics_header_value):
+ ttl = 500
+ request = make_request(
+ json.dumps({"access_token": "token", "expires_in": ttl}),
+ headers={"content-type": "application/json"},
+ )
+
+ token, expiry = _metadata.get_service_account_token(request, scopes=["foo", "bar"])
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH + "/token" + "?scopes=foo%2Cbar",
+ headers={
+ "metadata-flavor": "Google",
+ "x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ },
+ )
+ assert token == "token"
+ assert expiry == utcnow() + datetime.timedelta(seconds=ttl)
+
+
+@mock.patch(
+ "google.auth.metrics.token_request_access_token_mds",
+ return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+)
+@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_get_service_account_token_with_scopes_string(
+ utcnow, mock_metrics_header_value
+):
+ ttl = 500
+ request = make_request(
+ json.dumps({"access_token": "token", "expires_in": ttl}),
+ headers={"content-type": "application/json"},
+ )
+
+ token, expiry = _metadata.get_service_account_token(request, scopes="foo,bar")
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH + "/token" + "?scopes=foo%2Cbar",
+ headers={
+ "metadata-flavor": "Google",
+ "x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ },
+ )
+ assert token == "token"
+ assert expiry == utcnow() + datetime.timedelta(seconds=ttl)
+
+
+def test_get_service_account_info():
+ key, value = "foo", "bar"
+ request = make_request(
+ json.dumps({key: value}), headers={"content-type": "application/json"}
+ )
+
+ info = _metadata.get_service_account_info(request)
+
+ request.assert_called_once_with(
+ method="GET",
+ url=_metadata._METADATA_ROOT + PATH + "/?recursive=true",
+ headers=_metadata._METADATA_HEADERS,
+ )
+
+ assert info[key] == value
diff --git a/contrib/python/google-auth/py3/tests/compute_engine/test_credentials.py b/contrib/python/google-auth/py3/tests/compute_engine/test_credentials.py
new file mode 100644
index 0000000000..507fea9fcc
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/compute_engine/test_credentials.py
@@ -0,0 +1,875 @@
+# Copyright 2016 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import base64
+import datetime
+
+import mock
+import pytest # type: ignore
+import responses # type: ignore
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth import transport
+from google.auth.compute_engine import credentials
+from google.auth.transport import requests
+
+SAMPLE_ID_TOKEN_EXP = 1584393400
+
+# header: {"alg": "RS256", "typ": "JWT", "kid": "1"}
+# payload: {"iss": "issuer", "iat": 1584393348, "sub": "subject",
+# "exp": 1584393400,"aud": "audience"}
+SAMPLE_ID_TOKEN = (
+ b"eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJraWQiOiAiMSJ9."
+ b"eyJpc3MiOiAiaXNzdWVyIiwgImlhdCI6IDE1ODQzOTMzNDgsICJzdWIiO"
+ b"iAic3ViamVjdCIsICJleHAiOiAxNTg0MzkzNDAwLCAiYXVkIjogImF1ZG"
+ b"llbmNlIn0."
+ b"OquNjHKhTmlgCk361omRo18F_uY-7y0f_AmLbzW062Q1Zr61HAwHYP5FM"
+ b"316CK4_0cH8MUNGASsvZc3VqXAqub6PUTfhemH8pFEwBdAdG0LhrNkU0H"
+ b"WN1YpT55IiQ31esLdL5q-qDsOPpNZJUti1y1lAreM5nIn2srdWzGXGs4i"
+ b"TRQsn0XkNUCL4RErpciXmjfhMrPkcAjKA-mXQm2fa4jmTlEZFqFmUlym1"
+ b"ozJ0yf5grjN6AslN4OGvAv1pS-_Ko_pGBS6IQtSBC6vVKCUuBfaqNjykg"
+ b"bsxbLa6Fp0SYeYwO8ifEnkRvasVpc1WTQqfRB2JCj5pTBDzJpIpFCMmnQ"
+)
+
+ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
+ "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/mds"
+)
+ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
+ "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/mds"
+)
+
+
+class TestCredentials(object):
+ credentials = None
+
+ @pytest.fixture(autouse=True)
+ def credentials_fixture(self):
+ self.credentials = credentials.Credentials()
+
+ def test_default_state(self):
+ assert not self.credentials.valid
+ # Expiration hasn't been set yet
+ assert not self.credentials.expired
+ # Scopes are needed
+ assert self.credentials.requires_scopes
+ # Service account email hasn't been populated
+ assert self.credentials.service_account_email == "default"
+ # No quota project
+ assert not self.credentials._quota_project_id
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ def test_refresh_success(self, get, utcnow):
+ get.side_effect = [
+ {
+ # First request is for sevice account info.
+ "email": "service-account@example.com",
+ "scopes": ["one", "two"],
+ },
+ {
+ # Second request is for the token.
+ "access_token": "token",
+ "expires_in": 500,
+ },
+ ]
+
+ # Refresh credentials
+ self.credentials.refresh(None)
+
+ # Check that the credentials have the token and proper expiration
+ assert self.credentials.token == "token"
+ assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500))
+
+ # Check the credential info
+ assert self.credentials.service_account_email == "service-account@example.com"
+ assert self.credentials._scopes == ["one", "two"]
+
+ # Check that the credentials are valid (have a token and are not
+ # expired)
+ assert self.credentials.valid
+
+ @mock.patch(
+ "google.auth.metrics.token_request_access_token_mds",
+ return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ )
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ def test_refresh_success_with_scopes(self, get, utcnow, mock_metrics_header_value):
+ get.side_effect = [
+ {
+ # First request is for sevice account info.
+ "email": "service-account@example.com",
+ "scopes": ["one", "two"],
+ },
+ {
+ # Second request is for the token.
+ "access_token": "token",
+ "expires_in": 500,
+ },
+ ]
+
+ # Refresh credentials
+ scopes = ["three", "four"]
+ self.credentials = self.credentials.with_scopes(scopes)
+ self.credentials.refresh(None)
+
+ # Check that the credentials have the token and proper expiration
+ assert self.credentials.token == "token"
+ assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500))
+
+ # Check the credential info
+ assert self.credentials.service_account_email == "service-account@example.com"
+ assert self.credentials._scopes == scopes
+
+ # Check that the credentials are valid (have a token and are not
+ # expired)
+ assert self.credentials.valid
+
+ kwargs = get.call_args[1]
+ assert kwargs["params"] == {"scopes": "three,four"}
+ assert kwargs["headers"] == {
+ "x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE
+ }
+
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ def test_refresh_error(self, get):
+ get.side_effect = exceptions.TransportError("http error")
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ self.credentials.refresh(None)
+
+ assert excinfo.match(r"http error")
+
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ def test_before_request_refreshes(self, get):
+ get.side_effect = [
+ {
+ # First request is for sevice account info.
+ "email": "service-account@example.com",
+ "scopes": "one two",
+ },
+ {
+ # Second request is for the token.
+ "access_token": "token",
+ "expires_in": 500,
+ },
+ ]
+
+ # Credentials should start as invalid
+ assert not self.credentials.valid
+
+ # before_request should cause a refresh
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+ # The refresh endpoint should've been called.
+ assert get.called
+
+ # Credentials should now be valid.
+ assert self.credentials.valid
+
+ def test_with_quota_project(self):
+ quota_project_creds = self.credentials.with_quota_project("project-foo")
+
+ assert quota_project_creds._quota_project_id == "project-foo"
+
+ def test_with_scopes(self):
+ assert self.credentials._scopes is None
+
+ scopes = ["one", "two"]
+ self.credentials = self.credentials.with_scopes(scopes)
+
+ assert self.credentials._scopes == scopes
+
+ def test_token_usage_metrics(self):
+ self.credentials.token = "token"
+ self.credentials.expiry = None
+
+ headers = {}
+ self.credentials.before_request(mock.Mock(), None, None, headers)
+ assert headers["authorization"] == "Bearer token"
+ assert headers["x-goog-api-client"] == "cred-type/mds"
+
+
+class TestIDTokenCredentials(object):
+ credentials = None
+
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ def test_default_state(self, get):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scope": ["one", "two"]}
+ ]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request, target_audience="https://example.com"
+ )
+
+ assert not self.credentials.valid
+ # Expiration hasn't been set yet
+ assert not self.credentials.expired
+ # Service account email hasn't been populated
+ assert self.credentials.service_account_email == "service-account@example.com"
+ # Signer is initialized
+ assert self.credentials.signer
+ assert self.credentials.signer_email == "service-account@example.com"
+ # No quota project
+ assert not self.credentials._quota_project_id
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ def test_make_authorization_grant_assertion(self, sign, get, utcnow):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scopes": ["one", "two"]}
+ ]
+ sign.side_effect = [b"signature"]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request, target_audience="https://audience.com"
+ )
+
+ # Generate authorization grant:
+ token = self.credentials._make_authorization_grant_assertion()
+ payload = jwt.decode(token, verify=False)
+
+ # The JWT token signature is 'signature' encoded in base 64:
+ assert token.endswith(b".c2lnbmF0dXJl")
+
+ # Check that the credentials have the token and proper expiration
+ assert payload == {
+ "aud": "https://www.googleapis.com/oauth2/v4/token",
+ "exp": 3600,
+ "iat": 0,
+ "iss": "service-account@example.com",
+ "target_audience": "https://audience.com",
+ }
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ def test_with_service_account(self, sign, get, utcnow):
+ sign.side_effect = [b"signature"]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request,
+ target_audience="https://audience.com",
+ service_account_email="service-account@other.com",
+ )
+
+ # Generate authorization grant:
+ token = self.credentials._make_authorization_grant_assertion()
+ payload = jwt.decode(token, verify=False)
+
+ # The JWT token signature is 'signature' encoded in base 64:
+ assert token.endswith(b".c2lnbmF0dXJl")
+
+ # Check that the credentials have the token and proper expiration
+ assert payload == {
+ "aud": "https://www.googleapis.com/oauth2/v4/token",
+ "exp": 3600,
+ "iat": 0,
+ "iss": "service-account@other.com",
+ "target_audience": "https://audience.com",
+ }
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ def test_additional_claims(self, sign, get, utcnow):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scopes": ["one", "two"]}
+ ]
+ sign.side_effect = [b"signature"]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request,
+ target_audience="https://audience.com",
+ additional_claims={"foo": "bar"},
+ )
+
+ # Generate authorization grant:
+ token = self.credentials._make_authorization_grant_assertion()
+ payload = jwt.decode(token, verify=False)
+
+ # The JWT token signature is 'signature' encoded in base 64:
+ assert token.endswith(b".c2lnbmF0dXJl")
+
+ # Check that the credentials have the token and proper expiration
+ assert payload == {
+ "aud": "https://www.googleapis.com/oauth2/v4/token",
+ "exp": 3600,
+ "iat": 0,
+ "iss": "service-account@example.com",
+ "target_audience": "https://audience.com",
+ "foo": "bar",
+ }
+
+ def test_token_uri(self):
+ request = mock.create_autospec(transport.Request, instance=True)
+
+ self.credentials = credentials.IDTokenCredentials(
+ request=request,
+ signer=mock.Mock(),
+ service_account_email="foo@example.com",
+ target_audience="https://audience.com",
+ )
+ assert self.credentials._token_uri == credentials._DEFAULT_TOKEN_URI
+
+ self.credentials = credentials.IDTokenCredentials(
+ request=request,
+ signer=mock.Mock(),
+ service_account_email="foo@example.com",
+ target_audience="https://audience.com",
+ token_uri="https://example.com/token",
+ )
+ assert self.credentials._token_uri == "https://example.com/token"
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ def test_with_target_audience(self, sign, get, utcnow):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scopes": ["one", "two"]}
+ ]
+ sign.side_effect = [b"signature"]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request, target_audience="https://audience.com"
+ )
+ self.credentials = self.credentials.with_target_audience("https://actually.not")
+
+ # Generate authorization grant:
+ token = self.credentials._make_authorization_grant_assertion()
+ payload = jwt.decode(token, verify=False)
+
+ # The JWT token signature is 'signature' encoded in base 64:
+ assert token.endswith(b".c2lnbmF0dXJl")
+
+ # Check that the credentials have the token and proper expiration
+ assert payload == {
+ "aud": "https://www.googleapis.com/oauth2/v4/token",
+ "exp": 3600,
+ "iat": 0,
+ "iss": "service-account@example.com",
+ "target_audience": "https://actually.not",
+ }
+
+ # Check that the signer have been initialized with a Request object
+ assert isinstance(self.credentials._signer._request, transport.Request)
+
+ @responses.activate
+ def test_with_target_audience_integration(self):
+ """ Test that it is possible to refresh credentials
+ generated from `with_target_audience`.
+
+ Instead of mocking the methods, the HTTP responses
+ have been mocked.
+ """
+
+ # mock information about credentials
+ responses.add(
+ responses.GET,
+ "http://metadata.google.internal/computeMetadata/v1/instance/"
+ "service-accounts/default/?recursive=true",
+ status=200,
+ content_type="application/json",
+ json={
+ "scopes": "email",
+ "email": "service-account@example.com",
+ "aliases": ["default"],
+ },
+ )
+
+ # mock token for credentials
+ responses.add(
+ responses.GET,
+ "http://metadata.google.internal/computeMetadata/v1/instance/"
+ "service-accounts/service-account@example.com/token",
+ status=200,
+ content_type="application/json",
+ json={
+ "access_token": "some-token",
+ "expires_in": 3210,
+ "token_type": "Bearer",
+ },
+ )
+
+ # mock sign blob endpoint
+ signature = base64.b64encode(b"some-signature").decode("utf-8")
+ responses.add(
+ responses.POST,
+ "https://iamcredentials.googleapis.com/v1/projects/-/"
+ "serviceAccounts/service-account@example.com:signBlob?alt=json",
+ status=200,
+ content_type="application/json",
+ json={"keyId": "some-key-id", "signedBlob": signature},
+ )
+
+ id_token = "{}.{}.{}".format(
+ base64.b64encode(b'{"some":"some"}').decode("utf-8"),
+ base64.b64encode(b'{"exp": 3210}').decode("utf-8"),
+ base64.b64encode(b"token").decode("utf-8"),
+ )
+
+ # mock id token endpoint
+ responses.add(
+ responses.POST,
+ "https://www.googleapis.com/oauth2/v4/token",
+ status=200,
+ content_type="application/json",
+ json={"id_token": id_token, "expiry": 3210},
+ )
+
+ self.credentials = credentials.IDTokenCredentials(
+ request=requests.Request(),
+ service_account_email="service-account@example.com",
+ target_audience="https://audience.com",
+ )
+
+ self.credentials = self.credentials.with_target_audience("https://actually.not")
+
+ self.credentials.refresh(requests.Request())
+
+ assert self.credentials.token is not None
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ def test_with_quota_project(self, sign, get, utcnow):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scopes": ["one", "two"]}
+ ]
+ sign.side_effect = [b"signature"]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request, target_audience="https://audience.com"
+ )
+ self.credentials = self.credentials.with_quota_project("project-foo")
+
+ assert self.credentials._quota_project_id == "project-foo"
+
+ # Generate authorization grant:
+ token = self.credentials._make_authorization_grant_assertion()
+ payload = jwt.decode(token, verify=False)
+
+ # The JWT token signature is 'signature' encoded in base 64:
+ assert token.endswith(b".c2lnbmF0dXJl")
+
+ # Check that the credentials have the token and proper expiration
+ assert payload == {
+ "aud": "https://www.googleapis.com/oauth2/v4/token",
+ "exp": 3600,
+ "iat": 0,
+ "iss": "service-account@example.com",
+ "target_audience": "https://audience.com",
+ }
+
+ # Check that the signer have been initialized with a Request object
+ assert isinstance(self.credentials._signer._request, transport.Request)
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ def test_with_token_uri(self, sign, get, utcnow):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scopes": ["one", "two"]}
+ ]
+ sign.side_effect = [b"signature"]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request,
+ target_audience="https://audience.com",
+ token_uri="http://xyz.com",
+ )
+ assert self.credentials._token_uri == "http://xyz.com"
+ creds_with_token_uri = self.credentials.with_token_uri("http://example.com")
+ assert creds_with_token_uri._token_uri == "http://example.com"
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ def test_with_token_uri_exception(self, sign, get, utcnow):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scopes": ["one", "two"]}
+ ]
+ sign.side_effect = [b"signature"]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request,
+ target_audience="https://audience.com",
+ use_metadata_identity_endpoint=True,
+ )
+ assert self.credentials._token_uri is None
+ with pytest.raises(ValueError):
+ self.credentials.with_token_uri("http://example.com")
+
+ @responses.activate
+ def test_with_quota_project_integration(self):
+ """ Test that it is possible to refresh credentials
+ generated from `with_quota_project`.
+
+ Instead of mocking the methods, the HTTP responses
+ have been mocked.
+ """
+
+ # mock information about credentials
+ responses.add(
+ responses.GET,
+ "http://metadata.google.internal/computeMetadata/v1/instance/"
+ "service-accounts/default/?recursive=true",
+ status=200,
+ content_type="application/json",
+ json={
+ "scopes": "email",
+ "email": "service-account@example.com",
+ "aliases": ["default"],
+ },
+ )
+
+ # mock token for credentials
+ responses.add(
+ responses.GET,
+ "http://metadata.google.internal/computeMetadata/v1/instance/"
+ "service-accounts/service-account@example.com/token",
+ status=200,
+ content_type="application/json",
+ json={
+ "access_token": "some-token",
+ "expires_in": 3210,
+ "token_type": "Bearer",
+ },
+ )
+
+ # mock sign blob endpoint
+ signature = base64.b64encode(b"some-signature").decode("utf-8")
+ responses.add(
+ responses.POST,
+ "https://iamcredentials.googleapis.com/v1/projects/-/"
+ "serviceAccounts/service-account@example.com:signBlob?alt=json",
+ status=200,
+ content_type="application/json",
+ json={"keyId": "some-key-id", "signedBlob": signature},
+ )
+
+ id_token = "{}.{}.{}".format(
+ base64.b64encode(b'{"some":"some"}').decode("utf-8"),
+ base64.b64encode(b'{"exp": 3210}').decode("utf-8"),
+ base64.b64encode(b"token").decode("utf-8"),
+ )
+
+ # mock id token endpoint
+ responses.add(
+ responses.POST,
+ "https://www.googleapis.com/oauth2/v4/token",
+ status=200,
+ content_type="application/json",
+ json={"id_token": id_token, "expiry": 3210},
+ )
+
+ self.credentials = credentials.IDTokenCredentials(
+ request=requests.Request(),
+ service_account_email="service-account@example.com",
+ target_audience="https://audience.com",
+ )
+
+ self.credentials = self.credentials.with_quota_project("project-foo")
+
+ self.credentials.refresh(requests.Request())
+
+ assert self.credentials.token is not None
+ assert self.credentials._quota_project_id == "project-foo"
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
+ def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scopes": ["one", "two"]}
+ ]
+ sign.side_effect = [b"signature"]
+ id_token_jwt_grant.side_effect = [
+ ("idtoken", datetime.datetime.utcfromtimestamp(3600), {})
+ ]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request, target_audience="https://audience.com"
+ )
+
+ # Refresh credentials
+ self.credentials.refresh(None)
+
+ # Check that the credentials have the token and proper expiration
+ assert self.credentials.token == "idtoken"
+ assert self.credentials.expiry == (datetime.datetime.utcfromtimestamp(3600))
+
+ # Check the credential info
+ assert self.credentials.service_account_email == "service-account@example.com"
+
+ # Check that the credentials are valid (have a token and are not
+ # expired)
+ assert self.credentials.valid
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ def test_refresh_error(self, sign, get, utcnow):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scopes": ["one", "two"]}
+ ]
+ sign.side_effect = [b"signature"]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ response = mock.Mock()
+ response.data = b'{"error": "http error"}'
+ response.status = 404 # Throw a 404 so the request is not retried.
+ request.side_effect = [response]
+
+ self.credentials = credentials.IDTokenCredentials(
+ request=request, target_audience="https://audience.com"
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ self.credentials.refresh(request)
+
+ assert excinfo.match(r"http error")
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
+ def test_before_request_refreshes(self, id_token_jwt_grant, sign, get, utcnow):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scopes": "one two"}
+ ]
+ sign.side_effect = [b"signature"]
+ id_token_jwt_grant.side_effect = [
+ ("idtoken", datetime.datetime.utcfromtimestamp(3600), {})
+ ]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request, target_audience="https://audience.com"
+ )
+
+ # Credentials should start as invalid
+ assert not self.credentials.valid
+
+ # before_request should cause a refresh
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+ # The refresh endpoint should've been called.
+ assert get.called
+
+ # Credentials should now be valid.
+ assert self.credentials.valid
+
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ def test_sign_bytes(self, sign, get):
+ get.side_effect = [
+ {"email": "service-account@example.com", "scopes": ["one", "two"]}
+ ]
+ sign.side_effect = [b"signature"]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ response = mock.Mock()
+ response.data = b'{"signature": "c2lnbmF0dXJl"}'
+ response.status = 200
+ request.side_effect = [response]
+
+ self.credentials = credentials.IDTokenCredentials(
+ request=request, target_audience="https://audience.com"
+ )
+
+ # Generate authorization grant:
+ signature = self.credentials.sign_bytes(b"some bytes")
+
+ # The JWT token signature is 'signature' encoded in base 64:
+ assert signature == b"signature"
+
+ @mock.patch(
+ "google.auth.metrics.token_request_id_token_mds",
+ return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ )
+ @mock.patch(
+ "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ def test_get_id_token_from_metadata(
+ self, get, get_service_account_info, mock_metrics_header_value
+ ):
+ get.return_value = SAMPLE_ID_TOKEN
+ get_service_account_info.return_value = {"email": "foo@example.com"}
+
+ cred = credentials.IDTokenCredentials(
+ mock.Mock(), "audience", use_metadata_identity_endpoint=True
+ )
+ cred.refresh(request=mock.Mock())
+
+ assert get.call_args.kwargs["headers"] == {
+ "x-goog-api-client": ID_TOKEN_REQUEST_METRICS_HEADER_VALUE
+ }
+
+ assert cred.token == SAMPLE_ID_TOKEN
+ assert cred.expiry == datetime.datetime.utcfromtimestamp(SAMPLE_ID_TOKEN_EXP)
+ assert cred._use_metadata_identity_endpoint
+ assert cred._signer is None
+ assert cred._token_uri is None
+ assert cred._service_account_email == "foo@example.com"
+ assert cred._target_audience == "audience"
+ with pytest.raises(ValueError):
+ cred.sign_bytes(b"bytes")
+
+ @mock.patch(
+ "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+ )
+ def test_with_target_audience_for_metadata(self, get_service_account_info):
+ get_service_account_info.return_value = {"email": "foo@example.com"}
+
+ cred = credentials.IDTokenCredentials(
+ mock.Mock(), "audience", use_metadata_identity_endpoint=True
+ )
+ cred = cred.with_target_audience("new_audience")
+
+ assert cred._target_audience == "new_audience"
+ assert cred._use_metadata_identity_endpoint
+ assert cred._signer is None
+ assert cred._token_uri is None
+ assert cred._service_account_email == "foo@example.com"
+
+ @mock.patch(
+ "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+ )
+ def test_id_token_with_quota_project(self, get_service_account_info):
+ get_service_account_info.return_value = {"email": "foo@example.com"}
+
+ cred = credentials.IDTokenCredentials(
+ mock.Mock(), "audience", use_metadata_identity_endpoint=True
+ )
+ cred = cred.with_quota_project("project-foo")
+
+ assert cred._quota_project_id == "project-foo"
+ assert cred._use_metadata_identity_endpoint
+ assert cred._signer is None
+ assert cred._token_uri is None
+ assert cred._service_account_email == "foo@example.com"
+
+ @mock.patch(
+ "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ def test_invalid_id_token_from_metadata(self, get, get_service_account_info):
+ get.return_value = "invalid_id_token"
+ get_service_account_info.return_value = {"email": "foo@example.com"}
+
+ cred = credentials.IDTokenCredentials(
+ mock.Mock(), "audience", use_metadata_identity_endpoint=True
+ )
+
+ with pytest.raises(ValueError):
+ cred.refresh(request=mock.Mock())
+
+ @mock.patch(
+ "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ def test_transport_error_from_metadata(self, get, get_service_account_info):
+ get.side_effect = exceptions.TransportError("transport error")
+ get_service_account_info.return_value = {"email": "foo@example.com"}
+
+ cred = credentials.IDTokenCredentials(
+ mock.Mock(), "audience", use_metadata_identity_endpoint=True
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ cred.refresh(request=mock.Mock())
+ assert excinfo.match(r"transport error")
+
+ def test_get_id_token_from_metadata_constructor(self):
+ with pytest.raises(ValueError):
+ credentials.IDTokenCredentials(
+ mock.Mock(),
+ "audience",
+ use_metadata_identity_endpoint=True,
+ token_uri="token_uri",
+ )
+ with pytest.raises(ValueError):
+ credentials.IDTokenCredentials(
+ mock.Mock(),
+ "audience",
+ use_metadata_identity_endpoint=True,
+ signer=mock.Mock(),
+ )
+ with pytest.raises(ValueError):
+ credentials.IDTokenCredentials(
+ mock.Mock(),
+ "audience",
+ use_metadata_identity_endpoint=True,
+ additional_claims={"key", "value"},
+ )
+ with pytest.raises(ValueError):
+ credentials.IDTokenCredentials(
+ mock.Mock(),
+ "audience",
+ use_metadata_identity_endpoint=True,
+ service_account_email="foo@example.com",
+ )
diff --git a/contrib/python/google-auth/py3/tests/conftest.py b/contrib/python/google-auth/py3/tests/conftest.py
new file mode 100644
index 0000000000..08896b0f82
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/conftest.py
@@ -0,0 +1,45 @@
+# Copyright 2016 Google LLC
+#
+# 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 os
+import sys
+
+import mock
+import pytest # type: ignore
+
+
+def pytest_configure():
+ """Load public certificate and private key."""
+ import __res
+ pytest.private_key_bytes = __res.find("data/privatekey.pem")
+ pytest.public_cert_bytes = __res.find("data/public_cert.pem")
+
+
+@pytest.fixture
+def mock_non_existent_module(monkeypatch):
+ """Mocks a non-existing module in sys.modules.
+
+ Additionally mocks any non-existing modules specified in the dotted path.
+ """
+
+ def _mock_non_existent_module(path):
+ parts = path.split(".")
+ partial = []
+ for part in parts:
+ partial.append(part)
+ current_module = ".".join(partial)
+ if current_module not in sys.modules:
+ monkeypatch.setitem(sys.modules, current_module, mock.MagicMock())
+
+ return _mock_non_existent_module
diff --git a/contrib/python/google-auth/py3/tests/crypt/__init__.py b/contrib/python/google-auth/py3/tests/crypt/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/crypt/__init__.py
diff --git a/contrib/python/google-auth/py3/tests/crypt/test__cryptography_rsa.py b/contrib/python/google-auth/py3/tests/crypt/test__cryptography_rsa.py
new file mode 100644
index 0000000000..d19154b61b
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/crypt/test__cryptography_rsa.py
@@ -0,0 +1,162 @@
+# Copyright 2016 Google LLC
+#
+# 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 json
+import os
+
+from cryptography.hazmat.primitives.asymmetric import rsa
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth.crypt import _cryptography_rsa
+from google.auth.crypt import base
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+
+# To generate privatekey.pem, privatekey.pub, and public_cert.pem:
+# $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \
+# > -keyout privatekey.pem
+# $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+ PRIVATE_KEY_BYTES = fh.read()
+ PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES
+
+with open(os.path.join(DATA_DIR, "privatekey.pub"), "rb") as fh:
+ PUBLIC_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+ PUBLIC_CERT_BYTES = fh.read()
+
+# To generate pem_from_pkcs12.pem and privatekey.p12:
+# $ openssl pkcs12 -export -out privatekey.p12 -inkey privatekey.pem \
+# > -in public_cert.pem
+# $ openssl pkcs12 -in privatekey.p12 -nocerts -nodes \
+# > -out pem_from_pkcs12.pem
+
+with open(os.path.join(DATA_DIR, "pem_from_pkcs12.pem"), "rb") as fh:
+ PKCS8_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "privatekey.p12"), "rb") as fh:
+ PKCS12_KEY_BYTES = fh.read()
+
+# The service account JSON file can be generated from the Google Cloud Console.
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh:
+ SERVICE_ACCOUNT_INFO = json.load(fh)
+
+
+class TestRSAVerifier(object):
+ def test_verify_success(self):
+ to_sign = b"foo"
+ signer = _cryptography_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES)
+ actual_signature = signer.sign(to_sign)
+
+ verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+ assert verifier.verify(to_sign, actual_signature)
+
+ def test_verify_unicode_success(self):
+ to_sign = u"foo"
+ signer = _cryptography_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES)
+ actual_signature = signer.sign(to_sign)
+
+ verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+ assert verifier.verify(to_sign, actual_signature)
+
+ def test_verify_failure(self):
+ verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+ bad_signature1 = b""
+ assert not verifier.verify(b"foo", bad_signature1)
+ bad_signature2 = b"a"
+ assert not verifier.verify(b"foo", bad_signature2)
+
+ def test_from_string_pub_key(self):
+ verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+ assert isinstance(verifier, _cryptography_rsa.RSAVerifier)
+ assert isinstance(verifier._pubkey, rsa.RSAPublicKey)
+
+ def test_from_string_pub_key_unicode(self):
+ public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES)
+ verifier = _cryptography_rsa.RSAVerifier.from_string(public_key)
+ assert isinstance(verifier, _cryptography_rsa.RSAVerifier)
+ assert isinstance(verifier._pubkey, rsa.RSAPublicKey)
+
+ def test_from_string_pub_cert(self):
+ verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_CERT_BYTES)
+ assert isinstance(verifier, _cryptography_rsa.RSAVerifier)
+ assert isinstance(verifier._pubkey, rsa.RSAPublicKey)
+
+ def test_from_string_pub_cert_unicode(self):
+ public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES)
+ verifier = _cryptography_rsa.RSAVerifier.from_string(public_cert)
+ assert isinstance(verifier, _cryptography_rsa.RSAVerifier)
+ assert isinstance(verifier._pubkey, rsa.RSAPublicKey)
+
+
+class TestRSASigner(object):
+ def test_from_string_pkcs1(self):
+ signer = _cryptography_rsa.RSASigner.from_string(PKCS1_KEY_BYTES)
+ assert isinstance(signer, _cryptography_rsa.RSASigner)
+ assert isinstance(signer._key, rsa.RSAPrivateKey)
+
+ def test_from_string_pkcs1_unicode(self):
+ key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES)
+ signer = _cryptography_rsa.RSASigner.from_string(key_bytes)
+ assert isinstance(signer, _cryptography_rsa.RSASigner)
+ assert isinstance(signer._key, rsa.RSAPrivateKey)
+
+ def test_from_string_pkcs8(self):
+ signer = _cryptography_rsa.RSASigner.from_string(PKCS8_KEY_BYTES)
+ assert isinstance(signer, _cryptography_rsa.RSASigner)
+ assert isinstance(signer._key, rsa.RSAPrivateKey)
+
+ def test_from_string_pkcs8_unicode(self):
+ key_bytes = _helpers.from_bytes(PKCS8_KEY_BYTES)
+ signer = _cryptography_rsa.RSASigner.from_string(key_bytes)
+ assert isinstance(signer, _cryptography_rsa.RSASigner)
+ assert isinstance(signer._key, rsa.RSAPrivateKey)
+
+ def test_from_string_pkcs12(self):
+ with pytest.raises(ValueError):
+ _cryptography_rsa.RSASigner.from_string(PKCS12_KEY_BYTES)
+
+ def test_from_string_bogus_key(self):
+ key_bytes = "bogus-key"
+ with pytest.raises(ValueError):
+ _cryptography_rsa.RSASigner.from_string(key_bytes)
+
+ def test_from_service_account_info(self):
+ signer = _cryptography_rsa.RSASigner.from_service_account_info(
+ SERVICE_ACCOUNT_INFO
+ )
+
+ assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+ assert isinstance(signer._key, rsa.RSAPrivateKey)
+
+ def test_from_service_account_info_missing_key(self):
+ with pytest.raises(ValueError) as excinfo:
+ _cryptography_rsa.RSASigner.from_service_account_info({})
+
+ assert excinfo.match(base._JSON_FILE_PRIVATE_KEY)
+
+ def test_from_service_account_file(self):
+ signer = _cryptography_rsa.RSASigner.from_service_account_file(
+ SERVICE_ACCOUNT_JSON_FILE
+ )
+
+ assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+ assert isinstance(signer._key, rsa.RSAPrivateKey)
diff --git a/contrib/python/google-auth/py3/tests/crypt/test__python_rsa.py b/contrib/python/google-auth/py3/tests/crypt/test__python_rsa.py
new file mode 100644
index 0000000000..592b523d92
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/crypt/test__python_rsa.py
@@ -0,0 +1,194 @@
+# Copyright 2016 Google LLC
+#
+# 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 io
+import json
+import os
+
+import mock
+from pyasn1_modules import pem # type: ignore
+import pytest # type: ignore
+import rsa # type: ignore
+
+from google.auth import _helpers
+from google.auth.crypt import _python_rsa
+from google.auth.crypt import base
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+
+# To generate privatekey.pem, privatekey.pub, and public_cert.pem:
+# $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \
+# > -keyout privatekey.pem
+# $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+ PRIVATE_KEY_BYTES = fh.read()
+ PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES
+
+with open(os.path.join(DATA_DIR, "privatekey.pub"), "rb") as fh:
+ PUBLIC_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+ PUBLIC_CERT_BYTES = fh.read()
+
+# To generate pem_from_pkcs12.pem and privatekey.p12:
+# $ openssl pkcs12 -export -out privatekey.p12 -inkey privatekey.pem \
+# > -in public_cert.pem
+# $ openssl pkcs12 -in privatekey.p12 -nocerts -nodes \
+# > -out pem_from_pkcs12.pem
+
+with open(os.path.join(DATA_DIR, "pem_from_pkcs12.pem"), "rb") as fh:
+ PKCS8_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "privatekey.p12"), "rb") as fh:
+ PKCS12_KEY_BYTES = fh.read()
+
+# The service account JSON file can be generated from the Google Cloud Console.
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh:
+ SERVICE_ACCOUNT_INFO = json.load(fh)
+
+
+class TestRSAVerifier(object):
+ def test_verify_success(self):
+ to_sign = b"foo"
+ signer = _python_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES)
+ actual_signature = signer.sign(to_sign)
+
+ verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+ assert verifier.verify(to_sign, actual_signature)
+
+ def test_verify_unicode_success(self):
+ to_sign = u"foo"
+ signer = _python_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES)
+ actual_signature = signer.sign(to_sign)
+
+ verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+ assert verifier.verify(to_sign, actual_signature)
+
+ def test_verify_failure(self):
+ verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+ bad_signature1 = b""
+ assert not verifier.verify(b"foo", bad_signature1)
+ bad_signature2 = b"a"
+ assert not verifier.verify(b"foo", bad_signature2)
+
+ def test_from_string_pub_key(self):
+ verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+ assert isinstance(verifier, _python_rsa.RSAVerifier)
+ assert isinstance(verifier._pubkey, rsa.key.PublicKey)
+
+ def test_from_string_pub_key_unicode(self):
+ public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES)
+ verifier = _python_rsa.RSAVerifier.from_string(public_key)
+ assert isinstance(verifier, _python_rsa.RSAVerifier)
+ assert isinstance(verifier._pubkey, rsa.key.PublicKey)
+
+ def test_from_string_pub_cert(self):
+ verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_CERT_BYTES)
+ assert isinstance(verifier, _python_rsa.RSAVerifier)
+ assert isinstance(verifier._pubkey, rsa.key.PublicKey)
+
+ def test_from_string_pub_cert_unicode(self):
+ public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES)
+ verifier = _python_rsa.RSAVerifier.from_string(public_cert)
+ assert isinstance(verifier, _python_rsa.RSAVerifier)
+ assert isinstance(verifier._pubkey, rsa.key.PublicKey)
+
+ def test_from_string_pub_cert_failure(self):
+ cert_bytes = PUBLIC_CERT_BYTES
+ true_der = rsa.pem.load_pem(cert_bytes, "CERTIFICATE")
+ load_pem_patch = mock.patch(
+ "rsa.pem.load_pem", return_value=true_der + b"extra", autospec=True
+ )
+
+ with load_pem_patch as load_pem:
+ with pytest.raises(ValueError):
+ _python_rsa.RSAVerifier.from_string(cert_bytes)
+ load_pem.assert_called_once_with(cert_bytes, "CERTIFICATE")
+
+
+class TestRSASigner(object):
+ def test_from_string_pkcs1(self):
+ signer = _python_rsa.RSASigner.from_string(PKCS1_KEY_BYTES)
+ assert isinstance(signer, _python_rsa.RSASigner)
+ assert isinstance(signer._key, rsa.key.PrivateKey)
+
+ def test_from_string_pkcs1_unicode(self):
+ key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES)
+ signer = _python_rsa.RSASigner.from_string(key_bytes)
+ assert isinstance(signer, _python_rsa.RSASigner)
+ assert isinstance(signer._key, rsa.key.PrivateKey)
+
+ def test_from_string_pkcs8(self):
+ signer = _python_rsa.RSASigner.from_string(PKCS8_KEY_BYTES)
+ assert isinstance(signer, _python_rsa.RSASigner)
+ assert isinstance(signer._key, rsa.key.PrivateKey)
+
+ def test_from_string_pkcs8_extra_bytes(self):
+ key_bytes = PKCS8_KEY_BYTES
+ _, pem_bytes = pem.readPemBlocksFromFile(
+ io.StringIO(_helpers.from_bytes(key_bytes)), _python_rsa._PKCS8_MARKER
+ )
+
+ key_info, remaining = None, "extra"
+ decode_patch = mock.patch(
+ "pyasn1.codec.der.decoder.decode",
+ return_value=(key_info, remaining),
+ autospec=True,
+ )
+
+ with decode_patch as decode:
+ with pytest.raises(ValueError):
+ _python_rsa.RSASigner.from_string(key_bytes)
+ # Verify mock was called.
+ decode.assert_called_once_with(pem_bytes, asn1Spec=_python_rsa._PKCS8_SPEC)
+
+ def test_from_string_pkcs8_unicode(self):
+ key_bytes = _helpers.from_bytes(PKCS8_KEY_BYTES)
+ signer = _python_rsa.RSASigner.from_string(key_bytes)
+ assert isinstance(signer, _python_rsa.RSASigner)
+ assert isinstance(signer._key, rsa.key.PrivateKey)
+
+ def test_from_string_pkcs12(self):
+ with pytest.raises(ValueError):
+ _python_rsa.RSASigner.from_string(PKCS12_KEY_BYTES)
+
+ def test_from_string_bogus_key(self):
+ key_bytes = "bogus-key"
+ with pytest.raises(ValueError):
+ _python_rsa.RSASigner.from_string(key_bytes)
+
+ def test_from_service_account_info(self):
+ signer = _python_rsa.RSASigner.from_service_account_info(SERVICE_ACCOUNT_INFO)
+
+ assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+ assert isinstance(signer._key, rsa.key.PrivateKey)
+
+ def test_from_service_account_info_missing_key(self):
+ with pytest.raises(ValueError) as excinfo:
+ _python_rsa.RSASigner.from_service_account_info({})
+
+ assert excinfo.match(base._JSON_FILE_PRIVATE_KEY)
+
+ def test_from_service_account_file(self):
+ signer = _python_rsa.RSASigner.from_service_account_file(
+ SERVICE_ACCOUNT_JSON_FILE
+ )
+
+ assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+ assert isinstance(signer._key, rsa.key.PrivateKey)
diff --git a/contrib/python/google-auth/py3/tests/crypt/test_crypt.py b/contrib/python/google-auth/py3/tests/crypt/test_crypt.py
new file mode 100644
index 0000000000..97c2abc257
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/crypt/test_crypt.py
@@ -0,0 +1,59 @@
+# Copyright 2016 Google LLC
+#
+# 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 os
+
+from google.auth import crypt
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+
+# To generate privatekey.pem, privatekey.pub, and public_cert.pem:
+# $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \
+# > -keyout privatekey.pem
+# $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+ PRIVATE_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+ PUBLIC_CERT_BYTES = fh.read()
+
+# To generate other_cert.pem:
+# $ openssl req -new -newkey rsa:1024 -x509 -nodes -out other_cert.pem
+
+with open(os.path.join(DATA_DIR, "other_cert.pem"), "rb") as fh:
+ OTHER_CERT_BYTES = fh.read()
+
+
+def test_verify_signature():
+ to_sign = b"foo"
+ signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES)
+ signature = signer.sign(to_sign)
+
+ assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
+
+ # List of certs
+ assert crypt.verify_signature(
+ to_sign, signature, [OTHER_CERT_BYTES, PUBLIC_CERT_BYTES]
+ )
+
+
+def test_verify_signature_failure():
+ to_sign = b"foo"
+ signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES)
+ signature = signer.sign(to_sign)
+
+ assert not crypt.verify_signature(to_sign, signature, OTHER_CERT_BYTES)
diff --git a/contrib/python/google-auth/py3/tests/crypt/test_es256.py b/contrib/python/google-auth/py3/tests/crypt/test_es256.py
new file mode 100644
index 0000000000..1a43a2f01b
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/crypt/test_es256.py
@@ -0,0 +1,144 @@
+# Copyright 2016 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import json
+import os
+
+from cryptography.hazmat.primitives.asymmetric import ec
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth.crypt import base
+from google.auth.crypt import es256
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+
+# To generate es256_privatekey.pem, es256_privatekey.pub, and
+# es256_public_cert.pem:
+# $ openssl ecparam -genkey -name prime256v1 -noout -out es256_privatekey.pem
+# $ openssl ec -in es256-private-key.pem -pubout -out es256-publickey.pem
+# $ openssl req -new -x509 -key es256_privatekey.pem -out \
+# > es256_public_cert.pem
+
+with open(os.path.join(DATA_DIR, "es256_privatekey.pem"), "rb") as fh:
+ PRIVATE_KEY_BYTES = fh.read()
+ PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES
+
+with open(os.path.join(DATA_DIR, "es256_publickey.pem"), "rb") as fh:
+ PUBLIC_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "es256_public_cert.pem"), "rb") as fh:
+ PUBLIC_CERT_BYTES = fh.read()
+
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "es256_service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh:
+ SERVICE_ACCOUNT_INFO = json.load(fh)
+
+
+class TestES256Verifier(object):
+ def test_verify_success(self):
+ to_sign = b"foo"
+ signer = es256.ES256Signer.from_string(PRIVATE_KEY_BYTES)
+ actual_signature = signer.sign(to_sign)
+
+ verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES)
+ assert verifier.verify(to_sign, actual_signature)
+
+ def test_verify_unicode_success(self):
+ to_sign = u"foo"
+ signer = es256.ES256Signer.from_string(PRIVATE_KEY_BYTES)
+ actual_signature = signer.sign(to_sign)
+
+ verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES)
+ assert verifier.verify(to_sign, actual_signature)
+
+ def test_verify_failure(self):
+ verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES)
+ bad_signature1 = b""
+ assert not verifier.verify(b"foo", bad_signature1)
+ bad_signature2 = b"a"
+ assert not verifier.verify(b"foo", bad_signature2)
+
+ def test_verify_failure_with_wrong_raw_signature(self):
+ to_sign = b"foo"
+
+ # This signature has a wrong "r" value in the "(r,s)" raw signature.
+ wrong_signature = base64.urlsafe_b64decode(
+ b"m7oaRxUDeYqjZ8qiMwo0PZLTMZWKJLFQREpqce1StMIa_yXQQ-C5WgeIRHW7OqlYSDL0XbUrj_uAw9i-QhfOJQ=="
+ )
+
+ verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES)
+ assert not verifier.verify(to_sign, wrong_signature)
+
+ def test_from_string_pub_key(self):
+ verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES)
+ assert isinstance(verifier, es256.ES256Verifier)
+ assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey)
+
+ def test_from_string_pub_key_unicode(self):
+ public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES)
+ verifier = es256.ES256Verifier.from_string(public_key)
+ assert isinstance(verifier, es256.ES256Verifier)
+ assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey)
+
+ def test_from_string_pub_cert(self):
+ verifier = es256.ES256Verifier.from_string(PUBLIC_CERT_BYTES)
+ assert isinstance(verifier, es256.ES256Verifier)
+ assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey)
+
+ def test_from_string_pub_cert_unicode(self):
+ public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES)
+ verifier = es256.ES256Verifier.from_string(public_cert)
+ assert isinstance(verifier, es256.ES256Verifier)
+ assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey)
+
+
+class TestES256Signer(object):
+ def test_from_string_pkcs1(self):
+ signer = es256.ES256Signer.from_string(PKCS1_KEY_BYTES)
+ assert isinstance(signer, es256.ES256Signer)
+ assert isinstance(signer._key, ec.EllipticCurvePrivateKey)
+
+ def test_from_string_pkcs1_unicode(self):
+ key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES)
+ signer = es256.ES256Signer.from_string(key_bytes)
+ assert isinstance(signer, es256.ES256Signer)
+ assert isinstance(signer._key, ec.EllipticCurvePrivateKey)
+
+ def test_from_string_bogus_key(self):
+ key_bytes = "bogus-key"
+ with pytest.raises(ValueError):
+ es256.ES256Signer.from_string(key_bytes)
+
+ def test_from_service_account_info(self):
+ signer = es256.ES256Signer.from_service_account_info(SERVICE_ACCOUNT_INFO)
+
+ assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+ assert isinstance(signer._key, ec.EllipticCurvePrivateKey)
+
+ def test_from_service_account_info_missing_key(self):
+ with pytest.raises(ValueError) as excinfo:
+ es256.ES256Signer.from_service_account_info({})
+
+ assert excinfo.match(base._JSON_FILE_PRIVATE_KEY)
+
+ def test_from_service_account_file(self):
+ signer = es256.ES256Signer.from_service_account_file(SERVICE_ACCOUNT_JSON_FILE)
+
+ assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+ assert isinstance(signer._key, ec.EllipticCurvePrivateKey)
diff --git a/contrib/python/google-auth/py3/tests/data/authorized_user.json b/contrib/python/google-auth/py3/tests/data/authorized_user.json
new file mode 100644
index 0000000000..4787acee57
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/authorized_user.json
@@ -0,0 +1,6 @@
+{
+ "client_id": "123",
+ "client_secret": "secret",
+ "refresh_token": "alabalaportocala",
+ "type": "authorized_user"
+}
diff --git a/contrib/python/google-auth/py3/tests/data/authorized_user_cloud_sdk.json b/contrib/python/google-auth/py3/tests/data/authorized_user_cloud_sdk.json
new file mode 100644
index 0000000000..c9e19a66e0
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/authorized_user_cloud_sdk.json
@@ -0,0 +1,6 @@
+{
+ "client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com",
+ "client_secret": "secret",
+ "refresh_token": "alabalaportocala",
+ "type": "authorized_user"
+}
diff --git a/contrib/python/google-auth/py3/tests/data/authorized_user_cloud_sdk_with_quota_project_id.json b/contrib/python/google-auth/py3/tests/data/authorized_user_cloud_sdk_with_quota_project_id.json
new file mode 100644
index 0000000000..53a8ff88aa
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/authorized_user_cloud_sdk_with_quota_project_id.json
@@ -0,0 +1,7 @@
+{
+ "client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com",
+ "client_secret": "secret",
+ "refresh_token": "alabalaportocala",
+ "type": "authorized_user",
+ "quota_project_id": "quota_project_id"
+}
diff --git a/contrib/python/google-auth/py3/tests/data/authorized_user_with_rapt_token.json b/contrib/python/google-auth/py3/tests/data/authorized_user_with_rapt_token.json
new file mode 100644
index 0000000000..64b161d422
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/authorized_user_with_rapt_token.json
@@ -0,0 +1,8 @@
+{
+ "client_id": "123",
+ "client_secret": "secret",
+ "refresh_token": "alabalaportocala",
+ "type": "authorized_user",
+ "rapt_token": "rapt"
+ }
+ \ No newline at end of file
diff --git a/contrib/python/google-auth/py3/tests/data/client_secrets.json b/contrib/python/google-auth/py3/tests/data/client_secrets.json
new file mode 100644
index 0000000000..1baa4995af
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/client_secrets.json
@@ -0,0 +1,14 @@
+{
+ "web": {
+ "client_id": "example.apps.googleusercontent.com",
+ "project_id": "example",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://accounts.google.com/o/oauth2/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_secret": "itsasecrettoeveryone",
+ "redirect_uris": [
+ "urn:ietf:wg:oauth:2.0:oob",
+ "http://localhost"
+ ]
+ }
+}
diff --git a/contrib/python/google-auth/py3/tests/data/context_aware_metadata.json b/contrib/python/google-auth/py3/tests/data/context_aware_metadata.json
new file mode 100644
index 0000000000..ec40e783f1
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/context_aware_metadata.json
@@ -0,0 +1,6 @@
+{
+ "cert_provider_command":[
+ "/opt/google/endpoint-verification/bin/SecureConnectHelper",
+ "--print_certificate"],
+ "device_resource_ids":["11111111-1111-1111"]
+}
diff --git a/contrib/python/google-auth/py3/tests/data/enterprise_cert_invalid.json b/contrib/python/google-auth/py3/tests/data/enterprise_cert_invalid.json
new file mode 100644
index 0000000000..4715a590a1
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/enterprise_cert_invalid.json
@@ -0,0 +1,3 @@
+{
+ "libs": {}
+} \ No newline at end of file
diff --git a/contrib/python/google-auth/py3/tests/data/enterprise_cert_valid.json b/contrib/python/google-auth/py3/tests/data/enterprise_cert_valid.json
new file mode 100644
index 0000000000..e445f55f8a
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/enterprise_cert_valid.json
@@ -0,0 +1,6 @@
+{
+ "libs": {
+ "ecp_client": "/path/to/signer/lib",
+ "tls_offload": "/path/to/offload/lib"
+ }
+}
diff --git a/contrib/python/google-auth/py3/tests/data/es256_privatekey.pem b/contrib/python/google-auth/py3/tests/data/es256_privatekey.pem
new file mode 100644
index 0000000000..5c950b514f
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/es256_privatekey.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIAIC57aTx5ev4T2HBMQk4fXV09AzLDQ3Ju1uNoEB0LngoAoGCCqGSM49
+AwEHoUQDQgAEsACsrmP6Bp216OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJ
+z2O0IRAnXfso8K6YsjMuUDGCfj+b1IDIoA==
+-----END EC PRIVATE KEY-----
diff --git a/contrib/python/google-auth/py3/tests/data/es256_public_cert.pem b/contrib/python/google-auth/py3/tests/data/es256_public_cert.pem
new file mode 100644
index 0000000000..774ca14843
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/es256_public_cert.pem
@@ -0,0 +1,8 @@
+-----BEGIN CERTIFICATE-----
+MIIBGDCBwAIJAPUA0H4EQWsdMAoGCCqGSM49BAMCMBUxEzARBgNVBAMMCnVuaXQt
+dGVzdHMwHhcNMTkwNTA5MDI1MDExWhcNMTkwNjA4MDI1MDExWjAVMRMwEQYDVQQD
+DAp1bml0LXRlc3RzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsACsrmP6Bp21
+6OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJz2O0IRAnXfso8K6YsjMuUDGC
+fj+b1IDIoDAKBggqhkjOPQQDAgNHADBEAh8PcDTMyWk8SHqV/v8FLuMbDxdtAsq2
+dwCpuHQwqCcmAiEAnwtkiyieN+8zozaf1P4QKp2mAqNGqua50y3ua5uVotc=
+-----END CERTIFICATE-----
diff --git a/contrib/python/google-auth/py3/tests/data/es256_publickey.pem b/contrib/python/google-auth/py3/tests/data/es256_publickey.pem
new file mode 100644
index 0000000000..51f2a03fa4
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/es256_publickey.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsACsrmP6Bp216OCFm73C8W/VRHZW
+cO8yU/bMwx96f05BkTII3KeJz2O0IRAnXfso8K6YsjMuUDGCfj+b1IDIoA==
+-----END PUBLIC KEY-----
diff --git a/contrib/python/google-auth/py3/tests/data/es256_service_account.json b/contrib/python/google-auth/py3/tests/data/es256_service_account.json
new file mode 100644
index 0000000000..dd26719f62
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/es256_service_account.json
@@ -0,0 +1,10 @@
+{
+ "type": "service_account",
+ "project_id": "example-project",
+ "private_key_id": "1",
+ "private_key": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAIC57aTx5ev4T2HBMQk4fXV09AzLDQ3Ju1uNoEB0LngoAoGCCqGSM49\nAwEHoUQDQgAEsACsrmP6Bp216OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJ\nz2O0IRAnXfso8K6YsjMuUDGCfj+b1IDIoA==\n-----END EC PRIVATE KEY-----",
+ "client_email": "service-account@example.com",
+ "client_id": "1234",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://accounts.google.com/o/oauth2/token"
+}
diff --git a/contrib/python/google-auth/py3/tests/data/external_account_authorized_user.json b/contrib/python/google-auth/py3/tests/data/external_account_authorized_user.json
new file mode 100644
index 0000000000..e0bd20c8fd
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/external_account_authorized_user.json
@@ -0,0 +1,9 @@
+{
+ "type": "external_account_authorized_user",
+ "audience": "//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID",
+ "refresh_token": "refreshToken",
+ "token_url": "https://sts.googleapis.com/v1/oauth/token",
+ "token_info_url": "https://sts.googleapis.com/v1/instrospect",
+ "client_id": "clientId",
+ "client_secret": "clientSecret"
+}
diff --git a/contrib/python/google-auth/py3/tests/data/external_subject_token.json b/contrib/python/google-auth/py3/tests/data/external_subject_token.json
new file mode 100644
index 0000000000..a47ec34127
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/external_subject_token.json
@@ -0,0 +1,3 @@
+{
+ "access_token": "HEADER.SIMULATED_JWT_PAYLOAD.SIGNATURE"
+} \ No newline at end of file
diff --git a/contrib/python/google-auth/py3/tests/data/external_subject_token.txt b/contrib/python/google-auth/py3/tests/data/external_subject_token.txt
new file mode 100644
index 0000000000..c668d8f71d
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/external_subject_token.txt
@@ -0,0 +1 @@
+HEADER.SIMULATED_JWT_PAYLOAD.SIGNATURE \ No newline at end of file
diff --git a/contrib/python/google-auth/py3/tests/data/gdch_service_account.json b/contrib/python/google-auth/py3/tests/data/gdch_service_account.json
new file mode 100644
index 0000000000..172164e9fa
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/gdch_service_account.json
@@ -0,0 +1,11 @@
+{
+ "type": "gdch_service_account",
+ "format_version": "1",
+ "project": "project_foo",
+ "private_key_id": "key_foo",
+ "private_key": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIIGb2np7v54Hs6++NiLE7CQtQg7rzm4znstHvrOUlcMMoAoGCCqGSM49\nAwEHoUQDQgAECvv0VyZS9nYOa8tdwKCbkNxlWgrAZVClhJXqrvOZHlH4N3d8Rplk\n2DEJvzp04eMxlHw1jm6JCs3iJR6KAokG+w==\n-----END EC PRIVATE KEY-----\n",
+ "name": "service_identity_name",
+ "ca_cert_path": "/path/to/ca/cert",
+ "token_uri": "https://service-identity.<Domain>/authenticate"
+}
+
diff --git a/contrib/python/google-auth/py3/tests/data/impersonated_service_account_authorized_user_source.json b/contrib/python/google-auth/py3/tests/data/impersonated_service_account_authorized_user_source.json
new file mode 100644
index 0000000000..0e545392cc
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/impersonated_service_account_authorized_user_source.json
@@ -0,0 +1,13 @@
+{
+ "delegates": [
+ "service-account-delegate@example.com"
+ ],
+ "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-account-target@example.com:generateAccessToken",
+ "source_credentials": {
+ "client_id": "123",
+ "client_secret": "secret",
+ "refresh_token": "alabalaportocala",
+ "type": "authorized_user"
+ },
+ "type": "impersonated_service_account"
+} \ No newline at end of file
diff --git a/contrib/python/google-auth/py3/tests/data/impersonated_service_account_service_account_source.json b/contrib/python/google-auth/py3/tests/data/impersonated_service_account_service_account_source.json
new file mode 100644
index 0000000000..e1ff8e81f7
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/impersonated_service_account_service_account_source.json
@@ -0,0 +1,17 @@
+{
+ "delegates": [
+ "service-account-delegate@example.com"
+ ],
+ "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-account-target@example.com:generateAccessToken",
+ "source_credentials": {
+ "type": "service_account",
+ "project_id": "example-project",
+ "private_key_id": "1",
+ "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj\n7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/\nxmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs\nSliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18\npe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk\nSBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk\nnQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq\nHD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y\nnHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9\nIisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2\nYCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU\nZ422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ\nvzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP\nB8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl\naLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2\neCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI\naqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk\nklORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ\nCFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu\nUqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg\nsoBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28\nbvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH\n504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL\nYXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx\nBeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==\n-----END RSA PRIVATE KEY-----\n",
+ "client_email": "service-account@example.com",
+ "client_id": "1234",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://accounts.google.com/o/oauth2/token"
+ },
+ "type": "impersonated_service_account"
+} \ No newline at end of file
diff --git a/contrib/python/google-auth/py3/tests/data/impersonated_service_account_with_quota_project.json b/contrib/python/google-auth/py3/tests/data/impersonated_service_account_with_quota_project.json
new file mode 100644
index 0000000000..89db9617c4
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/impersonated_service_account_with_quota_project.json
@@ -0,0 +1,14 @@
+{
+ "delegates": [
+ "service-account-delegate@example.com"
+ ],
+ "quota_project_id": "quota_project",
+ "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-account-target@example.com:generateAccessToken",
+ "source_credentials": {
+ "client_id": "123",
+ "client_secret": "secret",
+ "refresh_token": "alabalaportocala",
+ "type": "authorized_user"
+ },
+ "type": "impersonated_service_account"
+} \ No newline at end of file
diff --git a/contrib/python/google-auth/py3/tests/data/old_oauth_credentials_py3.pickle b/contrib/python/google-auth/py3/tests/data/old_oauth_credentials_py3.pickle
new file mode 100644
index 0000000000..c8a05599b1
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/old_oauth_credentials_py3.pickle
Binary files differ
diff --git a/contrib/python/google-auth/py3/tests/data/other_cert.pem b/contrib/python/google-auth/py3/tests/data/other_cert.pem
new file mode 100644
index 0000000000..6895d1e7bf
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/other_cert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIJAPBsLZmNGfKtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTYwOTIxMDI0NTEyWhcNMTYxMDIxMDI0NTEyWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAsiMC7mTsmUXwZoYlT4aHY1FLw8bxIXC+z3IqA+TY1WqfbeiZRo8MA5Zx
+lTTxYMKPCZUE1XBc7jvD8GJhWIj6pToPYHn73B01IBkLBxq4kF1yV2Z7DVmkvc6H
+EcxXXq8zkCx0j6XOfiI4+qkXnuQn8cvrk8xfhtnMMZM7iVm6VSN93iRP/8ey6xuL
+XTHrDX7ukoRce1hpT8O+15GXNrY0irhhYQz5xKibNCJF3EjV28WMry8y7I8uYUFU
+RWDiQawwK9ec1zhZ94v92+GZDlPevmcFmSERKYQ0NsKcT0Y3lGuGnaExs8GyOpnC
+oksu4YJGXQjg7lkv4MxzsNbRqmCkUwxw1Mg6FP0tsCNsw9qTrkvWCRA9zp/aU+sZ
+IBGh1t4UGCub8joeQFvHxvr/3F7mH/dyvCjA34u0Lo1VPx+jYUIi9i0odltMspDW
+xOpjqdGARZYmlJP5Au9q5cQjPMcwS/EBIb8cwNl32mUE6WnFlep+38mNR/FghIjO
+ViAkXuKQmcHe6xppZAoHFsO/t3l4Tjek5vNW7erI1rgrFku/fvkIW/G8V1yIm/+Q
+F+CE4maQzCJfhftpkhM/sPC/FuLNBmNE8BHVX8y58xG4is/cQxL4Z9TsFIw0C5+3
+uTrFW9D0agysahMVzPGtCqhDQqJdIJrBQqlS6bztpzBA8zEI0skCAwEAAaOBpzCB
+pDAdBgNVHQ4EFgQUz/8FmW6TfqXyNJZr7rhc+Tn5sKQwdQYDVR0jBG4wbIAUz/8F
+mW6TfqXyNJZr7rhc+Tn5sKShSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDw
+bC2ZjRnyrTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCQmrcfhurX
+riR3Q0Y+nq040/3dJIAJXjyI9CEtxaU0nzCNTng7PwgZ0CKmCelQfInuwWFwBSHS
+6kBfC1rgJeFnjnTt8a3RCgRlIgUr9NCdPSEccB7TurobwPJ2h6cJjjR8urcb0CXh
+CEMvPneyPj0xUFY8vVKXMGWahz/kyfwIiVqcX/OtMZ29fUu1onbWl71g2gVLtUZl
+sECdZ+AC/6HDCVpYIVETMl1T7N/XyqXZQiDLDNRDeZhnapz8w9fsW1KVujAZLNQR
+pVnw2qa2UK1dSf2FHX+lQU5mFSYM4vtwaMlX/LgfdLZ9I796hFh619WwTVz+LO2N
+vHnwBMabld3XSPuZRqlbBulDQ07Vbqdjv8DYSLA2aKI4ZkMMKuFLG/oS28V2ZYmv
+/KpGEs5UgKY+P9NulYpTDwCU/6SomuQpP795wbG6sm7Hzq82r2RmB61GupNRGeqi
+pXKsy69T388zBxYu6zQrosXiDl5YzaViH7tm0J7opye8dCWjjpnahki0vq2znti7
+6cWla2j8Xz1glvLz+JI/NCOMfxUInb82T7ijo80N0VJ2hzf7p2GxRZXAxAV9knLI
+nM4F5TLjSd7ZhOOZ7ni/eZFueTMisWfypt2nc41whGjHMX/Zp1kPfhB4H2bLKIX/
+lSrwNr3qbGTEJX8JqpDBNVAd96XkMvDNyA==
+-----END CERTIFICATE-----
diff --git a/contrib/python/google-auth/py3/tests/data/pem_from_pkcs12.pem b/contrib/python/google-auth/py3/tests/data/pem_from_pkcs12.pem
new file mode 100644
index 0000000000..2d77e10c1f
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/pem_from_pkcs12.pem
@@ -0,0 +1,32 @@
+Bag Attributes
+ friendlyName: key
+ localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC
+Key Attributes: <No Attributes>
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi
+tUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p
+oJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR
+aIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt
+w21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE
+GKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp
++qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN
+TzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4
+QoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG
+Dy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo
+f1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR
++DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p
+IwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a
+c3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7
+SgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0
+jGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY
+iuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5
+sdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO
+GCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk
+Brn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk
+t7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2
+DwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS
+LZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB
+WGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa
+XUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB
+VL5h7N0VstYhGgycuPpcIUQa
+-----END PRIVATE KEY-----
diff --git a/contrib/python/google-auth/py3/tests/data/privatekey.p12 b/contrib/python/google-auth/py3/tests/data/privatekey.p12
new file mode 100644
index 0000000000..c369ecb6e6
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/privatekey.p12
Binary files differ
diff --git a/contrib/python/google-auth/py3/tests/data/privatekey.pem b/contrib/python/google-auth/py3/tests/data/privatekey.pem
new file mode 100644
index 0000000000..57443540ad
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/privatekey.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj
+7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/
+xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs
+SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18
+pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk
+SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk
+nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq
+HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y
+nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9
+IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2
+YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU
+Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ
+vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP
+B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl
+aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2
+eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI
+aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk
+klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ
+CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu
+UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg
+soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28
+bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH
+504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL
+YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx
+BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==
+-----END RSA PRIVATE KEY-----
diff --git a/contrib/python/google-auth/py3/tests/data/privatekey.pub b/contrib/python/google-auth/py3/tests/data/privatekey.pub
new file mode 100644
index 0000000000..11fdaa42f0
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/privatekey.pub
@@ -0,0 +1,8 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg
+kdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU
+1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS
+5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+z
+pyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc/
+/fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB
+-----END RSA PUBLIC KEY-----
diff --git a/contrib/python/google-auth/py3/tests/data/public_cert.pem b/contrib/python/google-auth/py3/tests/data/public_cert.pem
new file mode 100644
index 0000000000..7af6ca3f93
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/public_cert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV
+BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV
+MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM
+7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer
+uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp
+gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4
++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3
+ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O
+gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh
+GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD
+AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr
+odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk
++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9
+ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql
+ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT
+cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB
+-----END CERTIFICATE-----
diff --git a/contrib/python/google-auth/py3/tests/data/service_account.json b/contrib/python/google-auth/py3/tests/data/service_account.json
new file mode 100644
index 0000000000..9e76f4d355
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/service_account.json
@@ -0,0 +1,10 @@
+{
+ "type": "service_account",
+ "project_id": "example-project",
+ "private_key_id": "1",
+ "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj\n7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/\nxmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs\nSliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18\npe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk\nSBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk\nnQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq\nHD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y\nnHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9\nIisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2\nYCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU\nZ422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ\nvzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP\nB8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl\naLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2\neCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI\naqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk\nklORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ\nCFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu\nUqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg\nsoBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28\nbvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH\n504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL\nYXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx\nBeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==\n-----END RSA PRIVATE KEY-----\n",
+ "client_email": "service-account@example.com",
+ "client_id": "1234",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://accounts.google.com/o/oauth2/token"
+}
diff --git a/contrib/python/google-auth/py3/tests/data/service_account_non_gdu.json b/contrib/python/google-auth/py3/tests/data/service_account_non_gdu.json
new file mode 100644
index 0000000000..976184f8c2
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/data/service_account_non_gdu.json
@@ -0,0 +1,15 @@
+{
+ "type": "service_account",
+ "universe_domain": "universe.foo",
+ "project_id": "example_project",
+ "private_key_id": "1",
+ "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj\n7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/\nxmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs\nSliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18\npe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk\nSBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk\nnQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq\nHD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y\nnHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9\nIisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2\nYCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU\nZ422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ\nvzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP\nB8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl\naLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2\neCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI\naqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk\nklORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ\nCFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu\nUqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg\nsoBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28\nbvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH\n504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL\nYXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx\nBeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==\n-----END RSA PRIVATE KEY-----\n",
+ "client_email": "testsa@foo.iam.gserviceaccount.com",
+ "client_id": "1234",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.universe.foo/token",
+ "auth_provider_x509_cert_url": "https://www.universe.foo/oauth2/v1/certs",
+ "client_x509_cert_url": "https://www.universe.foo/robot/v1/metadata/x509/foo.iam.gserviceaccount.com"
+}
+
+ \ No newline at end of file
diff --git a/contrib/python/google-auth/py3/tests/oauth2/__init__.py b/contrib/python/google-auth/py3/tests/oauth2/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/__init__.py
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test__client.py b/contrib/python/google-auth/py3/tests/oauth2/test__client.py
new file mode 100644
index 0000000000..54179269bd
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test__client.py
@@ -0,0 +1,622 @@
+# Copyright 2016 Google LLC
+#
+# 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 datetime
+import http.client as http_client
+import json
+import os
+import urllib
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth import transport
+from google.oauth2 import _client
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+ PRIVATE_KEY_BYTES = fh.read()
+
+SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1")
+
+SCOPES_AS_LIST = [
+ "https://www.googleapis.com/auth/pubsub",
+ "https://www.googleapis.com/auth/logging.write",
+]
+SCOPES_AS_STRING = (
+ "https://www.googleapis.com/auth/pubsub"
+ " https://www.googleapis.com/auth/logging.write"
+)
+
+ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
+ "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/sa"
+)
+ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
+ "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/sa"
+)
+
+
+@pytest.mark.parametrize("retryable", [True, False])
+def test__handle_error_response(retryable):
+ response_data = {"error": "help", "error_description": "I'm alive"}
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _client._handle_error_response(response_data, retryable)
+
+ assert excinfo.value.retryable == retryable
+ assert excinfo.match(r"help: I\'m alive")
+
+
+def test__handle_error_response_no_error():
+ response_data = {"foo": "bar"}
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _client._handle_error_response(response_data, False)
+
+ assert not excinfo.value.retryable
+ assert excinfo.match(r"{\"foo\": \"bar\"}")
+
+
+def test__handle_error_response_not_json():
+ response_data = "this is an error message"
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _client._handle_error_response(response_data, False)
+
+ assert not excinfo.value.retryable
+ assert excinfo.match(response_data)
+
+
+def test__can_retry_retryable():
+ retryable_codes = transport.DEFAULT_RETRYABLE_STATUS_CODES
+ for status_code in range(100, 600):
+ if status_code in retryable_codes:
+ assert _client._can_retry(status_code, {"error": "invalid_scope"})
+ else:
+ assert not _client._can_retry(status_code, {"error": "invalid_scope"})
+
+
+@pytest.mark.parametrize(
+ "response_data", [{"error": "internal_failure"}, {"error": "server_error"}]
+)
+def test__can_retry_message(response_data):
+ assert _client._can_retry(http_client.OK, response_data)
+
+
+@pytest.mark.parametrize(
+ "response_data",
+ [
+ {"error": "invalid_scope"},
+ {"error": {"foo": "bar"}},
+ {"error_description": {"foo", "bar"}},
+ ],
+)
+def test__can_retry_no_retry_message(response_data):
+ assert not _client._can_retry(http_client.OK, response_data)
+
+
+@pytest.mark.parametrize("mock_expires_in", [500, "500"])
+@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test__parse_expiry(unused_utcnow, mock_expires_in):
+ result = _client._parse_expiry({"expires_in": mock_expires_in})
+ assert result == datetime.datetime.min + datetime.timedelta(seconds=500)
+
+
+def test__parse_expiry_none():
+ assert _client._parse_expiry({}) is None
+
+
+def make_request(response_data, status=http_client.OK):
+ response = mock.create_autospec(transport.Response, instance=True)
+ response.status = status
+ response.data = json.dumps(response_data).encode("utf-8")
+ request = mock.create_autospec(transport.Request)
+ request.return_value = response
+ return request
+
+
+def test__token_endpoint_request():
+ request = make_request({"test": "response"})
+
+ result = _client._token_endpoint_request(
+ request, "http://example.com", {"test": "params"}
+ )
+
+ # Check request call
+ request.assert_called_with(
+ method="POST",
+ url="http://example.com",
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
+ body="test=params".encode("utf-8"),
+ )
+
+ # Check result
+ assert result == {"test": "response"}
+
+
+def test__token_endpoint_request_use_json():
+ request = make_request({"test": "response"})
+
+ result = _client._token_endpoint_request(
+ request,
+ "http://example.com",
+ {"test": "params"},
+ access_token="access_token",
+ use_json=True,
+ )
+
+ # Check request call
+ request.assert_called_with(
+ method="POST",
+ url="http://example.com",
+ headers={
+ "Content-Type": "application/json",
+ "Authorization": "Bearer access_token",
+ },
+ body=b'{"test": "params"}',
+ )
+
+ # Check result
+ assert result == {"test": "response"}
+
+
+def test__token_endpoint_request_error():
+ request = make_request({}, status=http_client.BAD_REQUEST)
+
+ with pytest.raises(exceptions.RefreshError):
+ _client._token_endpoint_request(request, "http://example.com", {})
+
+
+def test__token_endpoint_request_internal_failure_error():
+ request = make_request(
+ {"error_description": "internal_failure"}, status=http_client.BAD_REQUEST
+ )
+
+ with pytest.raises(exceptions.RefreshError):
+ _client._token_endpoint_request(
+ request, "http://example.com", {"error_description": "internal_failure"}
+ )
+ # request should be called once and then with 3 retries
+ assert request.call_count == 4
+
+ request = make_request(
+ {"error": "internal_failure"}, status=http_client.BAD_REQUEST
+ )
+
+ with pytest.raises(exceptions.RefreshError):
+ _client._token_endpoint_request(
+ request, "http://example.com", {"error": "internal_failure"}
+ )
+ # request should be called once and then with 3 retries
+ assert request.call_count == 4
+
+
+def test__token_endpoint_request_internal_failure_and_retry_failure_error():
+ retryable_error = mock.create_autospec(transport.Response, instance=True)
+ retryable_error.status = http_client.BAD_REQUEST
+ retryable_error.data = json.dumps({"error_description": "internal_failure"}).encode(
+ "utf-8"
+ )
+
+ unretryable_error = mock.create_autospec(transport.Response, instance=True)
+ unretryable_error.status = http_client.BAD_REQUEST
+ unretryable_error.data = json.dumps({"error_description": "invalid_scope"}).encode(
+ "utf-8"
+ )
+
+ request = mock.create_autospec(transport.Request)
+
+ request.side_effect = [retryable_error, retryable_error, unretryable_error]
+
+ with pytest.raises(exceptions.RefreshError):
+ _client._token_endpoint_request(
+ request, "http://example.com", {"error_description": "invalid_scope"}
+ )
+ # request should be called three times. Two retryable errors and one
+ # unretryable error to break the retry loop.
+ assert request.call_count == 3
+
+
+def test__token_endpoint_request_internal_failure_and_retry_succeeds():
+ retryable_error = mock.create_autospec(transport.Response, instance=True)
+ retryable_error.status = http_client.BAD_REQUEST
+ retryable_error.data = json.dumps({"error_description": "internal_failure"}).encode(
+ "utf-8"
+ )
+
+ response = mock.create_autospec(transport.Response, instance=True)
+ response.status = http_client.OK
+ response.data = json.dumps({"hello": "world"}).encode("utf-8")
+
+ request = mock.create_autospec(transport.Request)
+
+ request.side_effect = [retryable_error, response]
+
+ _ = _client._token_endpoint_request(
+ request, "http://example.com", {"test": "params"}
+ )
+
+ assert request.call_count == 2
+
+
+def test__token_endpoint_request_string_error():
+ response = mock.create_autospec(transport.Response, instance=True)
+ response.status = http_client.BAD_REQUEST
+ response.data = "this is an error message"
+ request = mock.create_autospec(transport.Request)
+ request.return_value = response
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _client._token_endpoint_request(request, "http://example.com", {})
+ assert excinfo.match("this is an error message")
+
+
+def verify_request_params(request, params):
+ request_body = request.call_args[1]["body"].decode("utf-8")
+ request_params = urllib.parse.parse_qs(request_body)
+
+ for key, value in params.items():
+ assert request_params[key][0] == value
+
+
+@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_jwt_grant(utcnow):
+ request = make_request(
+ {"access_token": "token", "expires_in": 500, "extra": "data"}
+ )
+
+ token, expiry, extra_data = _client.jwt_grant(
+ request, "http://example.com", "assertion_value"
+ )
+
+ # Check request call
+ verify_request_params(
+ request, {"grant_type": _client._JWT_GRANT_TYPE, "assertion": "assertion_value"}
+ )
+
+ # Check result
+ assert token == "token"
+ assert expiry == utcnow() + datetime.timedelta(seconds=500)
+ assert extra_data["extra"] == "data"
+
+
+def test_jwt_grant_no_access_token():
+ request = make_request(
+ {
+ # No access token.
+ "expires_in": 500,
+ "extra": "data",
+ }
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _client.jwt_grant(request, "http://example.com", "assertion_value")
+ assert not excinfo.value.retryable
+
+
+def test_call_iam_generate_id_token_endpoint():
+ now = _helpers.utcnow()
+ id_token_expiry = _helpers.datetime_to_secs(now)
+ id_token = jwt.encode(SIGNER, {"exp": id_token_expiry}).decode("utf-8")
+ request = make_request({"token": id_token})
+
+ token, expiry = _client.call_iam_generate_id_token_endpoint(
+ request, "fake_email", "fake_audience", "fake_access_token"
+ )
+
+ assert (
+ request.call_args[1]["url"]
+ == "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/fake_email:generateIdToken"
+ )
+ assert request.call_args[1]["headers"]["Content-Type"] == "application/json"
+ assert (
+ request.call_args[1]["headers"]["Authorization"] == "Bearer fake_access_token"
+ )
+ response_body = json.loads(request.call_args[1]["body"])
+ assert response_body["audience"] == "fake_audience"
+ assert response_body["includeEmail"] == "true"
+ assert response_body["useEmailAzp"] == "true"
+
+ # Check result
+ assert token == id_token
+ # JWT does not store microseconds
+ now = now.replace(microsecond=0)
+ assert expiry == now
+
+
+def test_call_iam_generate_id_token_endpoint_no_id_token():
+ request = make_request(
+ {
+ # No access token.
+ "error": "no token"
+ }
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _client.call_iam_generate_id_token_endpoint(
+ request, "fake_email", "fake_audience", "fake_access_token"
+ )
+ assert excinfo.match("No ID token in response")
+
+
+def test_id_token_jwt_grant():
+ now = _helpers.utcnow()
+ id_token_expiry = _helpers.datetime_to_secs(now)
+ id_token = jwt.encode(SIGNER, {"exp": id_token_expiry}).decode("utf-8")
+ request = make_request({"id_token": id_token, "extra": "data"})
+
+ token, expiry, extra_data = _client.id_token_jwt_grant(
+ request, "http://example.com", "assertion_value"
+ )
+
+ # Check request call
+ verify_request_params(
+ request, {"grant_type": _client._JWT_GRANT_TYPE, "assertion": "assertion_value"}
+ )
+
+ # Check result
+ assert token == id_token
+ # JWT does not store microseconds
+ now = now.replace(microsecond=0)
+ assert expiry == now
+ assert extra_data["extra"] == "data"
+
+
+def test_id_token_jwt_grant_no_access_token():
+ request = make_request(
+ {
+ # No access token.
+ "expires_in": 500,
+ "extra": "data",
+ }
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _client.id_token_jwt_grant(request, "http://example.com", "assertion_value")
+ assert not excinfo.value.retryable
+
+
+@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_refresh_grant(unused_utcnow):
+ request = make_request(
+ {
+ "access_token": "token",
+ "refresh_token": "new_refresh_token",
+ "expires_in": 500,
+ "extra": "data",
+ }
+ )
+
+ token, refresh_token, expiry, extra_data = _client.refresh_grant(
+ request,
+ "http://example.com",
+ "refresh_token",
+ "client_id",
+ "client_secret",
+ rapt_token="rapt_token",
+ )
+
+ # Check request call
+ verify_request_params(
+ request,
+ {
+ "grant_type": _client._REFRESH_GRANT_TYPE,
+ "refresh_token": "refresh_token",
+ "client_id": "client_id",
+ "client_secret": "client_secret",
+ "rapt": "rapt_token",
+ },
+ )
+
+ # Check result
+ assert token == "token"
+ assert refresh_token == "new_refresh_token"
+ assert expiry == datetime.datetime.min + datetime.timedelta(seconds=500)
+ assert extra_data["extra"] == "data"
+
+
+@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_refresh_grant_with_scopes(unused_utcnow):
+ request = make_request(
+ {
+ "access_token": "token",
+ "refresh_token": "new_refresh_token",
+ "expires_in": 500,
+ "extra": "data",
+ "scope": SCOPES_AS_STRING,
+ }
+ )
+
+ token, refresh_token, expiry, extra_data = _client.refresh_grant(
+ request,
+ "http://example.com",
+ "refresh_token",
+ "client_id",
+ "client_secret",
+ SCOPES_AS_LIST,
+ )
+
+ # Check request call.
+ verify_request_params(
+ request,
+ {
+ "grant_type": _client._REFRESH_GRANT_TYPE,
+ "refresh_token": "refresh_token",
+ "client_id": "client_id",
+ "client_secret": "client_secret",
+ "scope": SCOPES_AS_STRING,
+ },
+ )
+
+ # Check result.
+ assert token == "token"
+ assert refresh_token == "new_refresh_token"
+ assert expiry == datetime.datetime.min + datetime.timedelta(seconds=500)
+ assert extra_data["extra"] == "data"
+
+
+def test_refresh_grant_no_access_token():
+ request = make_request(
+ {
+ # No access token.
+ "refresh_token": "new_refresh_token",
+ "expires_in": 500,
+ "extra": "data",
+ }
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _client.refresh_grant(
+ request, "http://example.com", "refresh_token", "client_id", "client_secret"
+ )
+ assert not excinfo.value.retryable
+
+
+@mock.patch(
+ "google.auth.metrics.token_request_access_token_sa_assertion",
+ return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+)
+@mock.patch("google.oauth2._client._parse_expiry", return_value=None)
+@mock.patch.object(_client, "_token_endpoint_request", autospec=True)
+def test_jwt_grant_retry_default(
+ mock_token_endpoint_request, mock_expiry, mock_metrics_header_value
+):
+ _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock())
+ mock_token_endpoint_request.assert_called_with(
+ mock.ANY,
+ mock.ANY,
+ mock.ANY,
+ can_retry=True,
+ headers={"x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE},
+ )
+
+
+@pytest.mark.parametrize("can_retry", [True, False])
+@mock.patch(
+ "google.auth.metrics.token_request_access_token_sa_assertion",
+ return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+)
+@mock.patch("google.oauth2._client._parse_expiry", return_value=None)
+@mock.patch.object(_client, "_token_endpoint_request", autospec=True)
+def test_jwt_grant_retry_with_retry(
+ mock_token_endpoint_request, mock_expiry, mock_metrics_header_value, can_retry
+):
+ _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry)
+ mock_token_endpoint_request.assert_called_with(
+ mock.ANY,
+ mock.ANY,
+ mock.ANY,
+ can_retry=can_retry,
+ headers={"x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE},
+ )
+
+
+@mock.patch(
+ "google.auth.metrics.token_request_id_token_sa_assertion",
+ return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+)
+@mock.patch("google.auth.jwt.decode", return_value={"exp": 0})
+@mock.patch.object(_client, "_token_endpoint_request", autospec=True)
+def test_id_token_jwt_grant_retry_default(
+ mock_token_endpoint_request, mock_jwt_decode, mock_metrics_header_value
+):
+ _client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock())
+ mock_token_endpoint_request.assert_called_with(
+ mock.ANY,
+ mock.ANY,
+ mock.ANY,
+ can_retry=True,
+ headers={"x-goog-api-client": ID_TOKEN_REQUEST_METRICS_HEADER_VALUE},
+ )
+
+
+@pytest.mark.parametrize("can_retry", [True, False])
+@mock.patch(
+ "google.auth.metrics.token_request_id_token_sa_assertion",
+ return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+)
+@mock.patch("google.auth.jwt.decode", return_value={"exp": 0})
+@mock.patch.object(_client, "_token_endpoint_request", autospec=True)
+def test_id_token_jwt_grant_retry_with_retry(
+ mock_token_endpoint_request, mock_jwt_decode, mock_metrics_header_value, can_retry
+):
+ _client.id_token_jwt_grant(
+ mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry
+ )
+ mock_token_endpoint_request.assert_called_with(
+ mock.ANY,
+ mock.ANY,
+ mock.ANY,
+ can_retry=can_retry,
+ headers={"x-goog-api-client": ID_TOKEN_REQUEST_METRICS_HEADER_VALUE},
+ )
+
+
+@mock.patch("google.oauth2._client._parse_expiry", return_value=None)
+@mock.patch.object(_client, "_token_endpoint_request", autospec=True)
+def test_refresh_grant_retry_default(mock_token_endpoint_request, mock_parse_expiry):
+ _client.refresh_grant(
+ mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock()
+ )
+ mock_token_endpoint_request.assert_called_with(
+ mock.ANY, mock.ANY, mock.ANY, can_retry=True
+ )
+
+
+@pytest.mark.parametrize("can_retry", [True, False])
+@mock.patch("google.oauth2._client._parse_expiry", return_value=None)
+@mock.patch.object(_client, "_token_endpoint_request", autospec=True)
+def test_refresh_grant_retry_with_retry(
+ mock_token_endpoint_request, mock_parse_expiry, can_retry
+):
+ _client.refresh_grant(
+ mock.Mock(),
+ mock.Mock(),
+ mock.Mock(),
+ mock.Mock(),
+ mock.Mock(),
+ can_retry=can_retry,
+ )
+ mock_token_endpoint_request.assert_called_with(
+ mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry
+ )
+
+
+@pytest.mark.parametrize("can_retry", [True, False])
+def test__token_endpoint_request_no_throw_with_retry(can_retry):
+ response_data = {"error": "help", "error_description": "I'm alive"}
+ body = "dummy body"
+
+ mock_response = mock.create_autospec(transport.Response, instance=True)
+ mock_response.status = http_client.INTERNAL_SERVER_ERROR
+ mock_response.data = json.dumps(response_data).encode("utf-8")
+
+ mock_request = mock.create_autospec(transport.Request)
+ mock_request.return_value = mock_response
+
+ _client._token_endpoint_request_no_throw(
+ mock_request, mock.Mock(), body, mock.Mock(), mock.Mock(), can_retry=can_retry
+ )
+
+ if can_retry:
+ assert mock_request.call_count == 4
+ else:
+ assert mock_request.call_count == 1
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_challenges.py b/contrib/python/google-auth/py3/tests/oauth2/test_challenges.py
new file mode 100644
index 0000000000..a06f552837
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_challenges.py
@@ -0,0 +1,198 @@
+# Copyright 2021 Google LLC
+#
+# 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.
+
+"""Tests for the reauth module."""
+
+import base64
+import sys
+
+import mock
+import pytest # type: ignore
+import pyu2f # type: ignore
+
+from google.auth import exceptions
+from google.oauth2 import challenges
+
+
+def test_get_user_password():
+ with mock.patch("getpass.getpass", return_value="foo"):
+ assert challenges.get_user_password("") == "foo"
+
+
+def test_security_key():
+ metadata = {
+ "status": "READY",
+ "challengeId": 2,
+ "challengeType": "SECURITY_KEY",
+ "securityKey": {
+ "applicationId": "security_key_application_id",
+ "challenges": [
+ {
+ "keyHandle": "some_key",
+ "challenge": base64.urlsafe_b64encode(
+ "some_challenge".encode("ascii")
+ ).decode("ascii"),
+ }
+ ],
+ "relyingPartyId": "security_key_application_id",
+ },
+ }
+ mock_key = mock.Mock()
+
+ challenge = challenges.SecurityKeyChallenge()
+
+ # Test the case that security key challenge is passed with applicationId and
+ # relyingPartyId the same.
+ with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
+ with mock.patch(
+ "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+ ) as mock_authenticate:
+ mock_authenticate.return_value = "security key response"
+ assert challenge.name == "SECURITY_KEY"
+ assert challenge.is_locally_eligible
+ assert challenge.obtain_challenge_input(metadata) == {
+ "securityKey": "security key response"
+ }
+ mock_authenticate.assert_called_with(
+ "security_key_application_id",
+ [{"key": mock_key, "challenge": b"some_challenge"}],
+ print_callback=sys.stderr.write,
+ )
+
+ # Test the case that security key challenge is passed with applicationId and
+ # relyingPartyId different, first call works.
+ metadata["securityKey"]["relyingPartyId"] = "security_key_relying_party_id"
+ sys.stderr.write("metadata=" + str(metadata) + "\n")
+ with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
+ with mock.patch(
+ "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+ ) as mock_authenticate:
+ mock_authenticate.return_value = "security key response"
+ assert challenge.name == "SECURITY_KEY"
+ assert challenge.is_locally_eligible
+ assert challenge.obtain_challenge_input(metadata) == {
+ "securityKey": "security key response"
+ }
+ mock_authenticate.assert_called_with(
+ "security_key_relying_party_id",
+ [{"key": mock_key, "challenge": b"some_challenge"}],
+ print_callback=sys.stderr.write,
+ )
+
+ # Test the case that security key challenge is passed with applicationId and
+ # relyingPartyId different, first call fails, requires retry.
+ metadata["securityKey"]["relyingPartyId"] = "security_key_relying_party_id"
+ with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
+ with mock.patch(
+ "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+ ) as mock_authenticate:
+ assert challenge.name == "SECURITY_KEY"
+ assert challenge.is_locally_eligible
+ mock_authenticate.side_effect = [
+ pyu2f.errors.U2FError(pyu2f.errors.U2FError.DEVICE_INELIGIBLE),
+ "security key response",
+ ]
+ assert challenge.obtain_challenge_input(metadata) == {
+ "securityKey": "security key response"
+ }
+ calls = [
+ mock.call(
+ "security_key_relying_party_id",
+ [{"key": mock_key, "challenge": b"some_challenge"}],
+ print_callback=sys.stderr.write,
+ ),
+ mock.call(
+ "security_key_application_id",
+ [{"key": mock_key, "challenge": b"some_challenge"}],
+ print_callback=sys.stderr.write,
+ ),
+ ]
+ mock_authenticate.assert_has_calls(calls)
+
+ # Test various types of exceptions.
+ with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
+ with mock.patch(
+ "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+ ) as mock_authenticate:
+ mock_authenticate.side_effect = pyu2f.errors.U2FError(
+ pyu2f.errors.U2FError.DEVICE_INELIGIBLE
+ )
+ assert challenge.obtain_challenge_input(metadata) is None
+
+ with mock.patch(
+ "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+ ) as mock_authenticate:
+ mock_authenticate.side_effect = pyu2f.errors.U2FError(
+ pyu2f.errors.U2FError.TIMEOUT
+ )
+ assert challenge.obtain_challenge_input(metadata) is None
+
+ with mock.patch(
+ "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+ ) as mock_authenticate:
+ mock_authenticate.side_effect = pyu2f.errors.PluginError()
+ assert challenge.obtain_challenge_input(metadata) is None
+
+ with mock.patch(
+ "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+ ) as mock_authenticate:
+ mock_authenticate.side_effect = pyu2f.errors.U2FError(
+ pyu2f.errors.U2FError.BAD_REQUEST
+ )
+ with pytest.raises(pyu2f.errors.U2FError):
+ challenge.obtain_challenge_input(metadata)
+
+ with mock.patch(
+ "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+ ) as mock_authenticate:
+ mock_authenticate.side_effect = pyu2f.errors.NoDeviceFoundError()
+ assert challenge.obtain_challenge_input(metadata) is None
+
+ with mock.patch(
+ "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+ ) as mock_authenticate:
+ mock_authenticate.side_effect = pyu2f.errors.UnsupportedVersionException()
+ with pytest.raises(pyu2f.errors.UnsupportedVersionException):
+ challenge.obtain_challenge_input(metadata)
+
+ with mock.patch.dict("sys.modules"):
+ sys.modules["pyu2f"] = None
+ with pytest.raises(exceptions.ReauthFailError) as excinfo:
+ challenge.obtain_challenge_input(metadata)
+ assert excinfo.match(r"pyu2f dependency is required")
+
+
+@mock.patch("getpass.getpass", return_value="foo")
+def test_password_challenge(getpass_mock):
+ challenge = challenges.PasswordChallenge()
+
+ with mock.patch("getpass.getpass", return_value="foo"):
+ assert challenge.is_locally_eligible
+ assert challenge.name == "PASSWORD"
+ assert challenges.PasswordChallenge().obtain_challenge_input({}) == {
+ "credential": "foo"
+ }
+
+ with mock.patch("getpass.getpass", return_value=None):
+ assert challenges.PasswordChallenge().obtain_challenge_input({}) == {
+ "credential": " "
+ }
+
+
+def test_saml_challenge():
+ challenge = challenges.SamlChallenge()
+ assert challenge.is_locally_eligible
+ assert challenge.name == "SAML"
+ with pytest.raises(exceptions.ReauthSamlChallengeFailError):
+ challenge.obtain_challenge_input(None)
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_credentials.py b/contrib/python/google-auth/py3/tests/oauth2/test_credentials.py
new file mode 100644
index 0000000000..f2604a5f18
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_credentials.py
@@ -0,0 +1,997 @@
+# Copyright 2016 Google LLC
+#
+# 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 datetime
+import json
+import os
+import pickle
+import sys
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import transport
+from google.oauth2 import credentials
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+
+AUTH_USER_JSON_FILE = os.path.join(DATA_DIR, "authorized_user.json")
+
+with open(AUTH_USER_JSON_FILE, "r") as fh:
+ AUTH_USER_INFO = json.load(fh)
+
+
+class TestCredentials(object):
+ TOKEN_URI = "https://example.com/oauth2/token"
+ REFRESH_TOKEN = "refresh_token"
+ RAPT_TOKEN = "rapt_token"
+ CLIENT_ID = "client_id"
+ CLIENT_SECRET = "client_secret"
+
+ @classmethod
+ def make_credentials(cls):
+ return credentials.Credentials(
+ token=None,
+ refresh_token=cls.REFRESH_TOKEN,
+ token_uri=cls.TOKEN_URI,
+ client_id=cls.CLIENT_ID,
+ client_secret=cls.CLIENT_SECRET,
+ rapt_token=cls.RAPT_TOKEN,
+ enable_reauth_refresh=True,
+ )
+
+ def test_default_state(self):
+ credentials = self.make_credentials()
+ assert not credentials.valid
+ # Expiration hasn't been set yet
+ assert not credentials.expired
+ # Scopes aren't required for these credentials
+ assert not credentials.requires_scopes
+ # Test properties
+ assert credentials.refresh_token == self.REFRESH_TOKEN
+ assert credentials.token_uri == self.TOKEN_URI
+ assert credentials.client_id == self.CLIENT_ID
+ assert credentials.client_secret == self.CLIENT_SECRET
+ assert credentials.rapt_token == self.RAPT_TOKEN
+ assert credentials.refresh_handler is None
+
+ def test_token_usage_metrics(self):
+ credentials = self.make_credentials()
+ credentials.token = "token"
+ credentials.expiry = None
+
+ headers = {}
+ credentials.before_request(mock.Mock(), None, None, headers)
+ assert headers["authorization"] == "Bearer token"
+ assert headers["x-goog-api-client"] == "cred-type/u"
+
+ def test_refresh_handler_setter_and_getter(self):
+ scopes = ["email", "profile"]
+ original_refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN_1", None))
+ updated_refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN_2", None))
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=None,
+ token_uri=None,
+ client_id=None,
+ client_secret=None,
+ rapt_token=None,
+ scopes=scopes,
+ default_scopes=None,
+ refresh_handler=original_refresh_handler,
+ )
+
+ assert creds.refresh_handler is original_refresh_handler
+
+ creds.refresh_handler = updated_refresh_handler
+
+ assert creds.refresh_handler is updated_refresh_handler
+
+ creds.refresh_handler = None
+
+ assert creds.refresh_handler is None
+
+ def test_invalid_refresh_handler(self):
+ scopes = ["email", "profile"]
+ with pytest.raises(TypeError) as excinfo:
+ credentials.Credentials(
+ token=None,
+ refresh_token=None,
+ token_uri=None,
+ client_id=None,
+ client_secret=None,
+ rapt_token=None,
+ scopes=scopes,
+ default_scopes=None,
+ refresh_handler=object(),
+ )
+
+ assert excinfo.match("The provided refresh_handler is not a callable or None.")
+
+ @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+ )
+ def test_refresh_success(self, unused_utcnow, refresh_grant):
+ token = "token"
+ new_rapt_token = "new_rapt_token"
+ expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+ grant_response = {"id_token": mock.sentinel.id_token}
+ refresh_grant.return_value = (
+ # Access token
+ token,
+ # New refresh token
+ None,
+ # Expiry,
+ expiry,
+ # Extra data
+ grant_response,
+ # rapt_token
+ new_rapt_token,
+ )
+
+ request = mock.create_autospec(transport.Request)
+ credentials = self.make_credentials()
+
+ # Refresh credentials
+ credentials.refresh(request)
+
+ # Check jwt grant call.
+ refresh_grant.assert_called_with(
+ request,
+ self.TOKEN_URI,
+ self.REFRESH_TOKEN,
+ self.CLIENT_ID,
+ self.CLIENT_SECRET,
+ None,
+ self.RAPT_TOKEN,
+ True,
+ )
+
+ # Check that the credentials have the token and expiry
+ assert credentials.token == token
+ assert credentials.expiry == expiry
+ assert credentials.id_token == mock.sentinel.id_token
+ assert credentials.rapt_token == new_rapt_token
+
+ # Check that the credentials are valid (have a token and are not
+ # expired)
+ assert credentials.valid
+
+ def test_refresh_no_refresh_token(self):
+ request = mock.create_autospec(transport.Request)
+ credentials_ = credentials.Credentials(token=None, refresh_token=None)
+
+ with pytest.raises(exceptions.RefreshError, match="necessary fields"):
+ credentials_.refresh(request)
+
+ request.assert_not_called()
+
+ @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+ )
+ def test_refresh_with_refresh_token_and_refresh_handler(
+ self, unused_utcnow, refresh_grant
+ ):
+ token = "token"
+ new_rapt_token = "new_rapt_token"
+ expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+ grant_response = {"id_token": mock.sentinel.id_token}
+ refresh_grant.return_value = (
+ # Access token
+ token,
+ # New refresh token
+ None,
+ # Expiry,
+ expiry,
+ # Extra data
+ grant_response,
+ # rapt_token
+ new_rapt_token,
+ )
+
+ refresh_handler = mock.Mock()
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ rapt_token=self.RAPT_TOKEN,
+ refresh_handler=refresh_handler,
+ )
+
+ # Refresh credentials
+ creds.refresh(request)
+
+ # Check jwt grant call.
+ refresh_grant.assert_called_with(
+ request,
+ self.TOKEN_URI,
+ self.REFRESH_TOKEN,
+ self.CLIENT_ID,
+ self.CLIENT_SECRET,
+ None,
+ self.RAPT_TOKEN,
+ False,
+ )
+
+ # Check that the credentials have the token and expiry
+ assert creds.token == token
+ assert creds.expiry == expiry
+ assert creds.id_token == mock.sentinel.id_token
+ assert creds.rapt_token == new_rapt_token
+
+ # Check that the credentials are valid (have a token and are not
+ # expired)
+ assert creds.valid
+
+ # Assert refresh handler not called as the refresh token has
+ # higher priority.
+ refresh_handler.assert_not_called()
+
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh_with_refresh_handler_success_scopes(self, unused_utcnow):
+ expected_expiry = datetime.datetime.min + datetime.timedelta(seconds=2800)
+ refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN", expected_expiry))
+ scopes = ["email", "profile"]
+ default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=None,
+ token_uri=None,
+ client_id=None,
+ client_secret=None,
+ rapt_token=None,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ refresh_handler=refresh_handler,
+ )
+
+ creds.refresh(request)
+
+ assert creds.token == "ACCESS_TOKEN"
+ assert creds.expiry == expected_expiry
+ assert creds.valid
+ assert not creds.expired
+ # Confirm refresh handler called with the expected arguments.
+ refresh_handler.assert_called_with(request, scopes=scopes)
+
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh_with_refresh_handler_success_default_scopes(self, unused_utcnow):
+ expected_expiry = datetime.datetime.min + datetime.timedelta(seconds=2800)
+ original_refresh_handler = mock.Mock(
+ return_value=("UNUSED_TOKEN", expected_expiry)
+ )
+ refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN", expected_expiry))
+ default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=None,
+ token_uri=None,
+ client_id=None,
+ client_secret=None,
+ rapt_token=None,
+ scopes=None,
+ default_scopes=default_scopes,
+ refresh_handler=original_refresh_handler,
+ )
+
+ # Test newly set refresh_handler is used instead of the original one.
+ creds.refresh_handler = refresh_handler
+ creds.refresh(request)
+
+ assert creds.token == "ACCESS_TOKEN"
+ assert creds.expiry == expected_expiry
+ assert creds.valid
+ assert not creds.expired
+ # default_scopes should be used since no developer provided scopes
+ # are provided.
+ refresh_handler.assert_called_with(request, scopes=default_scopes)
+
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh_with_refresh_handler_invalid_token(self, unused_utcnow):
+ expected_expiry = datetime.datetime.min + datetime.timedelta(seconds=2800)
+ # Simulate refresh handler does not return a valid token.
+ refresh_handler = mock.Mock(return_value=(None, expected_expiry))
+ scopes = ["email", "profile"]
+ default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=None,
+ token_uri=None,
+ client_id=None,
+ client_secret=None,
+ rapt_token=None,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ refresh_handler=refresh_handler,
+ )
+
+ with pytest.raises(
+ exceptions.RefreshError, match="returned token is not a string"
+ ):
+ creds.refresh(request)
+
+ assert creds.token is None
+ assert creds.expiry is None
+ assert not creds.valid
+ # Confirm refresh handler called with the expected arguments.
+ refresh_handler.assert_called_with(request, scopes=scopes)
+
+ def test_refresh_with_refresh_handler_invalid_expiry(self):
+ # Simulate refresh handler returns expiration time in an invalid unit.
+ refresh_handler = mock.Mock(return_value=("TOKEN", 2800))
+ scopes = ["email", "profile"]
+ default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=None,
+ token_uri=None,
+ client_id=None,
+ client_secret=None,
+ rapt_token=None,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ refresh_handler=refresh_handler,
+ )
+
+ with pytest.raises(
+ exceptions.RefreshError, match="returned expiry is not a datetime object"
+ ):
+ creds.refresh(request)
+
+ assert creds.token is None
+ assert creds.expiry is None
+ assert not creds.valid
+ # Confirm refresh handler called with the expected arguments.
+ refresh_handler.assert_called_with(request, scopes=scopes)
+
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh_with_refresh_handler_expired_token(self, unused_utcnow):
+ expected_expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD
+ # Simulate refresh handler returns an expired token.
+ refresh_handler = mock.Mock(return_value=("TOKEN", expected_expiry))
+ scopes = ["email", "profile"]
+ default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=None,
+ token_uri=None,
+ client_id=None,
+ client_secret=None,
+ rapt_token=None,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ refresh_handler=refresh_handler,
+ )
+
+ with pytest.raises(exceptions.RefreshError, match="already expired"):
+ creds.refresh(request)
+
+ assert creds.token is None
+ assert creds.expiry is None
+ assert not creds.valid
+ # Confirm refresh handler called with the expected arguments.
+ refresh_handler.assert_called_with(request, scopes=scopes)
+
+ @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+ )
+ def test_credentials_with_scopes_requested_refresh_success(
+ self, unused_utcnow, refresh_grant
+ ):
+ scopes = ["email", "profile"]
+ default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ token = "token"
+ new_rapt_token = "new_rapt_token"
+ expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+ grant_response = {"id_token": mock.sentinel.id_token, "scope": "email profile"}
+ refresh_grant.return_value = (
+ # Access token
+ token,
+ # New refresh token
+ None,
+ # Expiry,
+ expiry,
+ # Extra data
+ grant_response,
+ # rapt token
+ new_rapt_token,
+ )
+
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ rapt_token=self.RAPT_TOKEN,
+ enable_reauth_refresh=True,
+ )
+
+ # Refresh credentials
+ creds.refresh(request)
+
+ # Check jwt grant call.
+ refresh_grant.assert_called_with(
+ request,
+ self.TOKEN_URI,
+ self.REFRESH_TOKEN,
+ self.CLIENT_ID,
+ self.CLIENT_SECRET,
+ scopes,
+ self.RAPT_TOKEN,
+ True,
+ )
+
+ # Check that the credentials have the token and expiry
+ assert creds.token == token
+ assert creds.expiry == expiry
+ assert creds.id_token == mock.sentinel.id_token
+ assert creds.has_scopes(scopes)
+ assert creds.rapt_token == new_rapt_token
+ assert creds.granted_scopes == scopes
+
+ # Check that the credentials are valid (have a token and are not
+ # expired.)
+ assert creds.valid
+
+ @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+ )
+ def test_credentials_with_only_default_scopes_requested(
+ self, unused_utcnow, refresh_grant
+ ):
+ default_scopes = ["email", "profile"]
+ token = "token"
+ new_rapt_token = "new_rapt_token"
+ expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+ grant_response = {"id_token": mock.sentinel.id_token, "scope": "email profile"}
+ refresh_grant.return_value = (
+ # Access token
+ token,
+ # New refresh token
+ None,
+ # Expiry,
+ expiry,
+ # Extra data
+ grant_response,
+ # rapt token
+ new_rapt_token,
+ )
+
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ default_scopes=default_scopes,
+ rapt_token=self.RAPT_TOKEN,
+ enable_reauth_refresh=True,
+ )
+
+ # Refresh credentials
+ creds.refresh(request)
+
+ # Check jwt grant call.
+ refresh_grant.assert_called_with(
+ request,
+ self.TOKEN_URI,
+ self.REFRESH_TOKEN,
+ self.CLIENT_ID,
+ self.CLIENT_SECRET,
+ default_scopes,
+ self.RAPT_TOKEN,
+ True,
+ )
+
+ # Check that the credentials have the token and expiry
+ assert creds.token == token
+ assert creds.expiry == expiry
+ assert creds.id_token == mock.sentinel.id_token
+ assert creds.has_scopes(default_scopes)
+ assert creds.rapt_token == new_rapt_token
+ assert creds.granted_scopes == default_scopes
+
+ # Check that the credentials are valid (have a token and are not
+ # expired.)
+ assert creds.valid
+
+ @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+ )
+ def test_credentials_with_scopes_returned_refresh_success(
+ self, unused_utcnow, refresh_grant
+ ):
+ scopes = ["email", "profile"]
+ token = "token"
+ new_rapt_token = "new_rapt_token"
+ expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+ grant_response = {"id_token": mock.sentinel.id_token, "scope": " ".join(scopes)}
+ refresh_grant.return_value = (
+ # Access token
+ token,
+ # New refresh token
+ None,
+ # Expiry,
+ expiry,
+ # Extra data
+ grant_response,
+ # rapt token
+ new_rapt_token,
+ )
+
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ scopes=scopes,
+ rapt_token=self.RAPT_TOKEN,
+ enable_reauth_refresh=True,
+ )
+
+ # Refresh credentials
+ creds.refresh(request)
+
+ # Check jwt grant call.
+ refresh_grant.assert_called_with(
+ request,
+ self.TOKEN_URI,
+ self.REFRESH_TOKEN,
+ self.CLIENT_ID,
+ self.CLIENT_SECRET,
+ scopes,
+ self.RAPT_TOKEN,
+ True,
+ )
+
+ # Check that the credentials have the token and expiry
+ assert creds.token == token
+ assert creds.expiry == expiry
+ assert creds.id_token == mock.sentinel.id_token
+ assert creds.has_scopes(scopes)
+ assert creds.rapt_token == new_rapt_token
+ assert creds.granted_scopes == scopes
+
+ # Check that the credentials are valid (have a token and are not
+ # expired.)
+ assert creds.valid
+
+ @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+ )
+ def test_credentials_with_only_default_scopes_requested_different_granted_scopes(
+ self, unused_utcnow, refresh_grant
+ ):
+ default_scopes = ["email", "profile"]
+ token = "token"
+ new_rapt_token = "new_rapt_token"
+ expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+ grant_response = {"id_token": mock.sentinel.id_token, "scope": "email"}
+ refresh_grant.return_value = (
+ # Access token
+ token,
+ # New refresh token
+ None,
+ # Expiry,
+ expiry,
+ # Extra data
+ grant_response,
+ # rapt token
+ new_rapt_token,
+ )
+
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ default_scopes=default_scopes,
+ rapt_token=self.RAPT_TOKEN,
+ enable_reauth_refresh=True,
+ )
+
+ # Refresh credentials
+ creds.refresh(request)
+
+ # Check jwt grant call.
+ refresh_grant.assert_called_with(
+ request,
+ self.TOKEN_URI,
+ self.REFRESH_TOKEN,
+ self.CLIENT_ID,
+ self.CLIENT_SECRET,
+ default_scopes,
+ self.RAPT_TOKEN,
+ True,
+ )
+
+ # Check that the credentials have the token and expiry
+ assert creds.token == token
+ assert creds.expiry == expiry
+ assert creds.id_token == mock.sentinel.id_token
+ assert creds.has_scopes(default_scopes)
+ assert creds.rapt_token == new_rapt_token
+ assert creds.granted_scopes == ["email"]
+
+ # Check that the credentials are valid (have a token and are not
+ # expired.)
+ assert creds.valid
+
+ @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+ )
+ def test_credentials_with_scopes_refresh_different_granted_scopes(
+ self, unused_utcnow, refresh_grant
+ ):
+ scopes = ["email", "profile"]
+ scopes_returned = ["email"]
+ token = "token"
+ new_rapt_token = "new_rapt_token"
+ expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+ grant_response = {
+ "id_token": mock.sentinel.id_token,
+ "scope": " ".join(scopes_returned),
+ }
+ refresh_grant.return_value = (
+ # Access token
+ token,
+ # New refresh token
+ None,
+ # Expiry,
+ expiry,
+ # Extra data
+ grant_response,
+ # rapt token
+ new_rapt_token,
+ )
+
+ request = mock.create_autospec(transport.Request)
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ scopes=scopes,
+ rapt_token=self.RAPT_TOKEN,
+ enable_reauth_refresh=True,
+ )
+
+ # Refresh credentials
+ creds.refresh(request)
+
+ # Check jwt grant call.
+ refresh_grant.assert_called_with(
+ request,
+ self.TOKEN_URI,
+ self.REFRESH_TOKEN,
+ self.CLIENT_ID,
+ self.CLIENT_SECRET,
+ scopes,
+ self.RAPT_TOKEN,
+ True,
+ )
+
+ # Check that the credentials have the token and expiry
+ assert creds.token == token
+ assert creds.expiry == expiry
+ assert creds.id_token == mock.sentinel.id_token
+ assert creds.has_scopes(scopes)
+ assert creds.rapt_token == new_rapt_token
+ assert creds.granted_scopes == scopes_returned
+
+ # Check that the credentials are valid (have a token and are not
+ # expired.)
+ assert creds.valid
+
+ def test_apply_with_quota_project_id(self):
+ creds = credentials.Credentials(
+ token="token",
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ quota_project_id="quota-project-123",
+ )
+
+ headers = {}
+ creds.apply(headers)
+ assert headers["x-goog-user-project"] == "quota-project-123"
+ assert "token" in headers["authorization"]
+
+ def test_apply_with_no_quota_project_id(self):
+ creds = credentials.Credentials(
+ token="token",
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ )
+
+ headers = {}
+ creds.apply(headers)
+ assert "x-goog-user-project" not in headers
+ assert "token" in headers["authorization"]
+
+ def test_with_quota_project(self):
+ creds = credentials.Credentials(
+ token="token",
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ quota_project_id="quota-project-123",
+ )
+
+ new_creds = creds.with_quota_project("new-project-456")
+ assert new_creds.quota_project_id == "new-project-456"
+ headers = {}
+ creds.apply(headers)
+ assert "x-goog-user-project" in headers
+
+ def test_with_token_uri(self):
+ info = AUTH_USER_INFO.copy()
+
+ creds = credentials.Credentials.from_authorized_user_info(info)
+ new_token_uri = "https://oauth2-eu.googleapis.com/token"
+
+ assert creds._token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+
+ creds_with_new_token_uri = creds.with_token_uri(new_token_uri)
+
+ assert creds_with_new_token_uri._token_uri == new_token_uri
+
+ def test_from_authorized_user_info(self):
+ info = AUTH_USER_INFO.copy()
+
+ creds = credentials.Credentials.from_authorized_user_info(info)
+ assert creds.client_secret == info["client_secret"]
+ assert creds.client_id == info["client_id"]
+ assert creds.refresh_token == info["refresh_token"]
+ assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+ assert creds.scopes is None
+
+ scopes = ["email", "profile"]
+ creds = credentials.Credentials.from_authorized_user_info(info, scopes)
+ assert creds.client_secret == info["client_secret"]
+ assert creds.client_id == info["client_id"]
+ assert creds.refresh_token == info["refresh_token"]
+ assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+ assert creds.scopes == scopes
+
+ info["scopes"] = "email" # single non-array scope from file
+ creds = credentials.Credentials.from_authorized_user_info(info)
+ assert creds.scopes == [info["scopes"]]
+
+ info["scopes"] = ["email", "profile"] # array scope from file
+ creds = credentials.Credentials.from_authorized_user_info(info)
+ assert creds.scopes == info["scopes"]
+
+ expiry = datetime.datetime(2020, 8, 14, 15, 54, 1)
+ info["expiry"] = expiry.isoformat() + "Z"
+ creds = credentials.Credentials.from_authorized_user_info(info)
+ assert creds.expiry == expiry
+ assert creds.expired
+
+ def test_from_authorized_user_file(self):
+ info = AUTH_USER_INFO.copy()
+
+ creds = credentials.Credentials.from_authorized_user_file(AUTH_USER_JSON_FILE)
+ assert creds.client_secret == info["client_secret"]
+ assert creds.client_id == info["client_id"]
+ assert creds.refresh_token == info["refresh_token"]
+ assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+ assert creds.scopes is None
+ assert creds.rapt_token is None
+
+ scopes = ["email", "profile"]
+ creds = credentials.Credentials.from_authorized_user_file(
+ AUTH_USER_JSON_FILE, scopes
+ )
+ assert creds.client_secret == info["client_secret"]
+ assert creds.client_id == info["client_id"]
+ assert creds.refresh_token == info["refresh_token"]
+ assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+ assert creds.scopes == scopes
+
+ def test_from_authorized_user_file_with_rapt_token(self):
+ info = AUTH_USER_INFO.copy()
+ file_path = os.path.join(DATA_DIR, "authorized_user_with_rapt_token.json")
+
+ creds = credentials.Credentials.from_authorized_user_file(file_path)
+ assert creds.client_secret == info["client_secret"]
+ assert creds.client_id == info["client_id"]
+ assert creds.refresh_token == info["refresh_token"]
+ assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+ assert creds.scopes is None
+ assert creds.rapt_token == "rapt"
+
+ def test_to_json(self):
+ info = AUTH_USER_INFO.copy()
+ expiry = datetime.datetime(2020, 8, 14, 15, 54, 1)
+ info["expiry"] = expiry.isoformat() + "Z"
+ creds = credentials.Credentials.from_authorized_user_info(info)
+ assert creds.expiry == expiry
+
+ # Test with no `strip` arg
+ json_output = creds.to_json()
+ json_asdict = json.loads(json_output)
+ assert json_asdict.get("token") == creds.token
+ assert json_asdict.get("refresh_token") == creds.refresh_token
+ assert json_asdict.get("token_uri") == creds.token_uri
+ assert json_asdict.get("client_id") == creds.client_id
+ assert json_asdict.get("scopes") == creds.scopes
+ assert json_asdict.get("client_secret") == creds.client_secret
+ assert json_asdict.get("expiry") == info["expiry"]
+
+ # Test with a `strip` arg
+ json_output = creds.to_json(strip=["client_secret"])
+ json_asdict = json.loads(json_output)
+ assert json_asdict.get("token") == creds.token
+ assert json_asdict.get("refresh_token") == creds.refresh_token
+ assert json_asdict.get("token_uri") == creds.token_uri
+ assert json_asdict.get("client_id") == creds.client_id
+ assert json_asdict.get("scopes") == creds.scopes
+ assert json_asdict.get("client_secret") is None
+
+ # Test with no expiry
+ creds.expiry = None
+ json_output = creds.to_json()
+ json_asdict = json.loads(json_output)
+ assert json_asdict.get("expiry") is None
+
+ def test_pickle_and_unpickle(self):
+ creds = self.make_credentials()
+ unpickled = pickle.loads(pickle.dumps(creds))
+
+ # make sure attributes aren't lost during pickling
+ assert list(creds.__dict__).sort() == list(unpickled.__dict__).sort()
+
+ for attr in list(creds.__dict__):
+ assert getattr(creds, attr) == getattr(unpickled, attr)
+
+ def test_pickle_and_unpickle_with_refresh_handler(self):
+ expected_expiry = _helpers.utcnow() + datetime.timedelta(seconds=2800)
+ refresh_handler = mock.Mock(return_value=("TOKEN", expected_expiry))
+
+ creds = credentials.Credentials(
+ token=None,
+ refresh_token=None,
+ token_uri=None,
+ client_id=None,
+ client_secret=None,
+ rapt_token=None,
+ refresh_handler=refresh_handler,
+ )
+ unpickled = pickle.loads(pickle.dumps(creds))
+
+ # make sure attributes aren't lost during pickling
+ assert list(creds.__dict__).sort() == list(unpickled.__dict__).sort()
+
+ for attr in list(creds.__dict__):
+ # For the _refresh_handler property, the unpickled creds should be
+ # set to None.
+ if attr == "_refresh_handler":
+ assert getattr(unpickled, attr) is None
+ else:
+ assert getattr(creds, attr) == getattr(unpickled, attr)
+
+ def test_pickle_with_missing_attribute(self):
+ creds = self.make_credentials()
+
+ # remove an optional attribute before pickling
+ # this mimics a pickle created with a previous class definition with
+ # fewer attributes
+ del creds.__dict__["_quota_project_id"]
+
+ unpickled = pickle.loads(pickle.dumps(creds))
+
+ # Attribute should be initialized by `__setstate__`
+ assert unpickled.quota_project_id is None
+
+ # pickles are not compatible across versions
+ @pytest.mark.skipif(
+ sys.version_info < (3, 5),
+ reason="pickle file can only be loaded with Python >= 3.5",
+ )
+ def test_unpickle_old_credentials_pickle(self):
+ # make sure a credentials file pickled with an older
+ # library version (google-auth==1.5.1) can be unpickled
+ with open(
+ os.path.join(DATA_DIR, "old_oauth_credentials_py3.pickle"), "rb"
+ ) as f:
+ credentials = pickle.load(f)
+ assert credentials.quota_project_id is None
+
+
+class TestUserAccessTokenCredentials(object):
+ def test_instance(self):
+ with pytest.warns(
+ UserWarning, match="UserAccessTokenCredentials is deprecated"
+ ):
+ cred = credentials.UserAccessTokenCredentials()
+ assert cred._account is None
+
+ cred = cred.with_account("account")
+ assert cred._account == "account"
+
+ @mock.patch("google.auth._cloud_sdk.get_auth_access_token", autospec=True)
+ def test_refresh(self, get_auth_access_token):
+ with pytest.warns(
+ UserWarning, match="UserAccessTokenCredentials is deprecated"
+ ):
+ get_auth_access_token.return_value = "access_token"
+ cred = credentials.UserAccessTokenCredentials()
+ cred.refresh(None)
+ assert cred.token == "access_token"
+
+ def test_with_quota_project(self):
+ with pytest.warns(
+ UserWarning, match="UserAccessTokenCredentials is deprecated"
+ ):
+ cred = credentials.UserAccessTokenCredentials()
+ quota_project_cred = cred.with_quota_project("project-foo")
+
+ assert quota_project_cred._quota_project_id == "project-foo"
+ assert quota_project_cred._account == cred._account
+
+ @mock.patch(
+ "google.oauth2.credentials.UserAccessTokenCredentials.apply", autospec=True
+ )
+ @mock.patch(
+ "google.oauth2.credentials.UserAccessTokenCredentials.refresh", autospec=True
+ )
+ def test_before_request(self, refresh, apply):
+ with pytest.warns(
+ UserWarning, match="UserAccessTokenCredentials is deprecated"
+ ):
+ cred = credentials.UserAccessTokenCredentials()
+ cred.before_request(mock.Mock(), "GET", "https://example.com", {})
+ refresh.assert_called()
+ apply.assert_called()
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_gdch_credentials.py b/contrib/python/google-auth/py3/tests/oauth2/test_gdch_credentials.py
new file mode 100644
index 0000000000..1ff61d8683
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_gdch_credentials.py
@@ -0,0 +1,175 @@
+# Copyright 2022 Google LLC
+#
+# 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 copy
+import datetime
+import json
+import os
+
+import mock
+import pytest # type: ignore
+import requests
+
+from google.auth import exceptions
+from google.auth import jwt
+import google.auth.transport.requests
+from google.oauth2 import gdch_credentials
+from google.oauth2.gdch_credentials import ServiceAccountCredentials
+
+import yatest.common
+
+
+class TestServiceAccountCredentials(object):
+ AUDIENCE = "https://service-identity.<Domain>/authenticate"
+ PROJECT = "project_foo"
+ PRIVATE_KEY_ID = "key_foo"
+ NAME = "service_identity_name"
+ CA_CERT_PATH = "/path/to/ca/cert"
+ TOKEN_URI = "https://service-identity.<Domain>/authenticate"
+
+ JSON_PATH = os.path.join(
+ yatest.common.test_source_path(), "data", "gdch_service_account.json"
+ )
+ with open(JSON_PATH, "rb") as fh:
+ INFO = json.load(fh)
+
+ def test_with_gdch_audience(self):
+ mock_signer = mock.Mock()
+ creds = ServiceAccountCredentials._from_signer_and_info(mock_signer, self.INFO)
+ assert creds._signer == mock_signer
+ assert creds._service_identity_name == self.NAME
+ assert creds._audience is None
+ assert creds._token_uri == self.TOKEN_URI
+ assert creds._ca_cert_path == self.CA_CERT_PATH
+
+ new_creds = creds.with_gdch_audience(self.AUDIENCE)
+ assert new_creds._signer == mock_signer
+ assert new_creds._service_identity_name == self.NAME
+ assert new_creds._audience == self.AUDIENCE
+ assert new_creds._token_uri == self.TOKEN_URI
+ assert new_creds._ca_cert_path == self.CA_CERT_PATH
+
+ def test__create_jwt(self):
+ creds = ServiceAccountCredentials.from_service_account_file(self.JSON_PATH)
+ with mock.patch("google.auth._helpers.utcnow") as utcnow:
+ utcnow.return_value = datetime.datetime.now()
+ jwt_token = creds._create_jwt()
+ header, payload, _, _ = jwt._unverified_decode(jwt_token)
+
+ expected_iss_sub_value = (
+ "system:serviceaccount:project_foo:service_identity_name"
+ )
+ assert isinstance(jwt_token, str)
+ assert header["alg"] == "ES256"
+ assert header["kid"] == self.PRIVATE_KEY_ID
+ assert payload["iss"] == expected_iss_sub_value
+ assert payload["sub"] == expected_iss_sub_value
+ assert payload["aud"] == self.AUDIENCE
+ assert payload["exp"] == (payload["iat"] + 3600)
+
+ @mock.patch(
+ "google.oauth2.gdch_credentials.ServiceAccountCredentials._create_jwt",
+ autospec=True,
+ )
+ @mock.patch("google.oauth2._client._token_endpoint_request", autospec=True)
+ def test_refresh(self, token_endpoint_request, create_jwt):
+ creds = ServiceAccountCredentials.from_service_account_info(self.INFO)
+ creds = creds.with_gdch_audience(self.AUDIENCE)
+ req = google.auth.transport.requests.Request()
+
+ mock_jwt_token = "jwt token"
+ create_jwt.return_value = mock_jwt_token
+ sts_token = "STS token"
+ token_endpoint_request.return_value = {
+ "access_token": sts_token,
+ "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ }
+
+ creds.refresh(req)
+
+ token_endpoint_request.assert_called_with(
+ req,
+ self.TOKEN_URI,
+ {
+ "grant_type": gdch_credentials.TOKEN_EXCHANGE_TYPE,
+ "audience": self.AUDIENCE,
+ "requested_token_type": gdch_credentials.ACCESS_TOKEN_TOKEN_TYPE,
+ "subject_token": mock_jwt_token,
+ "subject_token_type": gdch_credentials.SERVICE_ACCOUNT_TOKEN_TYPE,
+ },
+ access_token=None,
+ use_json=True,
+ verify=self.CA_CERT_PATH,
+ )
+ assert creds.token == sts_token
+
+ def test_refresh_wrong_requests_object(self):
+ creds = ServiceAccountCredentials.from_service_account_info(self.INFO)
+ creds = creds.with_gdch_audience(self.AUDIENCE)
+ req = requests.Request()
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ creds.refresh(req)
+ assert excinfo.match(
+ "request must be a google.auth.transport.requests.Request object"
+ )
+
+ def test__from_signer_and_info_wrong_format_version(self):
+ with pytest.raises(ValueError) as excinfo:
+ ServiceAccountCredentials._from_signer_and_info(
+ mock.Mock(), {"format_version": "2"}
+ )
+ assert excinfo.match("Only format version 1 is supported")
+
+ def test_from_service_account_info_miss_field(self):
+ for field in [
+ "format_version",
+ "private_key_id",
+ "private_key",
+ "name",
+ "project",
+ "token_uri",
+ ]:
+ info_with_missing_field = copy.deepcopy(self.INFO)
+ del info_with_missing_field[field]
+ with pytest.raises(ValueError) as excinfo:
+ ServiceAccountCredentials.from_service_account_info(
+ info_with_missing_field
+ )
+ assert excinfo.match("missing fields")
+
+ @mock.patch("google.auth._service_account_info.from_filename")
+ def test_from_service_account_file(self, from_filename):
+ mock_signer = mock.Mock()
+ from_filename.return_value = (self.INFO, mock_signer)
+ creds = ServiceAccountCredentials.from_service_account_file(self.JSON_PATH)
+ from_filename.assert_called_with(
+ self.JSON_PATH,
+ require=[
+ "format_version",
+ "private_key_id",
+ "private_key",
+ "name",
+ "project",
+ "token_uri",
+ ],
+ use_rsa_signer=False,
+ )
+ assert creds._signer == mock_signer
+ assert creds._service_identity_name == self.NAME
+ assert creds._audience is None
+ assert creds._token_uri == self.TOKEN_URI
+ assert creds._ca_cert_path == self.CA_CERT_PATH
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_id_token.py b/contrib/python/google-auth/py3/tests/oauth2/test_id_token.py
new file mode 100644
index 0000000000..861f76ce4f
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_id_token.py
@@ -0,0 +1,312 @@
+# Copyright 2014 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+
+import mock
+import pytest # type: ignore
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+from google.oauth2 import id_token
+from google.oauth2 import service_account
+
+import yatest.common
+SERVICE_ACCOUNT_FILE = os.path.join(
+ yatest.common.test_source_path(), "data/service_account.json"
+)
+ID_TOKEN_AUDIENCE = "https://pubsub.googleapis.com"
+
+
+def make_request(status, data=None):
+ response = mock.create_autospec(transport.Response, instance=True)
+ response.status = status
+
+ if data is not None:
+ response.data = json.dumps(data).encode("utf-8")
+
+ request = mock.create_autospec(transport.Request)
+ request.return_value = response
+ return request
+
+
+def test__fetch_certs_success():
+ certs = {"1": "cert"}
+ request = make_request(200, certs)
+
+ returned_certs = id_token._fetch_certs(request, mock.sentinel.cert_url)
+
+ request.assert_called_once_with(mock.sentinel.cert_url, method="GET")
+ assert returned_certs == certs
+
+
+def test__fetch_certs_failure():
+ request = make_request(404)
+
+ with pytest.raises(exceptions.TransportError):
+ id_token._fetch_certs(request, mock.sentinel.cert_url)
+
+ request.assert_called_once_with(mock.sentinel.cert_url, method="GET")
+
+
+@mock.patch("google.auth.jwt.decode", autospec=True)
+@mock.patch("google.oauth2.id_token._fetch_certs", autospec=True)
+def test_verify_token(_fetch_certs, decode):
+ result = id_token.verify_token(mock.sentinel.token, mock.sentinel.request)
+
+ assert result == decode.return_value
+ _fetch_certs.assert_called_once_with(
+ mock.sentinel.request, id_token._GOOGLE_OAUTH2_CERTS_URL
+ )
+ decode.assert_called_once_with(
+ mock.sentinel.token,
+ certs=_fetch_certs.return_value,
+ audience=None,
+ clock_skew_in_seconds=0,
+ )
+
+
+@mock.patch("google.auth.jwt.decode", autospec=True)
+@mock.patch("google.oauth2.id_token._fetch_certs", autospec=True)
+def test_verify_token_args(_fetch_certs, decode):
+ result = id_token.verify_token(
+ mock.sentinel.token,
+ mock.sentinel.request,
+ audience=mock.sentinel.audience,
+ certs_url=mock.sentinel.certs_url,
+ )
+
+ assert result == decode.return_value
+ _fetch_certs.assert_called_once_with(mock.sentinel.request, mock.sentinel.certs_url)
+ decode.assert_called_once_with(
+ mock.sentinel.token,
+ certs=_fetch_certs.return_value,
+ audience=mock.sentinel.audience,
+ clock_skew_in_seconds=0,
+ )
+
+
+@mock.patch("google.auth.jwt.decode", autospec=True)
+@mock.patch("google.oauth2.id_token._fetch_certs", autospec=True)
+def test_verify_token_clock_skew(_fetch_certs, decode):
+ result = id_token.verify_token(
+ mock.sentinel.token,
+ mock.sentinel.request,
+ audience=mock.sentinel.audience,
+ certs_url=mock.sentinel.certs_url,
+ clock_skew_in_seconds=10,
+ )
+
+ assert result == decode.return_value
+ _fetch_certs.assert_called_once_with(mock.sentinel.request, mock.sentinel.certs_url)
+ decode.assert_called_once_with(
+ mock.sentinel.token,
+ certs=_fetch_certs.return_value,
+ audience=mock.sentinel.audience,
+ clock_skew_in_seconds=10,
+ )
+
+
+@mock.patch("google.oauth2.id_token.verify_token", autospec=True)
+def test_verify_oauth2_token(verify_token):
+ verify_token.return_value = {"iss": "accounts.google.com"}
+ result = id_token.verify_oauth2_token(
+ mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience
+ )
+
+ assert result == verify_token.return_value
+ verify_token.assert_called_once_with(
+ mock.sentinel.token,
+ mock.sentinel.request,
+ audience=mock.sentinel.audience,
+ certs_url=id_token._GOOGLE_OAUTH2_CERTS_URL,
+ clock_skew_in_seconds=0,
+ )
+
+
+@mock.patch("google.oauth2.id_token.verify_token", autospec=True)
+def test_verify_oauth2_token_clock_skew(verify_token):
+ verify_token.return_value = {"iss": "accounts.google.com"}
+ result = id_token.verify_oauth2_token(
+ mock.sentinel.token,
+ mock.sentinel.request,
+ audience=mock.sentinel.audience,
+ clock_skew_in_seconds=10,
+ )
+
+ assert result == verify_token.return_value
+ verify_token.assert_called_once_with(
+ mock.sentinel.token,
+ mock.sentinel.request,
+ audience=mock.sentinel.audience,
+ certs_url=id_token._GOOGLE_OAUTH2_CERTS_URL,
+ clock_skew_in_seconds=10,
+ )
+
+
+@mock.patch("google.oauth2.id_token.verify_token", autospec=True)
+def test_verify_oauth2_token_invalid_iss(verify_token):
+ verify_token.return_value = {"iss": "invalid_issuer"}
+
+ with pytest.raises(exceptions.GoogleAuthError):
+ id_token.verify_oauth2_token(
+ mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience
+ )
+
+
+@mock.patch("google.oauth2.id_token.verify_token", autospec=True)
+def test_verify_firebase_token(verify_token):
+ result = id_token.verify_firebase_token(
+ mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience
+ )
+
+ assert result == verify_token.return_value
+ verify_token.assert_called_once_with(
+ mock.sentinel.token,
+ mock.sentinel.request,
+ audience=mock.sentinel.audience,
+ certs_url=id_token._GOOGLE_APIS_CERTS_URL,
+ clock_skew_in_seconds=0,
+ )
+
+
+@mock.patch("google.oauth2.id_token.verify_token", autospec=True)
+def test_verify_firebase_token_clock_skew(verify_token):
+ result = id_token.verify_firebase_token(
+ mock.sentinel.token,
+ mock.sentinel.request,
+ audience=mock.sentinel.audience,
+ clock_skew_in_seconds=10,
+ )
+
+ assert result == verify_token.return_value
+ verify_token.assert_called_once_with(
+ mock.sentinel.token,
+ mock.sentinel.request,
+ audience=mock.sentinel.audience,
+ certs_url=id_token._GOOGLE_APIS_CERTS_URL,
+ clock_skew_in_seconds=10,
+ )
+
+
+def test_fetch_id_token_credentials_optional_request(monkeypatch):
+ monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
+
+ # Test a request object is created if not provided
+ with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True):
+ with mock.patch(
+ "google.auth.compute_engine.IDTokenCredentials.__init__", return_value=None
+ ):
+ with mock.patch(
+ "google.auth.transport.requests.Request.__init__", return_value=None
+ ) as mock_request:
+ id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+ mock_request.assert_called()
+
+
+def test_fetch_id_token_credentials_from_metadata_server(monkeypatch):
+ monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
+
+ mock_req = mock.Mock()
+
+ with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True):
+ with mock.patch(
+ "google.auth.compute_engine.IDTokenCredentials.__init__", return_value=None
+ ) as mock_init:
+ id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE, request=mock_req)
+ mock_init.assert_called_once_with(
+ mock_req, ID_TOKEN_AUDIENCE, use_metadata_identity_endpoint=True
+ )
+
+
+def test_fetch_id_token_credentials_from_explicit_cred_json_file(monkeypatch):
+ monkeypatch.setenv(environment_vars.CREDENTIALS, SERVICE_ACCOUNT_FILE)
+
+ cred = id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+ assert isinstance(cred, service_account.IDTokenCredentials)
+ assert cred._target_audience == ID_TOKEN_AUDIENCE
+
+
+def test_fetch_id_token_credentials_no_cred_exists(monkeypatch):
+ monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
+
+ with mock.patch(
+ "google.auth.compute_engine._metadata.ping",
+ side_effect=exceptions.TransportError(),
+ ):
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+ assert excinfo.match(
+ r"Neither metadata server or valid service account credentials are found."
+ )
+
+ with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+ assert excinfo.match(
+ r"Neither metadata server or valid service account credentials are found."
+ )
+
+
+def test_fetch_id_token_credentials_invalid_cred_file_type(monkeypatch):
+ user_credentials_file = os.path.join(
+ yatest.common.test_source_path(), "data/authorized_user.json"
+ )
+ monkeypatch.setenv(environment_vars.CREDENTIALS, user_credentials_file)
+
+ with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+ assert excinfo.match(
+ r"Neither metadata server or valid service account credentials are found."
+ )
+
+
+def test_fetch_id_token_credentials_invalid_json(monkeypatch):
+ not_json_file = os.path.join(yatest.common.test_source_path(), "data/public_cert.pem")
+ monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+ assert excinfo.match(
+ r"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials."
+ )
+
+
+def test_fetch_id_token_credentials_invalid_cred_path(monkeypatch):
+ not_json_file = os.path.join(yatest.common.test_source_path(), "data/not_exists.json")
+ monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+ assert excinfo.match(
+ r"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
+ )
+
+
+def test_fetch_id_token(monkeypatch):
+ mock_cred = mock.MagicMock()
+ mock_cred.token = "token"
+
+ mock_req = mock.Mock()
+
+ with mock.patch(
+ "google.oauth2.id_token.fetch_id_token_credentials", return_value=mock_cred
+ ) as mock_fetch:
+ token = id_token.fetch_id_token(mock_req, ID_TOKEN_AUDIENCE)
+ mock_fetch.assert_called_once_with(ID_TOKEN_AUDIENCE, request=mock_req)
+ mock_cred.refresh.assert_called_once_with(mock_req)
+ assert token == "token"
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_reauth.py b/contrib/python/google-auth/py3/tests/oauth2/test_reauth.py
new file mode 100644
index 0000000000..5b15ad3b56
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_reauth.py
@@ -0,0 +1,388 @@
+# Copyright 2021 Google LLC
+#
+# 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 copy
+
+import mock
+import pytest # type: ignore
+
+from google.auth import exceptions
+from google.oauth2 import reauth
+
+
+MOCK_REQUEST = mock.Mock()
+CHALLENGES_RESPONSE_TEMPLATE = {
+ "status": "CHALLENGE_REQUIRED",
+ "sessionId": "123",
+ "challenges": [
+ {
+ "status": "READY",
+ "challengeId": 1,
+ "challengeType": "PASSWORD",
+ "securityKey": {},
+ }
+ ],
+}
+CHALLENGES_RESPONSE_AUTHENTICATED = {
+ "status": "AUTHENTICATED",
+ "sessionId": "123",
+ "encodedProofOfReauthToken": "new_rapt_token",
+}
+
+REAUTH_START_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 auth-request-type/re-start"
+REAUTH_CONTINUE_METRICS_HEADER_VALUE = (
+ "gl-python/3.7 auth/1.1 auth-request-type/re-cont"
+)
+TOKEN_REQUEST_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 cred-type/u"
+
+
+class MockChallenge(object):
+ def __init__(self, name, locally_eligible, challenge_input):
+ self.name = name
+ self.is_locally_eligible = locally_eligible
+ self.challenge_input = challenge_input
+
+ def obtain_challenge_input(self, metadata):
+ return self.challenge_input
+
+
+def _test_is_interactive():
+ with mock.patch("sys.stdin.isatty", return_value=True):
+ assert reauth.is_interactive()
+
+
+@mock.patch(
+ "google.auth.metrics.reauth_start", return_value=REAUTH_START_METRICS_HEADER_VALUE
+)
+def test__get_challenges(mock_metrics_header_value):
+ with mock.patch(
+ "google.oauth2._client._token_endpoint_request"
+ ) as mock_token_endpoint_request:
+ reauth._get_challenges(MOCK_REQUEST, ["SAML"], "token")
+ mock_token_endpoint_request.assert_called_with(
+ MOCK_REQUEST,
+ reauth._REAUTH_API + ":start",
+ {"supportedChallengeTypes": ["SAML"]},
+ access_token="token",
+ use_json=True,
+ headers={"x-goog-api-client": REAUTH_START_METRICS_HEADER_VALUE},
+ )
+
+
+@mock.patch(
+ "google.auth.metrics.reauth_start", return_value=REAUTH_START_METRICS_HEADER_VALUE
+)
+def test__get_challenges_with_scopes(mock_metrics_header_value):
+ with mock.patch(
+ "google.oauth2._client._token_endpoint_request"
+ ) as mock_token_endpoint_request:
+ reauth._get_challenges(
+ MOCK_REQUEST, ["SAML"], "token", requested_scopes=["scope"]
+ )
+ mock_token_endpoint_request.assert_called_with(
+ MOCK_REQUEST,
+ reauth._REAUTH_API + ":start",
+ {
+ "supportedChallengeTypes": ["SAML"],
+ "oauthScopesForDomainPolicyLookup": ["scope"],
+ },
+ access_token="token",
+ use_json=True,
+ headers={"x-goog-api-client": REAUTH_START_METRICS_HEADER_VALUE},
+ )
+
+
+@mock.patch(
+ "google.auth.metrics.reauth_continue",
+ return_value=REAUTH_CONTINUE_METRICS_HEADER_VALUE,
+)
+def test__send_challenge_result(mock_metrics_header_value):
+ with mock.patch(
+ "google.oauth2._client._token_endpoint_request"
+ ) as mock_token_endpoint_request:
+ reauth._send_challenge_result(
+ MOCK_REQUEST, "123", "1", {"credential": "password"}, "token"
+ )
+ mock_token_endpoint_request.assert_called_with(
+ MOCK_REQUEST,
+ reauth._REAUTH_API + "/123:continue",
+ {
+ "sessionId": "123",
+ "challengeId": "1",
+ "action": "RESPOND",
+ "proposalResponse": {"credential": "password"},
+ },
+ access_token="token",
+ use_json=True,
+ headers={"x-goog-api-client": REAUTH_CONTINUE_METRICS_HEADER_VALUE},
+ )
+
+
+def test__run_next_challenge_not_ready():
+ challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
+ challenges_response["challenges"][0]["status"] = "STATUS_UNSPECIFIED"
+ assert (
+ reauth._run_next_challenge(challenges_response, MOCK_REQUEST, "token") is None
+ )
+
+
+def test__run_next_challenge_not_supported():
+ challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
+ challenges_response["challenges"][0]["challengeType"] = "CHALLENGE_TYPE_UNSPECIFIED"
+ with pytest.raises(exceptions.ReauthFailError) as excinfo:
+ reauth._run_next_challenge(challenges_response, MOCK_REQUEST, "token")
+ assert excinfo.match(r"Unsupported challenge type CHALLENGE_TYPE_UNSPECIFIED")
+
+
+def test__run_next_challenge_not_locally_eligible():
+ mock_challenge = MockChallenge("PASSWORD", False, "challenge_input")
+ with mock.patch(
+ "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
+ ):
+ with pytest.raises(exceptions.ReauthFailError) as excinfo:
+ reauth._run_next_challenge(
+ CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
+ )
+ assert excinfo.match(r"Challenge PASSWORD is not locally eligible")
+
+
+def test__run_next_challenge_no_challenge_input():
+ mock_challenge = MockChallenge("PASSWORD", True, None)
+ with mock.patch(
+ "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
+ ):
+ assert (
+ reauth._run_next_challenge(
+ CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
+ )
+ is None
+ )
+
+
+def test__run_next_challenge_success():
+ mock_challenge = MockChallenge("PASSWORD", True, {"credential": "password"})
+ with mock.patch(
+ "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
+ ):
+ with mock.patch(
+ "google.oauth2.reauth._send_challenge_result"
+ ) as mock_send_challenge_result:
+ reauth._run_next_challenge(
+ CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
+ )
+ mock_send_challenge_result.assert_called_with(
+ MOCK_REQUEST, "123", 1, {"credential": "password"}, "token"
+ )
+
+
+def test__obtain_rapt_authenticated():
+ with mock.patch(
+ "google.oauth2.reauth._get_challenges",
+ return_value=CHALLENGES_RESPONSE_AUTHENTICATED,
+ ):
+ assert reauth._obtain_rapt(MOCK_REQUEST, "token", None) == "new_rapt_token"
+
+
+def test__obtain_rapt_authenticated_after_run_next_challenge():
+ with mock.patch(
+ "google.oauth2.reauth._get_challenges",
+ return_value=CHALLENGES_RESPONSE_TEMPLATE,
+ ):
+ with mock.patch(
+ "google.oauth2.reauth._run_next_challenge",
+ side_effect=[
+ CHALLENGES_RESPONSE_TEMPLATE,
+ CHALLENGES_RESPONSE_AUTHENTICATED,
+ ],
+ ):
+ with mock.patch("google.oauth2.reauth.is_interactive", return_value=True):
+ assert (
+ reauth._obtain_rapt(MOCK_REQUEST, "token", None) == "new_rapt_token"
+ )
+
+
+def test__obtain_rapt_unsupported_status():
+ challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
+ challenges_response["status"] = "STATUS_UNSPECIFIED"
+ with mock.patch(
+ "google.oauth2.reauth._get_challenges", return_value=challenges_response
+ ):
+ with pytest.raises(exceptions.ReauthFailError) as excinfo:
+ reauth._obtain_rapt(MOCK_REQUEST, "token", None)
+ assert excinfo.match(r"API error: STATUS_UNSPECIFIED")
+
+
+def test__obtain_rapt_no_challenge_output():
+ challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
+ with mock.patch(
+ "google.oauth2.reauth._get_challenges", return_value=challenges_response
+ ):
+ with mock.patch("google.oauth2.reauth.is_interactive", return_value=True):
+ with mock.patch(
+ "google.oauth2.reauth._run_next_challenge", return_value=None
+ ):
+ with pytest.raises(exceptions.ReauthFailError) as excinfo:
+ reauth._obtain_rapt(MOCK_REQUEST, "token", None)
+ assert excinfo.match(r"Failed to obtain rapt token")
+
+
+def test__obtain_rapt_not_interactive():
+ with mock.patch(
+ "google.oauth2.reauth._get_challenges",
+ return_value=CHALLENGES_RESPONSE_TEMPLATE,
+ ):
+ with mock.patch("google.oauth2.reauth.is_interactive", return_value=False):
+ with pytest.raises(exceptions.ReauthFailError) as excinfo:
+ reauth._obtain_rapt(MOCK_REQUEST, "token", None)
+ assert excinfo.match(r"not in an interactive session")
+
+
+def test__obtain_rapt_not_authenticated():
+ with mock.patch(
+ "google.oauth2.reauth._get_challenges",
+ return_value=CHALLENGES_RESPONSE_TEMPLATE,
+ ):
+ with mock.patch("google.oauth2.reauth.RUN_CHALLENGE_RETRY_LIMIT", 0):
+ with pytest.raises(exceptions.ReauthFailError) as excinfo:
+ reauth._obtain_rapt(MOCK_REQUEST, "token", None)
+ assert excinfo.match(r"Reauthentication failed")
+
+
+def test_get_rapt_token():
+ with mock.patch(
+ "google.oauth2._client.refresh_grant", return_value=("token", None, None, None)
+ ) as mock_refresh_grant:
+ with mock.patch(
+ "google.oauth2.reauth._obtain_rapt", return_value="new_rapt_token"
+ ) as mock_obtain_rapt:
+ assert (
+ reauth.get_rapt_token(
+ MOCK_REQUEST,
+ "client_id",
+ "client_secret",
+ "refresh_token",
+ "token_uri",
+ )
+ == "new_rapt_token"
+ )
+ mock_refresh_grant.assert_called_with(
+ request=MOCK_REQUEST,
+ client_id="client_id",
+ client_secret="client_secret",
+ refresh_token="refresh_token",
+ token_uri="token_uri",
+ scopes=[reauth._REAUTH_SCOPE],
+ )
+ mock_obtain_rapt.assert_called_with(
+ MOCK_REQUEST, "token", requested_scopes=None
+ )
+
+
+@mock.patch(
+ "google.auth.metrics.token_request_user",
+ return_value=TOKEN_REQUEST_METRICS_HEADER_VALUE,
+)
+def test_refresh_grant_failed(mock_metrics_header_value):
+ with mock.patch(
+ "google.oauth2._client._token_endpoint_request_no_throw"
+ ) as mock_token_request:
+ mock_token_request.return_value = (False, {"error": "Bad request"}, False)
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ reauth.refresh_grant(
+ MOCK_REQUEST,
+ "token_uri",
+ "refresh_token",
+ "client_id",
+ "client_secret",
+ scopes=["foo", "bar"],
+ rapt_token="rapt_token",
+ enable_reauth_refresh=True,
+ )
+ assert excinfo.match(r"Bad request")
+ assert not excinfo.value.retryable
+ mock_token_request.assert_called_with(
+ MOCK_REQUEST,
+ "token_uri",
+ {
+ "grant_type": "refresh_token",
+ "client_id": "client_id",
+ "client_secret": "client_secret",
+ "refresh_token": "refresh_token",
+ "scope": "foo bar",
+ "rapt": "rapt_token",
+ },
+ headers={"x-goog-api-client": TOKEN_REQUEST_METRICS_HEADER_VALUE},
+ )
+
+
+def test_refresh_grant_failed_with_string_type_response():
+ with mock.patch(
+ "google.oauth2._client._token_endpoint_request_no_throw"
+ ) as mock_token_request:
+ mock_token_request.return_value = (False, "string type error", False)
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ reauth.refresh_grant(
+ MOCK_REQUEST,
+ "token_uri",
+ "refresh_token",
+ "client_id",
+ "client_secret",
+ scopes=["foo", "bar"],
+ rapt_token="rapt_token",
+ enable_reauth_refresh=True,
+ )
+ assert excinfo.match(r"string type error")
+ assert not excinfo.value.retryable
+
+
+def test_refresh_grant_success():
+ with mock.patch(
+ "google.oauth2._client._token_endpoint_request_no_throw"
+ ) as mock_token_request:
+ mock_token_request.side_effect = [
+ (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True),
+ (True, {"access_token": "access_token"}, None),
+ ]
+ with mock.patch(
+ "google.oauth2.reauth.get_rapt_token", return_value="new_rapt_token"
+ ):
+ assert reauth.refresh_grant(
+ MOCK_REQUEST,
+ "token_uri",
+ "refresh_token",
+ "client_id",
+ "client_secret",
+ enable_reauth_refresh=True,
+ ) == (
+ "access_token",
+ "refresh_token",
+ None,
+ {"access_token": "access_token"},
+ "new_rapt_token",
+ )
+
+
+def test_refresh_grant_reauth_refresh_disabled():
+ with mock.patch(
+ "google.oauth2._client._token_endpoint_request_no_throw"
+ ) as mock_token_request:
+ mock_token_request.side_effect = [
+ (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True),
+ (True, {"access_token": "access_token"}, None),
+ ]
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ reauth.refresh_grant(
+ MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret"
+ )
+ assert excinfo.match(r"Reauthentication is needed")
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_service_account.py b/contrib/python/google-auth/py3/tests/oauth2/test_service_account.py
new file mode 100644
index 0000000000..c474c90e6b
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_service_account.py
@@ -0,0 +1,789 @@
+# Copyright 2016 Google LLC
+#
+# 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 datetime
+import json
+import os
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth import transport
+from google.oauth2 import service_account
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+ PRIVATE_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+ PUBLIC_CERT_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "other_cert.pem"), "rb") as fh:
+ OTHER_CERT_BYTES = fh.read()
+
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+SERVICE_ACCOUNT_NON_GDU_JSON_FILE = os.path.join(
+ DATA_DIR, "service_account_non_gdu.json"
+)
+FAKE_UNIVERSE_DOMAIN = "universe.foo"
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh:
+ SERVICE_ACCOUNT_INFO = json.load(fh)
+
+with open(SERVICE_ACCOUNT_NON_GDU_JSON_FILE, "rb") as fh:
+ SERVICE_ACCOUNT_INFO_NON_GDU = json.load(fh)
+
+SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1")
+
+
+class TestCredentials(object):
+ SERVICE_ACCOUNT_EMAIL = "service-account@example.com"
+ TOKEN_URI = "https://example.com/oauth2/token"
+
+ @classmethod
+ def make_credentials(cls, universe_domain=service_account._DEFAULT_UNIVERSE_DOMAIN):
+ return service_account.Credentials(
+ SIGNER,
+ cls.SERVICE_ACCOUNT_EMAIL,
+ cls.TOKEN_URI,
+ universe_domain=universe_domain,
+ )
+
+ def test_constructor_no_universe_domain(self):
+ credentials = service_account.Credentials(
+ SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, universe_domain=None
+ )
+ assert credentials.universe_domain == service_account._DEFAULT_UNIVERSE_DOMAIN
+
+ def test_from_service_account_info(self):
+ credentials = service_account.Credentials.from_service_account_info(
+ SERVICE_ACCOUNT_INFO
+ )
+
+ assert credentials._signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"]
+ assert credentials.service_account_email == SERVICE_ACCOUNT_INFO["client_email"]
+ assert credentials._token_uri == SERVICE_ACCOUNT_INFO["token_uri"]
+ assert credentials._universe_domain == service_account._DEFAULT_UNIVERSE_DOMAIN
+ assert not credentials._always_use_jwt_access
+
+ def test_from_service_account_info_non_gdu(self):
+ credentials = service_account.Credentials.from_service_account_info(
+ SERVICE_ACCOUNT_INFO_NON_GDU
+ )
+
+ assert credentials.universe_domain == FAKE_UNIVERSE_DOMAIN
+ assert credentials._always_use_jwt_access
+
+ def test_from_service_account_info_args(self):
+ info = SERVICE_ACCOUNT_INFO.copy()
+ scopes = ["email", "profile"]
+ subject = "subject"
+ additional_claims = {"meta": "data"}
+
+ credentials = service_account.Credentials.from_service_account_info(
+ info, scopes=scopes, subject=subject, additional_claims=additional_claims
+ )
+
+ assert credentials.service_account_email == info["client_email"]
+ assert credentials.project_id == info["project_id"]
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._token_uri == info["token_uri"]
+ assert credentials._scopes == scopes
+ assert credentials._subject == subject
+ assert credentials._additional_claims == additional_claims
+ assert not credentials._always_use_jwt_access
+
+ def test_from_service_account_file(self):
+ info = SERVICE_ACCOUNT_INFO.copy()
+
+ credentials = service_account.Credentials.from_service_account_file(
+ SERVICE_ACCOUNT_JSON_FILE
+ )
+
+ assert credentials.service_account_email == info["client_email"]
+ assert credentials.project_id == info["project_id"]
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._token_uri == info["token_uri"]
+
+ def test_from_service_account_file_non_gdu(self):
+ info = SERVICE_ACCOUNT_INFO_NON_GDU.copy()
+
+ credentials = service_account.Credentials.from_service_account_file(
+ SERVICE_ACCOUNT_NON_GDU_JSON_FILE
+ )
+
+ assert credentials.service_account_email == info["client_email"]
+ assert credentials.project_id == info["project_id"]
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._token_uri == info["token_uri"]
+ assert credentials._universe_domain == FAKE_UNIVERSE_DOMAIN
+ assert credentials._always_use_jwt_access
+
+ def test_from_service_account_file_args(self):
+ info = SERVICE_ACCOUNT_INFO.copy()
+ scopes = ["email", "profile"]
+ subject = "subject"
+ additional_claims = {"meta": "data"}
+
+ credentials = service_account.Credentials.from_service_account_file(
+ SERVICE_ACCOUNT_JSON_FILE,
+ subject=subject,
+ scopes=scopes,
+ additional_claims=additional_claims,
+ )
+
+ assert credentials.service_account_email == info["client_email"]
+ assert credentials.project_id == info["project_id"]
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._token_uri == info["token_uri"]
+ assert credentials._scopes == scopes
+ assert credentials._subject == subject
+ assert credentials._additional_claims == additional_claims
+
+ def test_default_state(self):
+ credentials = self.make_credentials()
+ assert not credentials.valid
+ # Expiration hasn't been set yet
+ assert not credentials.expired
+ # Scopes haven't been specified yet
+ assert credentials.requires_scopes
+
+ def test_sign_bytes(self):
+ credentials = self.make_credentials()
+ to_sign = b"123"
+ signature = credentials.sign_bytes(to_sign)
+ assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
+
+ def test_signer(self):
+ credentials = self.make_credentials()
+ assert isinstance(credentials.signer, crypt.Signer)
+
+ def test_signer_email(self):
+ credentials = self.make_credentials()
+ assert credentials.signer_email == self.SERVICE_ACCOUNT_EMAIL
+
+ def test_create_scoped(self):
+ credentials = self.make_credentials()
+ scopes = ["email", "profile"]
+ credentials = credentials.with_scopes(scopes)
+ assert credentials._scopes == scopes
+
+ def test_with_claims(self):
+ credentials = self.make_credentials()
+ new_credentials = credentials.with_claims({"meep": "moop"})
+ assert new_credentials._additional_claims == {"meep": "moop"}
+
+ def test_with_quota_project(self):
+ credentials = self.make_credentials()
+ new_credentials = credentials.with_quota_project("new-project-456")
+ assert new_credentials.quota_project_id == "new-project-456"
+ hdrs = {}
+ new_credentials.apply(hdrs, token="tok")
+ assert "x-goog-user-project" in hdrs
+
+ def test_with_token_uri(self):
+ credentials = self.make_credentials()
+ new_token_uri = "https://example2.com/oauth2/token"
+ assert credentials._token_uri == self.TOKEN_URI
+ creds_with_new_token_uri = credentials.with_token_uri(new_token_uri)
+ assert creds_with_new_token_uri._token_uri == new_token_uri
+
+ def test__with_always_use_jwt_access(self):
+ credentials = self.make_credentials()
+ assert not credentials._always_use_jwt_access
+
+ new_credentials = credentials.with_always_use_jwt_access(True)
+ assert new_credentials._always_use_jwt_access
+
+ def test__with_always_use_jwt_access_non_default_universe_domain(self):
+ credentials = self.make_credentials(universe_domain=FAKE_UNIVERSE_DOMAIN)
+ with pytest.raises(exceptions.InvalidValue) as excinfo:
+ credentials.with_always_use_jwt_access(False)
+
+ assert excinfo.match(
+ "always_use_jwt_access should be True for non-default universe domain"
+ )
+
+ def test__make_authorization_grant_assertion(self):
+ credentials = self.make_credentials()
+ token = credentials._make_authorization_grant_assertion()
+ payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+ assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+ assert payload["aud"] == service_account._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+
+ def test__make_authorization_grant_assertion_scoped(self):
+ credentials = self.make_credentials()
+ scopes = ["email", "profile"]
+ credentials = credentials.with_scopes(scopes)
+ token = credentials._make_authorization_grant_assertion()
+ payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+ assert payload["scope"] == "email profile"
+
+ def test__make_authorization_grant_assertion_subject(self):
+ credentials = self.make_credentials()
+ subject = "user@example.com"
+ credentials = credentials.with_subject(subject)
+ token = credentials._make_authorization_grant_assertion()
+ payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+ assert payload["sub"] == subject
+
+ def test_apply_with_quota_project_id(self):
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ quota_project_id="quota-project-123",
+ )
+
+ headers = {}
+ credentials.apply(headers, token="token")
+
+ assert headers["x-goog-user-project"] == "quota-project-123"
+ assert "token" in headers["authorization"]
+
+ def test_apply_with_no_quota_project_id(self):
+ credentials = service_account.Credentials(
+ SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI
+ )
+
+ headers = {}
+ credentials.apply(headers, token="token")
+
+ assert "x-goog-user-project" not in headers
+ assert "token" in headers["authorization"]
+
+ @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+ def test__create_self_signed_jwt(self, jwt):
+ credentials = service_account.Credentials(
+ SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI
+ )
+
+ audience = "https://pubsub.googleapis.com"
+ credentials._create_self_signed_jwt(audience)
+ jwt.from_signing_credentials.assert_called_once_with(credentials, audience)
+
+ @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+ def test__create_self_signed_jwt_with_user_scopes(self, jwt):
+ credentials = service_account.Credentials(
+ SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, scopes=["foo"]
+ )
+
+ audience = "https://pubsub.googleapis.com"
+ credentials._create_self_signed_jwt(audience)
+
+ # JWT should not be created if there are user-defined scopes
+ jwt.from_signing_credentials.assert_not_called()
+
+ @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+ def test__create_self_signed_jwt_always_use_jwt_access_with_audience(self, jwt):
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ default_scopes=["bar", "foo"],
+ always_use_jwt_access=True,
+ )
+
+ audience = "https://pubsub.googleapis.com"
+ credentials._create_self_signed_jwt(audience)
+ jwt.from_signing_credentials.assert_called_once_with(credentials, audience)
+
+ @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+ def test__create_self_signed_jwt_always_use_jwt_access_with_audience_similar_jwt_is_reused(
+ self, jwt
+ ):
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ default_scopes=["bar", "foo"],
+ always_use_jwt_access=True,
+ )
+
+ audience = "https://pubsub.googleapis.com"
+ credentials._create_self_signed_jwt(audience)
+ credentials._jwt_credentials._audience = audience
+ credentials._create_self_signed_jwt(audience)
+ jwt.from_signing_credentials.assert_called_once_with(credentials, audience)
+
+ @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+ def test__create_self_signed_jwt_always_use_jwt_access_with_scopes(self, jwt):
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ scopes=["bar", "foo"],
+ always_use_jwt_access=True,
+ )
+
+ audience = "https://pubsub.googleapis.com"
+ credentials._create_self_signed_jwt(audience)
+ jwt.from_signing_credentials.assert_called_once_with(
+ credentials, None, additional_claims={"scope": "bar foo"}
+ )
+
+ @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+ def test__create_self_signed_jwt_always_use_jwt_access_with_scopes_similar_jwt_is_reused(
+ self, jwt
+ ):
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ scopes=["bar", "foo"],
+ always_use_jwt_access=True,
+ )
+
+ audience = "https://pubsub.googleapis.com"
+ credentials._create_self_signed_jwt(audience)
+ credentials._jwt_credentials.additional_claims = {"scope": "bar foo"}
+ credentials._create_self_signed_jwt(audience)
+ jwt.from_signing_credentials.assert_called_once_with(
+ credentials, None, additional_claims={"scope": "bar foo"}
+ )
+
+ @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+ def test__create_self_signed_jwt_always_use_jwt_access_with_default_scopes(
+ self, jwt
+ ):
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ default_scopes=["bar", "foo"],
+ always_use_jwt_access=True,
+ )
+
+ credentials._create_self_signed_jwt(None)
+ jwt.from_signing_credentials.assert_called_once_with(
+ credentials, None, additional_claims={"scope": "bar foo"}
+ )
+
+ @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+ def test__create_self_signed_jwt_always_use_jwt_access_with_default_scopes_similar_jwt_is_reused(
+ self, jwt
+ ):
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ default_scopes=["bar", "foo"],
+ always_use_jwt_access=True,
+ )
+
+ credentials._create_self_signed_jwt(None)
+ credentials._jwt_credentials.additional_claims = {"scope": "bar foo"}
+ credentials._create_self_signed_jwt(None)
+ jwt.from_signing_credentials.assert_called_once_with(
+ credentials, None, additional_claims={"scope": "bar foo"}
+ )
+
+ @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+ def test__create_self_signed_jwt_always_use_jwt_access(self, jwt):
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ always_use_jwt_access=True,
+ )
+
+ credentials._create_self_signed_jwt(None)
+ jwt.from_signing_credentials.assert_not_called()
+
+ def test_token_usage_metrics_assertion(self):
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ always_use_jwt_access=False,
+ )
+ credentials.token = "token"
+ credentials.expiry = None
+
+ headers = {}
+ credentials.before_request(mock.Mock(), None, None, headers)
+ assert headers["authorization"] == "Bearer token"
+ assert headers["x-goog-api-client"] == "cred-type/sa"
+
+ def test_token_usage_metrics_self_signed_jwt(self):
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ always_use_jwt_access=True,
+ )
+ credentials._create_self_signed_jwt("foo.googleapis.com")
+ credentials.token = "token"
+ credentials.expiry = None
+
+ headers = {}
+ credentials.before_request(mock.Mock(), None, None, headers)
+ assert headers["authorization"] == "Bearer token"
+ assert headers["x-goog-api-client"] == "cred-type/jwt"
+
+ @mock.patch("google.oauth2._client.jwt_grant", autospec=True)
+ def test_refresh_success(self, jwt_grant):
+ credentials = self.make_credentials()
+ token = "token"
+ jwt_grant.return_value = (
+ token,
+ _helpers.utcnow() + datetime.timedelta(seconds=500),
+ {},
+ )
+ request = mock.create_autospec(transport.Request, instance=True)
+
+ # Refresh credentials
+ credentials.refresh(request)
+
+ # Check jwt grant call.
+ assert jwt_grant.called
+
+ called_request, token_uri, assertion = jwt_grant.call_args[0]
+ assert called_request == request
+ assert token_uri == credentials._token_uri
+ assert jwt.decode(assertion, PUBLIC_CERT_BYTES)
+ # No further assertion done on the token, as there are separate tests
+ # for checking the authorization grant assertion.
+
+ # Check that the credentials have the token.
+ assert credentials.token == token
+
+ # Check that the credentials are valid (have a token and are not
+ # expired)
+ assert credentials.valid
+
+ @mock.patch("google.oauth2._client.jwt_grant", autospec=True)
+ def test_before_request_refreshes(self, jwt_grant):
+ credentials = self.make_credentials()
+ token = "token"
+ jwt_grant.return_value = (
+ token,
+ _helpers.utcnow() + datetime.timedelta(seconds=500),
+ None,
+ )
+ request = mock.create_autospec(transport.Request, instance=True)
+
+ # Credentials should start as invalid
+ assert not credentials.valid
+
+ # before_request should cause a refresh
+ credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+ # The refresh endpoint should've been called.
+ assert jwt_grant.called
+
+ # Credentials should now be valid.
+ assert credentials.valid
+
+ @mock.patch("google.auth.jwt.Credentials._make_jwt")
+ def test_refresh_with_jwt_credentials(self, make_jwt):
+ credentials = self.make_credentials()
+ credentials._create_self_signed_jwt("https://pubsub.googleapis.com")
+
+ request = mock.create_autospec(transport.Request, instance=True)
+
+ token = "token"
+ expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+ make_jwt.return_value = (b"token", expiry)
+
+ # Credentials should start as invalid
+ assert not credentials.valid
+
+ # before_request should cause a refresh
+ credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+ # Credentials should now be valid.
+ assert credentials.valid
+
+ # Assert make_jwt was called
+ assert make_jwt.call_count == 1
+
+ assert credentials.token == token
+ assert credentials.expiry == expiry
+
+ def test_refresh_with_jwt_credentials_token_type_check(self):
+ credentials = self.make_credentials()
+ credentials._create_self_signed_jwt("https://pubsub.googleapis.com")
+ credentials.refresh(mock.Mock())
+
+ # Credentials token should be a JWT string.
+ assert isinstance(credentials.token, str)
+ payload = jwt.decode(credentials.token, verify=False)
+ assert payload["aud"] == "https://pubsub.googleapis.com"
+
+ @mock.patch("google.oauth2._client.jwt_grant", autospec=True)
+ @mock.patch("google.auth.jwt.Credentials.refresh", autospec=True)
+ def test_refresh_jwt_not_used_for_domain_wide_delegation(
+ self, self_signed_jwt_refresh, jwt_grant
+ ):
+ # Create a domain wide delegation credentials by setting the subject.
+ credentials = service_account.Credentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ always_use_jwt_access=True,
+ subject="subject",
+ )
+ credentials._create_self_signed_jwt("https://pubsub.googleapis.com")
+ jwt_grant.return_value = (
+ "token",
+ _helpers.utcnow() + datetime.timedelta(seconds=500),
+ {},
+ )
+ request = mock.create_autospec(transport.Request, instance=True)
+
+ # Refresh credentials
+ credentials.refresh(request)
+
+ # Make sure we are using jwt_grant and not self signed JWT refresh
+ # method to obtain the token.
+ assert jwt_grant.called
+ assert not self_signed_jwt_refresh.called
+
+ def test_refresh_non_gdu_missing_jwt_credentials(self):
+ credentials = self.make_credentials(universe_domain="foo")
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.refresh(None)
+ assert excinfo.match("self._jwt_credentials is missing")
+
+ def test_refresh_non_gdu_domain_wide_delegation_not_supported(self):
+ credentials = self.make_credentials(universe_domain="foo")
+ credentials._subject = "bar@example.com"
+ credentials._create_self_signed_jwt("https://pubsub.googleapis.com")
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.refresh(None)
+ assert excinfo.match("domain wide delegation is not supported")
+
+
+class TestIDTokenCredentials(object):
+ SERVICE_ACCOUNT_EMAIL = "service-account@example.com"
+ TOKEN_URI = "https://example.com/oauth2/token"
+ TARGET_AUDIENCE = "https://example.com"
+
+ @classmethod
+ def make_credentials(cls, universe_domain=service_account._DEFAULT_UNIVERSE_DOMAIN):
+ return service_account.IDTokenCredentials(
+ SIGNER,
+ cls.SERVICE_ACCOUNT_EMAIL,
+ cls.TOKEN_URI,
+ cls.TARGET_AUDIENCE,
+ universe_domain=universe_domain,
+ )
+
+ def test_constructor_no_universe_domain(self):
+ credentials = service_account.IDTokenCredentials(
+ SIGNER,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.TOKEN_URI,
+ self.TARGET_AUDIENCE,
+ universe_domain=None,
+ )
+ assert credentials._universe_domain == service_account._DEFAULT_UNIVERSE_DOMAIN
+
+ def test_from_service_account_info(self):
+ credentials = service_account.IDTokenCredentials.from_service_account_info(
+ SERVICE_ACCOUNT_INFO, target_audience=self.TARGET_AUDIENCE
+ )
+
+ assert credentials._signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"]
+ assert credentials.service_account_email == SERVICE_ACCOUNT_INFO["client_email"]
+ assert credentials._token_uri == SERVICE_ACCOUNT_INFO["token_uri"]
+ assert credentials._target_audience == self.TARGET_AUDIENCE
+ assert not credentials._use_iam_endpoint
+
+ def test_from_service_account_info_non_gdu(self):
+ credentials = service_account.IDTokenCredentials.from_service_account_info(
+ SERVICE_ACCOUNT_INFO_NON_GDU, target_audience=self.TARGET_AUDIENCE
+ )
+
+ assert (
+ credentials._signer.key_id == SERVICE_ACCOUNT_INFO_NON_GDU["private_key_id"]
+ )
+ assert (
+ credentials.service_account_email
+ == SERVICE_ACCOUNT_INFO_NON_GDU["client_email"]
+ )
+ assert credentials._token_uri == SERVICE_ACCOUNT_INFO_NON_GDU["token_uri"]
+ assert credentials._target_audience == self.TARGET_AUDIENCE
+ assert credentials._use_iam_endpoint
+
+ def test_from_service_account_file(self):
+ info = SERVICE_ACCOUNT_INFO.copy()
+
+ credentials = service_account.IDTokenCredentials.from_service_account_file(
+ SERVICE_ACCOUNT_JSON_FILE, target_audience=self.TARGET_AUDIENCE
+ )
+
+ assert credentials.service_account_email == info["client_email"]
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._token_uri == info["token_uri"]
+ assert credentials._target_audience == self.TARGET_AUDIENCE
+ assert not credentials._use_iam_endpoint
+
+ def test_from_service_account_file_non_gdu(self):
+ info = SERVICE_ACCOUNT_INFO_NON_GDU.copy()
+
+ credentials = service_account.IDTokenCredentials.from_service_account_file(
+ SERVICE_ACCOUNT_NON_GDU_JSON_FILE, target_audience=self.TARGET_AUDIENCE
+ )
+
+ assert credentials.service_account_email == info["client_email"]
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._token_uri == info["token_uri"]
+ assert credentials._target_audience == self.TARGET_AUDIENCE
+ assert credentials._use_iam_endpoint
+
+ def test_default_state(self):
+ credentials = self.make_credentials()
+ assert not credentials.valid
+ # Expiration hasn't been set yet
+ assert not credentials.expired
+
+ def test_sign_bytes(self):
+ credentials = self.make_credentials()
+ to_sign = b"123"
+ signature = credentials.sign_bytes(to_sign)
+ assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
+
+ def test_signer(self):
+ credentials = self.make_credentials()
+ assert isinstance(credentials.signer, crypt.Signer)
+
+ def test_signer_email(self):
+ credentials = self.make_credentials()
+ assert credentials.signer_email == self.SERVICE_ACCOUNT_EMAIL
+
+ def test_with_target_audience(self):
+ credentials = self.make_credentials()
+ new_credentials = credentials.with_target_audience("https://new.example.com")
+ assert new_credentials._target_audience == "https://new.example.com"
+
+ def test__with_use_iam_endpoint(self):
+ credentials = self.make_credentials()
+ new_credentials = credentials._with_use_iam_endpoint(True)
+ assert new_credentials._use_iam_endpoint
+
+ def test__with_use_iam_endpoint_non_default_universe_domain(self):
+ credentials = self.make_credentials(universe_domain=FAKE_UNIVERSE_DOMAIN)
+ with pytest.raises(exceptions.InvalidValue) as excinfo:
+ credentials._with_use_iam_endpoint(False)
+
+ assert excinfo.match(
+ "use_iam_endpoint should be True for non-default universe domain"
+ )
+
+ def test_with_quota_project(self):
+ credentials = self.make_credentials()
+ new_credentials = credentials.with_quota_project("project-foo")
+ assert new_credentials._quota_project_id == "project-foo"
+
+ def test_with_token_uri(self):
+ credentials = self.make_credentials()
+ new_token_uri = "https://example2.com/oauth2/token"
+ assert credentials._token_uri == self.TOKEN_URI
+ creds_with_new_token_uri = credentials.with_token_uri(new_token_uri)
+ assert creds_with_new_token_uri._token_uri == new_token_uri
+
+ def test__make_authorization_grant_assertion(self):
+ credentials = self.make_credentials()
+ token = credentials._make_authorization_grant_assertion()
+ payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+ assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+ assert payload["aud"] == service_account._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+ assert payload["target_audience"] == self.TARGET_AUDIENCE
+
+ @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
+ def test_refresh_success(self, id_token_jwt_grant):
+ credentials = self.make_credentials()
+ token = "token"
+ id_token_jwt_grant.return_value = (
+ token,
+ _helpers.utcnow() + datetime.timedelta(seconds=500),
+ {},
+ )
+ request = mock.create_autospec(transport.Request, instance=True)
+
+ # Refresh credentials
+ credentials.refresh(request)
+
+ # Check jwt grant call.
+ assert id_token_jwt_grant.called
+
+ called_request, token_uri, assertion = id_token_jwt_grant.call_args[0]
+ assert called_request == request
+ assert token_uri == credentials._token_uri
+ assert jwt.decode(assertion, PUBLIC_CERT_BYTES)
+ # No further assertion done on the token, as there are separate tests
+ # for checking the authorization grant assertion.
+
+ # Check that the credentials have the token.
+ assert credentials.token == token
+
+ # Check that the credentials are valid (have a token and are not
+ # expired)
+ assert credentials.valid
+
+ @mock.patch(
+ "google.oauth2._client.call_iam_generate_id_token_endpoint", autospec=True
+ )
+ def test_refresh_iam_flow(self, call_iam_generate_id_token_endpoint):
+ credentials = self.make_credentials()
+ credentials._use_iam_endpoint = True
+ token = "id_token"
+ call_iam_generate_id_token_endpoint.return_value = (
+ token,
+ _helpers.utcnow() + datetime.timedelta(seconds=500),
+ )
+ request = mock.Mock()
+ credentials.refresh(request)
+ req, signer_email, target_audience, access_token = call_iam_generate_id_token_endpoint.call_args[
+ 0
+ ]
+ assert req == request
+ assert signer_email == "service-account@example.com"
+ assert target_audience == "https://example.com"
+ decoded_access_token = jwt.decode(access_token, verify=False)
+ assert decoded_access_token["scope"] == "https://www.googleapis.com/auth/iam"
+
+ @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
+ def test_before_request_refreshes(self, id_token_jwt_grant):
+ credentials = self.make_credentials()
+ token = "token"
+ id_token_jwt_grant.return_value = (
+ token,
+ _helpers.utcnow() + datetime.timedelta(seconds=500),
+ None,
+ )
+ request = mock.create_autospec(transport.Request, instance=True)
+
+ # Credentials should start as invalid
+ assert not credentials.valid
+
+ # before_request should cause a refresh
+ credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+ # The refresh endpoint should've been called.
+ assert id_token_jwt_grant.called
+
+ # Credentials should now be valid.
+ assert credentials.valid
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_sts.py b/contrib/python/google-auth/py3/tests/oauth2/test_sts.py
new file mode 100644
index 0000000000..e0fb4ae23e
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_sts.py
@@ -0,0 +1,480 @@
+# Copyright 2020 Google LLC
+#
+# 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 http.client as http_client
+import json
+import urllib
+
+import mock
+import pytest # type: ignore
+
+from google.auth import exceptions
+from google.auth import transport
+from google.oauth2 import sts
+from google.oauth2 import utils
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password"
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+
+
+class TestStsClient(object):
+ GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
+ RESOURCE = "https://api.example.com/"
+ AUDIENCE = "urn:example:cooperation-context"
+ SCOPES = ["scope1", "scope2"]
+ REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+ SUBJECT_TOKEN = "HEADER.SUBJECT_TOKEN_PAYLOAD.SIGNATURE"
+ SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
+ ACTOR_TOKEN = "HEADER.ACTOR_TOKEN_PAYLOAD.SIGNATURE"
+ ACTOR_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
+ TOKEN_EXCHANGE_ENDPOINT = "https://example.com/token.oauth2"
+ ADDON_HEADERS = {"x-client-version": "0.1.2"}
+ ADDON_OPTIONS = {"additional": {"non-standard": ["options"], "other": "some-value"}}
+ SUCCESS_RESPONSE = {
+ "access_token": "ACCESS_TOKEN",
+ "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "scope1 scope2",
+ }
+ SUCCESS_RESPONSE_WITH_REFRESH = {
+ "access_token": "abc",
+ "refresh_token": "xyz",
+ "expires_in": 3600,
+ }
+ ERROR_RESPONSE = {
+ "error": "invalid_request",
+ "error_description": "Invalid subject token",
+ "error_uri": "https://tools.ietf.org/html/rfc6749",
+ }
+ CLIENT_AUTH_BASIC = utils.ClientAuthentication(
+ utils.ClientAuthType.basic, CLIENT_ID, CLIENT_SECRET
+ )
+ CLIENT_AUTH_REQUEST_BODY = utils.ClientAuthentication(
+ utils.ClientAuthType.request_body, CLIENT_ID, CLIENT_SECRET
+ )
+
+ @classmethod
+ def make_client(cls, client_auth=None):
+ return sts.Client(cls.TOKEN_EXCHANGE_ENDPOINT, client_auth)
+
+ @classmethod
+ def make_mock_request(cls, data, status=http_client.OK):
+ response = mock.create_autospec(transport.Response, instance=True)
+ response.status = status
+ response.data = json.dumps(data).encode("utf-8")
+
+ request = mock.create_autospec(transport.Request)
+ request.return_value = response
+
+ return request
+
+ @classmethod
+ def assert_request_kwargs(cls, request_kwargs, headers, request_data):
+ """Asserts the request was called with the expected parameters.
+ """
+ assert request_kwargs["url"] == cls.TOKEN_EXCHANGE_ENDPOINT
+ assert request_kwargs["method"] == "POST"
+ assert request_kwargs["headers"] == headers
+ assert request_kwargs["body"] is not None
+ body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
+ for (k, v) in body_tuples:
+ assert v.decode("utf-8") == request_data[k.decode("utf-8")]
+ assert len(body_tuples) == len(request_data.keys())
+
+ def test_exchange_token_full_success_without_auth(self):
+ """Test token exchange success without client authentication using full
+ parameters.
+ """
+ client = self.make_client()
+ headers = self.ADDON_HEADERS.copy()
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
+ request_data = {
+ "grant_type": self.GRANT_TYPE,
+ "resource": self.RESOURCE,
+ "audience": self.AUDIENCE,
+ "scope": " ".join(self.SCOPES),
+ "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+ "subject_token": self.SUBJECT_TOKEN,
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "actor_token": self.ACTOR_TOKEN,
+ "actor_token_type": self.ACTOR_TOKEN_TYPE,
+ "options": urllib.parse.quote(json.dumps(self.ADDON_OPTIONS)),
+ }
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+
+ response = client.exchange_token(
+ request,
+ self.GRANT_TYPE,
+ self.SUBJECT_TOKEN,
+ self.SUBJECT_TOKEN_TYPE,
+ self.RESOURCE,
+ self.AUDIENCE,
+ self.SCOPES,
+ self.REQUESTED_TOKEN_TYPE,
+ self.ACTOR_TOKEN,
+ self.ACTOR_TOKEN_TYPE,
+ self.ADDON_OPTIONS,
+ self.ADDON_HEADERS,
+ )
+
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert response == self.SUCCESS_RESPONSE
+
+ def test_exchange_token_partial_success_without_auth(self):
+ """Test token exchange success without client authentication using
+ partial (required only) parameters.
+ """
+ client = self.make_client()
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
+ request_data = {
+ "grant_type": self.GRANT_TYPE,
+ "audience": self.AUDIENCE,
+ "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+ "subject_token": self.SUBJECT_TOKEN,
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ }
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+
+ response = client.exchange_token(
+ request,
+ grant_type=self.GRANT_TYPE,
+ subject_token=self.SUBJECT_TOKEN,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ audience=self.AUDIENCE,
+ requested_token_type=self.REQUESTED_TOKEN_TYPE,
+ )
+
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert response == self.SUCCESS_RESPONSE
+
+ def test_exchange_token_non200_without_auth(self):
+ """Test token exchange without client auth responding with non-200 status.
+ """
+ client = self.make_client()
+ request = self.make_mock_request(
+ status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE
+ )
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ client.exchange_token(
+ request,
+ self.GRANT_TYPE,
+ self.SUBJECT_TOKEN,
+ self.SUBJECT_TOKEN_TYPE,
+ self.RESOURCE,
+ self.AUDIENCE,
+ self.SCOPES,
+ self.REQUESTED_TOKEN_TYPE,
+ self.ACTOR_TOKEN,
+ self.ACTOR_TOKEN_TYPE,
+ self.ADDON_OPTIONS,
+ self.ADDON_HEADERS,
+ )
+
+ assert excinfo.match(
+ r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+ )
+
+ def test_exchange_token_full_success_with_basic_auth(self):
+ """Test token exchange success with basic client authentication using full
+ parameters.
+ """
+ client = self.make_client(self.CLIENT_AUTH_BASIC)
+ headers = self.ADDON_HEADERS.copy()
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
+ headers["Authorization"] = "Basic {}".format(BASIC_AUTH_ENCODING)
+ request_data = {
+ "grant_type": self.GRANT_TYPE,
+ "resource": self.RESOURCE,
+ "audience": self.AUDIENCE,
+ "scope": " ".join(self.SCOPES),
+ "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+ "subject_token": self.SUBJECT_TOKEN,
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "actor_token": self.ACTOR_TOKEN,
+ "actor_token_type": self.ACTOR_TOKEN_TYPE,
+ "options": urllib.parse.quote(json.dumps(self.ADDON_OPTIONS)),
+ }
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+
+ response = client.exchange_token(
+ request,
+ self.GRANT_TYPE,
+ self.SUBJECT_TOKEN,
+ self.SUBJECT_TOKEN_TYPE,
+ self.RESOURCE,
+ self.AUDIENCE,
+ self.SCOPES,
+ self.REQUESTED_TOKEN_TYPE,
+ self.ACTOR_TOKEN,
+ self.ACTOR_TOKEN_TYPE,
+ self.ADDON_OPTIONS,
+ self.ADDON_HEADERS,
+ )
+
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert response == self.SUCCESS_RESPONSE
+
+ def test_exchange_token_partial_success_with_basic_auth(self):
+ """Test token exchange success with basic client authentication using
+ partial (required only) parameters.
+ """
+ client = self.make_client(self.CLIENT_AUTH_BASIC)
+ headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+ }
+ request_data = {
+ "grant_type": self.GRANT_TYPE,
+ "audience": self.AUDIENCE,
+ "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+ "subject_token": self.SUBJECT_TOKEN,
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ }
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+
+ response = client.exchange_token(
+ request,
+ grant_type=self.GRANT_TYPE,
+ subject_token=self.SUBJECT_TOKEN,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ audience=self.AUDIENCE,
+ requested_token_type=self.REQUESTED_TOKEN_TYPE,
+ )
+
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert response == self.SUCCESS_RESPONSE
+
+ def test_exchange_token_non200_with_basic_auth(self):
+ """Test token exchange with basic client auth responding with non-200
+ status.
+ """
+ client = self.make_client(self.CLIENT_AUTH_BASIC)
+ request = self.make_mock_request(
+ status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE
+ )
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ client.exchange_token(
+ request,
+ self.GRANT_TYPE,
+ self.SUBJECT_TOKEN,
+ self.SUBJECT_TOKEN_TYPE,
+ self.RESOURCE,
+ self.AUDIENCE,
+ self.SCOPES,
+ self.REQUESTED_TOKEN_TYPE,
+ self.ACTOR_TOKEN,
+ self.ACTOR_TOKEN_TYPE,
+ self.ADDON_OPTIONS,
+ self.ADDON_HEADERS,
+ )
+
+ assert excinfo.match(
+ r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+ )
+
+ def test_exchange_token_full_success_with_reqbody_auth(self):
+ """Test token exchange success with request body client authenticaiton
+ using full parameters.
+ """
+ client = self.make_client(self.CLIENT_AUTH_REQUEST_BODY)
+ headers = self.ADDON_HEADERS.copy()
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
+ request_data = {
+ "grant_type": self.GRANT_TYPE,
+ "resource": self.RESOURCE,
+ "audience": self.AUDIENCE,
+ "scope": " ".join(self.SCOPES),
+ "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+ "subject_token": self.SUBJECT_TOKEN,
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "actor_token": self.ACTOR_TOKEN,
+ "actor_token_type": self.ACTOR_TOKEN_TYPE,
+ "options": urllib.parse.quote(json.dumps(self.ADDON_OPTIONS)),
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ }
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+
+ response = client.exchange_token(
+ request,
+ self.GRANT_TYPE,
+ self.SUBJECT_TOKEN,
+ self.SUBJECT_TOKEN_TYPE,
+ self.RESOURCE,
+ self.AUDIENCE,
+ self.SCOPES,
+ self.REQUESTED_TOKEN_TYPE,
+ self.ACTOR_TOKEN,
+ self.ACTOR_TOKEN_TYPE,
+ self.ADDON_OPTIONS,
+ self.ADDON_HEADERS,
+ )
+
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert response == self.SUCCESS_RESPONSE
+
+ def test_exchange_token_partial_success_with_reqbody_auth(self):
+ """Test token exchange success with request body client authentication
+ using partial (required only) parameters.
+ """
+ client = self.make_client(self.CLIENT_AUTH_REQUEST_BODY)
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
+ request_data = {
+ "grant_type": self.GRANT_TYPE,
+ "audience": self.AUDIENCE,
+ "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+ "subject_token": self.SUBJECT_TOKEN,
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ }
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+
+ response = client.exchange_token(
+ request,
+ grant_type=self.GRANT_TYPE,
+ subject_token=self.SUBJECT_TOKEN,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ audience=self.AUDIENCE,
+ requested_token_type=self.REQUESTED_TOKEN_TYPE,
+ )
+
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert response == self.SUCCESS_RESPONSE
+
+ def test_exchange_token_non200_with_reqbody_auth(self):
+ """Test token exchange with POST request body client auth responding
+ with non-200 status.
+ """
+ client = self.make_client(self.CLIENT_AUTH_REQUEST_BODY)
+ request = self.make_mock_request(
+ status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE
+ )
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ client.exchange_token(
+ request,
+ self.GRANT_TYPE,
+ self.SUBJECT_TOKEN,
+ self.SUBJECT_TOKEN_TYPE,
+ self.RESOURCE,
+ self.AUDIENCE,
+ self.SCOPES,
+ self.REQUESTED_TOKEN_TYPE,
+ self.ACTOR_TOKEN,
+ self.ACTOR_TOKEN_TYPE,
+ self.ADDON_OPTIONS,
+ self.ADDON_HEADERS,
+ )
+
+ assert excinfo.match(
+ r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+ )
+
+ def test_refresh_token_success(self):
+ """Test refresh token with successful response."""
+ client = self.make_client(self.CLIENT_AUTH_BASIC)
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+
+ response = client.refresh_token(request, "refreshtoken")
+
+ headers = {
+ "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
+ "Content-Type": "application/x-www-form-urlencoded",
+ }
+ request_data = {"grant_type": "refresh_token", "refresh_token": "refreshtoken"}
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert response == self.SUCCESS_RESPONSE
+
+ def test_refresh_token_success_with_refresh(self):
+ """Test refresh token with successful response."""
+ client = self.make_client(self.CLIENT_AUTH_BASIC)
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE_WITH_REFRESH
+ )
+
+ response = client.refresh_token(request, "refreshtoken")
+
+ headers = {
+ "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
+ "Content-Type": "application/x-www-form-urlencoded",
+ }
+ request_data = {"grant_type": "refresh_token", "refresh_token": "refreshtoken"}
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert response == self.SUCCESS_RESPONSE_WITH_REFRESH
+
+ def test_refresh_token_failure(self):
+ """Test refresh token with failure response."""
+ client = self.make_client(self.CLIENT_AUTH_BASIC)
+ request = self.make_mock_request(
+ status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE
+ )
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ client.refresh_token(request, "refreshtoken")
+
+ assert excinfo.match(
+ r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+ )
+
+ def test__make_request_success(self):
+ """Test base method with successful response."""
+ client = self.make_client(self.CLIENT_AUTH_BASIC)
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+
+ response = client._make_request(request, {"a": "b"}, {"c": "d"})
+
+ headers = {
+ "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
+ "Content-Type": "application/x-www-form-urlencoded",
+ "a": "b",
+ }
+ request_data = {"c": "d"}
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert response == self.SUCCESS_RESPONSE
+
+ def test_make_request_failure(self):
+ """Test refresh token with failure response."""
+ client = self.make_client(self.CLIENT_AUTH_BASIC)
+ request = self.make_mock_request(
+ status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE
+ )
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ client._make_request(request, {"a": "b"}, {"c": "d"})
+
+ assert excinfo.match(
+ r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+ )
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_utils.py b/contrib/python/google-auth/py3/tests/oauth2/test_utils.py
new file mode 100644
index 0000000000..543a693a98
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_utils.py
@@ -0,0 +1,264 @@
+# Copyright 2020 Google LLC
+#
+# 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 json
+
+import pytest # type: ignore
+
+from google.auth import exceptions
+from google.oauth2 import utils
+
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password"
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+# Base64 encoding of "username:"
+BASIC_AUTH_ENCODING_SECRETLESS = "dXNlcm5hbWU6"
+
+
+class AuthHandler(utils.OAuthClientAuthHandler):
+ def __init__(self, client_auth=None):
+ super(AuthHandler, self).__init__(client_auth)
+
+ def apply_client_authentication_options(
+ self, headers, request_body=None, bearer_token=None
+ ):
+ return super(AuthHandler, self).apply_client_authentication_options(
+ headers, request_body, bearer_token
+ )
+
+
+class TestClientAuthentication(object):
+ @classmethod
+ def make_client_auth(cls, client_secret=None):
+ return utils.ClientAuthentication(
+ utils.ClientAuthType.basic, CLIENT_ID, client_secret
+ )
+
+ def test_initialization_with_client_secret(self):
+ client_auth = self.make_client_auth(CLIENT_SECRET)
+
+ assert client_auth.client_auth_type == utils.ClientAuthType.basic
+ assert client_auth.client_id == CLIENT_ID
+ assert client_auth.client_secret == CLIENT_SECRET
+
+ def test_initialization_no_client_secret(self):
+ client_auth = self.make_client_auth()
+
+ assert client_auth.client_auth_type == utils.ClientAuthType.basic
+ assert client_auth.client_id == CLIENT_ID
+ assert client_auth.client_secret is None
+
+
+class TestOAuthClientAuthHandler(object):
+ CLIENT_AUTH_BASIC = utils.ClientAuthentication(
+ utils.ClientAuthType.basic, CLIENT_ID, CLIENT_SECRET
+ )
+ CLIENT_AUTH_BASIC_SECRETLESS = utils.ClientAuthentication(
+ utils.ClientAuthType.basic, CLIENT_ID
+ )
+ CLIENT_AUTH_REQUEST_BODY = utils.ClientAuthentication(
+ utils.ClientAuthType.request_body, CLIENT_ID, CLIENT_SECRET
+ )
+ CLIENT_AUTH_REQUEST_BODY_SECRETLESS = utils.ClientAuthentication(
+ utils.ClientAuthType.request_body, CLIENT_ID
+ )
+
+ @classmethod
+ def make_oauth_client_auth_handler(cls, client_auth=None):
+ return AuthHandler(client_auth)
+
+ def test_apply_client_authentication_options_none(self):
+ headers = {"Content-Type": "application/json"}
+ request_body = {"foo": "bar"}
+ auth_handler = self.make_oauth_client_auth_handler()
+
+ auth_handler.apply_client_authentication_options(headers, request_body)
+
+ assert headers == {"Content-Type": "application/json"}
+ assert request_body == {"foo": "bar"}
+
+ def test_apply_client_authentication_options_basic(self):
+ headers = {"Content-Type": "application/json"}
+ request_body = {"foo": "bar"}
+ auth_handler = self.make_oauth_client_auth_handler(self.CLIENT_AUTH_BASIC)
+
+ auth_handler.apply_client_authentication_options(headers, request_body)
+
+ assert headers == {
+ "Content-Type": "application/json",
+ "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+ }
+ assert request_body == {"foo": "bar"}
+
+ def test_apply_client_authentication_options_basic_nosecret(self):
+ headers = {"Content-Type": "application/json"}
+ request_body = {"foo": "bar"}
+ auth_handler = self.make_oauth_client_auth_handler(
+ self.CLIENT_AUTH_BASIC_SECRETLESS
+ )
+
+ auth_handler.apply_client_authentication_options(headers, request_body)
+
+ assert headers == {
+ "Content-Type": "application/json",
+ "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING_SECRETLESS),
+ }
+ assert request_body == {"foo": "bar"}
+
+ def test_apply_client_authentication_options_request_body(self):
+ headers = {"Content-Type": "application/json"}
+ request_body = {"foo": "bar"}
+ auth_handler = self.make_oauth_client_auth_handler(
+ self.CLIENT_AUTH_REQUEST_BODY
+ )
+
+ auth_handler.apply_client_authentication_options(headers, request_body)
+
+ assert headers == {"Content-Type": "application/json"}
+ assert request_body == {
+ "foo": "bar",
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ }
+
+ def test_apply_client_authentication_options_request_body_nosecret(self):
+ headers = {"Content-Type": "application/json"}
+ request_body = {"foo": "bar"}
+ auth_handler = self.make_oauth_client_auth_handler(
+ self.CLIENT_AUTH_REQUEST_BODY_SECRETLESS
+ )
+
+ auth_handler.apply_client_authentication_options(headers, request_body)
+
+ assert headers == {"Content-Type": "application/json"}
+ assert request_body == {
+ "foo": "bar",
+ "client_id": CLIENT_ID,
+ "client_secret": "",
+ }
+
+ def test_apply_client_authentication_options_request_body_no_body(self):
+ headers = {"Content-Type": "application/json"}
+ auth_handler = self.make_oauth_client_auth_handler(
+ self.CLIENT_AUTH_REQUEST_BODY
+ )
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ auth_handler.apply_client_authentication_options(headers)
+
+ assert excinfo.match(r"HTTP request does not support request-body")
+
+ def test_apply_client_authentication_options_bearer_token(self):
+ bearer_token = "ACCESS_TOKEN"
+ headers = {"Content-Type": "application/json"}
+ request_body = {"foo": "bar"}
+ auth_handler = self.make_oauth_client_auth_handler()
+
+ auth_handler.apply_client_authentication_options(
+ headers, request_body, bearer_token
+ )
+
+ assert headers == {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer {}".format(bearer_token),
+ }
+ assert request_body == {"foo": "bar"}
+
+ def test_apply_client_authentication_options_bearer_and_basic(self):
+ bearer_token = "ACCESS_TOKEN"
+ headers = {"Content-Type": "application/json"}
+ request_body = {"foo": "bar"}
+ auth_handler = self.make_oauth_client_auth_handler(self.CLIENT_AUTH_BASIC)
+
+ auth_handler.apply_client_authentication_options(
+ headers, request_body, bearer_token
+ )
+
+ # Bearer token should have higher priority.
+ assert headers == {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer {}".format(bearer_token),
+ }
+ assert request_body == {"foo": "bar"}
+
+ def test_apply_client_authentication_options_bearer_and_request_body(self):
+ bearer_token = "ACCESS_TOKEN"
+ headers = {"Content-Type": "application/json"}
+ request_body = {"foo": "bar"}
+ auth_handler = self.make_oauth_client_auth_handler(
+ self.CLIENT_AUTH_REQUEST_BODY
+ )
+
+ auth_handler.apply_client_authentication_options(
+ headers, request_body, bearer_token
+ )
+
+ # Bearer token should have higher priority.
+ assert headers == {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer {}".format(bearer_token),
+ }
+ assert request_body == {"foo": "bar"}
+
+
+def test__handle_error_response_code_only():
+ error_resp = {"error": "unsupported_grant_type"}
+ response_data = json.dumps(error_resp)
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ utils.handle_error_response(response_data)
+
+ assert excinfo.match(r"Error code unsupported_grant_type")
+
+
+def test__handle_error_response_code_description():
+ error_resp = {
+ "error": "unsupported_grant_type",
+ "error_description": "The provided grant_type is unsupported",
+ }
+ response_data = json.dumps(error_resp)
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ utils.handle_error_response(response_data)
+
+ assert excinfo.match(
+ r"Error code unsupported_grant_type: The provided grant_type is unsupported"
+ )
+
+
+def test__handle_error_response_code_description_uri():
+ error_resp = {
+ "error": "unsupported_grant_type",
+ "error_description": "The provided grant_type is unsupported",
+ "error_uri": "https://tools.ietf.org/html/rfc6749",
+ }
+ response_data = json.dumps(error_resp)
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ utils.handle_error_response(response_data)
+
+ assert excinfo.match(
+ r"Error code unsupported_grant_type: The provided grant_type is unsupported - https://tools.ietf.org/html/rfc6749"
+ )
+
+
+def test__handle_error_response_non_json():
+ response_data = "Oops, something wrong happened"
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ utils.handle_error_response(response_data)
+
+ assert excinfo.match(r"Oops, something wrong happened")
diff --git a/contrib/python/google-auth/py3/tests/test__cloud_sdk.py b/contrib/python/google-auth/py3/tests/test__cloud_sdk.py
new file mode 100644
index 0000000000..18ac18fa35
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test__cloud_sdk.py
@@ -0,0 +1,182 @@
+# Copyright 2016 Google LLC
+#
+# 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 io
+import json
+import os
+import subprocess
+import sys
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _cloud_sdk
+from google.auth import environment_vars
+from google.auth import exceptions
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json")
+
+with io.open(AUTHORIZED_USER_FILE, "rb") as fh:
+ AUTHORIZED_USER_FILE_DATA = json.load(fh)
+
+SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+with io.open(SERVICE_ACCOUNT_FILE, "rb") as fh:
+ SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
+
+
+@pytest.mark.parametrize(
+ "data, expected_project_id",
+ [(b"example-project\n", "example-project"), (b"", None)],
+)
+def test_get_project_id(data, expected_project_id):
+ check_output_patch = mock.patch(
+ "subprocess.check_output", autospec=True, return_value=data
+ )
+
+ with check_output_patch as check_output:
+ project_id = _cloud_sdk.get_project_id()
+
+ assert project_id == expected_project_id
+ assert check_output.called
+
+
+@mock.patch(
+ "subprocess.check_output",
+ autospec=True,
+ side_effect=subprocess.CalledProcessError(-1, "testing"),
+)
+def test_get_project_id_call_error(check_output):
+ project_id = _cloud_sdk.get_project_id()
+ assert project_id is None
+ assert check_output.called
+
+
+@pytest.mark.xfail
+def test__run_subprocess_ignore_stderr():
+ command = [
+ sys.executable,
+ "-c",
+ "from __future__ import print_function;"
+ + "import sys;"
+ + "print('error', file=sys.stderr);"
+ + "print('output', file=sys.stdout)",
+ ]
+
+ # If we ignore stderr, then the output only has stdout
+ output = _cloud_sdk._run_subprocess_ignore_stderr(command)
+ assert output == b"output\n"
+
+ # If we pipe stderr to stdout, then the output is mixed with stdout and stderr.
+ output = subprocess.check_output(command, stderr=subprocess.STDOUT)
+ assert output == b"output\nerror\n" or output == b"error\noutput\n"
+
+
+@mock.patch("os.name", new="nt")
+def test_get_project_id_windows():
+ check_output_patch = mock.patch(
+ "subprocess.check_output", autospec=True, return_value=b"example-project\n"
+ )
+
+ with check_output_patch as check_output:
+ project_id = _cloud_sdk.get_project_id()
+
+ assert project_id == "example-project"
+ assert check_output.called
+ # Make sure the executable is `gcloud.cmd`.
+ args = check_output.call_args[0]
+ command = args[0]
+ executable = command[0]
+ assert executable == "gcloud.cmd"
+
+
+@mock.patch("google.auth._cloud_sdk.get_config_path", autospec=True)
+def test_get_application_default_credentials_path(get_config_dir):
+ config_path = "config_path"
+ get_config_dir.return_value = config_path
+ credentials_path = _cloud_sdk.get_application_default_credentials_path()
+ assert credentials_path == os.path.join(
+ config_path, _cloud_sdk._CREDENTIALS_FILENAME
+ )
+
+
+def test_get_config_path_env_var(monkeypatch):
+ config_path_sentinel = "config_path"
+ monkeypatch.setenv(environment_vars.CLOUD_SDK_CONFIG_DIR, config_path_sentinel)
+ config_path = _cloud_sdk.get_config_path()
+ assert config_path == config_path_sentinel
+
+
+@mock.patch("os.path.expanduser")
+def test_get_config_path_unix(expanduser):
+ expanduser.side_effect = lambda path: path
+
+ config_path = _cloud_sdk.get_config_path()
+
+ assert os.path.split(config_path) == ("~/.config", _cloud_sdk._CONFIG_DIRECTORY)
+
+
+@mock.patch("os.name", new="nt")
+def test_get_config_path_windows(monkeypatch):
+ appdata = "appdata"
+ monkeypatch.setenv(_cloud_sdk._WINDOWS_CONFIG_ROOT_ENV_VAR, appdata)
+
+ config_path = _cloud_sdk.get_config_path()
+
+ assert os.path.split(config_path) == (appdata, _cloud_sdk._CONFIG_DIRECTORY)
+
+
+@mock.patch("os.name", new="nt")
+def test_get_config_path_no_appdata(monkeypatch):
+ monkeypatch.delenv(_cloud_sdk._WINDOWS_CONFIG_ROOT_ENV_VAR, raising=False)
+ monkeypatch.setenv("SystemDrive", "G:")
+
+ config_path = _cloud_sdk.get_config_path()
+
+ assert os.path.split(config_path) == ("G:/\\", _cloud_sdk._CONFIG_DIRECTORY)
+
+
+@mock.patch("os.name", new="nt")
+@mock.patch("subprocess.check_output", autospec=True)
+def test_get_auth_access_token_windows(check_output):
+ check_output.return_value = b"access_token\n"
+
+ token = _cloud_sdk.get_auth_access_token()
+ assert token == "access_token"
+ check_output.assert_called_with(
+ ("gcloud.cmd", "auth", "print-access-token"), stderr=subprocess.STDOUT
+ )
+
+
+@mock.patch("subprocess.check_output", autospec=True)
+def test_get_auth_access_token_with_account(check_output):
+ check_output.return_value = b"access_token\n"
+
+ token = _cloud_sdk.get_auth_access_token(account="account")
+ assert token == "access_token"
+ check_output.assert_called_with(
+ ("gcloud", "auth", "print-access-token", "--account=account"),
+ stderr=subprocess.STDOUT,
+ )
+
+
+@mock.patch("subprocess.check_output", autospec=True)
+def test_get_auth_access_token_with_exception(check_output):
+ check_output.side_effect = OSError()
+
+ with pytest.raises(exceptions.UserAccessTokenError):
+ _cloud_sdk.get_auth_access_token(account="account")
diff --git a/contrib/python/google-auth/py3/tests/test__default.py b/contrib/python/google-auth/py3/tests/test__default.py
new file mode 100644
index 0000000000..29904ec7aa
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test__default.py
@@ -0,0 +1,1352 @@
+# Copyright 2016 Google LLC
+#
+# 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 json
+import os
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _default
+from google.auth import api_key
+from google.auth import app_engine
+from google.auth import aws
+from google.auth import compute_engine
+from google.auth import credentials
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import external_account
+from google.auth import external_account_authorized_user
+from google.auth import identity_pool
+from google.auth import impersonated_credentials
+from google.auth import pluggable
+from google.oauth2 import gdch_credentials
+from google.oauth2 import service_account
+import google.oauth2.credentials
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json")
+
+with open(AUTHORIZED_USER_FILE) as fh:
+ AUTHORIZED_USER_FILE_DATA = json.load(fh)
+
+AUTHORIZED_USER_CLOUD_SDK_FILE = os.path.join(
+ DATA_DIR, "authorized_user_cloud_sdk.json"
+)
+
+AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE = os.path.join(
+ DATA_DIR, "authorized_user_cloud_sdk_with_quota_project_id.json"
+)
+
+SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json")
+
+GDCH_SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "gdch_service_account.json")
+
+with open(SERVICE_ACCOUNT_FILE) as fh:
+ SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
+
+SUBJECT_TOKEN_TEXT_FILE = os.path.join(DATA_DIR, "external_subject_token.txt")
+TOKEN_URL = "https://sts.googleapis.com/v1/token"
+AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
+WORKFORCE_AUDIENCE = (
+ "//iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID"
+)
+WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER"
+REGION_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone"
+SECURITY_CREDS_URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials"
+CRED_VERIFICATION_URL = (
+ "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
+)
+IDENTITY_POOL_DATA = {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
+ "token_url": TOKEN_URL,
+ "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
+}
+PLUGGABLE_DATA = {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
+ "token_url": TOKEN_URL,
+ "credential_source": {"executable": {"command": "command"}},
+}
+AWS_DATA = {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
+ "token_url": TOKEN_URL,
+ "credential_source": {
+ "environment_id": "aws1",
+ "region_url": REGION_URL,
+ "url": SECURITY_CREDS_URL,
+ "regional_cred_verification_url": CRED_VERIFICATION_URL,
+ },
+}
+SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com"
+SERVICE_ACCOUNT_IMPERSONATION_URL = (
+ "https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
+ + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
+)
+IMPERSONATED_IDENTITY_POOL_DATA = {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
+ "token_url": TOKEN_URL,
+ "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
+ "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+}
+IMPERSONATED_AWS_DATA = {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
+ "token_url": TOKEN_URL,
+ "credential_source": {
+ "environment_id": "aws1",
+ "region_url": REGION_URL,
+ "url": SECURITY_CREDS_URL,
+ "regional_cred_verification_url": CRED_VERIFICATION_URL,
+ },
+ "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+}
+IDENTITY_POOL_WORKFORCE_DATA = {
+ "type": "external_account",
+ "audience": WORKFORCE_AUDIENCE,
+ "subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "token_url": TOKEN_URL,
+ "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
+ "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
+}
+IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA = {
+ "type": "external_account",
+ "audience": WORKFORCE_AUDIENCE,
+ "subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "token_url": TOKEN_URL,
+ "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
+ "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
+}
+
+IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE = os.path.join(
+ DATA_DIR, "impersonated_service_account_authorized_user_source.json"
+)
+
+IMPERSONATED_SERVICE_ACCOUNT_WITH_QUOTA_PROJECT_FILE = os.path.join(
+ DATA_DIR, "impersonated_service_account_with_quota_project.json"
+)
+
+IMPERSONATED_SERVICE_ACCOUNT_SERVICE_ACCOUNT_SOURCE_FILE = os.path.join(
+ DATA_DIR, "impersonated_service_account_service_account_source.json"
+)
+
+EXTERNAL_ACCOUNT_AUTHORIZED_USER_FILE = os.path.join(
+ DATA_DIR, "external_account_authorized_user.json"
+)
+
+MOCK_CREDENTIALS = mock.Mock(spec=credentials.CredentialsWithQuotaProject)
+MOCK_CREDENTIALS.with_quota_project.return_value = MOCK_CREDENTIALS
+
+
+def get_project_id_side_effect(self, request=None):
+ # If no scopes are set, this will always return None.
+ if not self.scopes:
+ return None
+ return mock.sentinel.project_id
+
+
+LOAD_FILE_PATCH = mock.patch(
+ "google.auth._default.load_credentials_from_file",
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+ autospec=True,
+)
+EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH = mock.patch.object(
+ external_account.Credentials,
+ "get_project_id",
+ side_effect=get_project_id_side_effect,
+ autospec=True,
+)
+
+
+def test_load_credentials_from_missing_file():
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_file("")
+
+ assert excinfo.match(r"not found")
+
+
+def test_load_credentials_from_dict_non_dict_object():
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_dict("")
+ assert excinfo.match(r"dict type was expected")
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_dict(None)
+ assert excinfo.match(r"dict type was expected")
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_dict(1)
+ assert excinfo.match(r"dict type was expected")
+
+
+def test_load_credentials_from_dict_authorized_user():
+ credentials, project_id = _default.load_credentials_from_dict(
+ AUTHORIZED_USER_FILE_DATA
+ )
+ assert isinstance(credentials, google.oauth2.credentials.Credentials)
+ assert project_id is None
+
+
+def test_load_credentials_from_file_invalid_json(tmpdir):
+ jsonfile = tmpdir.join("invalid.json")
+ jsonfile.write("{")
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_file(str(jsonfile))
+
+ assert excinfo.match(r"not a valid json file")
+
+
+def test_load_credentials_from_file_invalid_type(tmpdir):
+ jsonfile = tmpdir.join("invalid.json")
+ jsonfile.write(json.dumps({"type": "not-a-real-type"}))
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_file(str(jsonfile))
+
+ assert excinfo.match(r"does not have a valid type")
+
+
+def test_load_credentials_from_file_authorized_user():
+ credentials, project_id = _default.load_credentials_from_file(AUTHORIZED_USER_FILE)
+ assert isinstance(credentials, google.oauth2.credentials.Credentials)
+ assert project_id is None
+
+
+def test_load_credentials_from_file_no_type(tmpdir):
+ # use the client_secrets.json, which is valid json but not a
+ # loadable credentials type
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_file(CLIENT_SECRETS_FILE)
+
+ assert excinfo.match(r"does not have a valid type")
+ assert excinfo.match(r"Type is None")
+
+
+def test_load_credentials_from_file_authorized_user_bad_format(tmpdir):
+ filename = tmpdir.join("authorized_user_bad.json")
+ filename.write(json.dumps({"type": "authorized_user"}))
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_file(str(filename))
+
+ assert excinfo.match(r"Failed to load authorized user")
+ assert excinfo.match(r"missing fields")
+
+
+def test_load_credentials_from_file_authorized_user_cloud_sdk():
+ with pytest.warns(UserWarning, match="Cloud SDK"):
+ credentials, project_id = _default.load_credentials_from_file(
+ AUTHORIZED_USER_CLOUD_SDK_FILE
+ )
+ assert isinstance(credentials, google.oauth2.credentials.Credentials)
+ assert project_id is None
+
+ # No warning if the json file has quota project id.
+ credentials, project_id = _default.load_credentials_from_file(
+ AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE
+ )
+ assert isinstance(credentials, google.oauth2.credentials.Credentials)
+ assert project_id is None
+
+
+def test_load_credentials_from_file_authorized_user_cloud_sdk_with_scopes():
+ with pytest.warns(UserWarning, match="Cloud SDK"):
+ credentials, project_id = _default.load_credentials_from_file(
+ AUTHORIZED_USER_CLOUD_SDK_FILE,
+ scopes=["https://www.google.com/calendar/feeds"],
+ )
+ assert isinstance(credentials, google.oauth2.credentials.Credentials)
+ assert project_id is None
+ assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+def test_load_credentials_from_file_authorized_user_cloud_sdk_with_quota_project():
+ credentials, project_id = _default.load_credentials_from_file(
+ AUTHORIZED_USER_CLOUD_SDK_FILE, quota_project_id="project-foo"
+ )
+
+ assert isinstance(credentials, google.oauth2.credentials.Credentials)
+ assert project_id is None
+ assert credentials.quota_project_id == "project-foo"
+
+
+def test_load_credentials_from_file_service_account():
+ credentials, project_id = _default.load_credentials_from_file(SERVICE_ACCOUNT_FILE)
+ assert isinstance(credentials, service_account.Credentials)
+ assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"]
+
+
+def test_load_credentials_from_file_service_account_with_scopes():
+ credentials, project_id = _default.load_credentials_from_file(
+ SERVICE_ACCOUNT_FILE, scopes=["https://www.google.com/calendar/feeds"]
+ )
+ assert isinstance(credentials, service_account.Credentials)
+ assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"]
+ assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+def test_load_credentials_from_file_service_account_with_quota_project():
+ credentials, project_id = _default.load_credentials_from_file(
+ SERVICE_ACCOUNT_FILE, quota_project_id="project-foo"
+ )
+ assert isinstance(credentials, service_account.Credentials)
+ assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"]
+ assert credentials.quota_project_id == "project-foo"
+
+
+def test_load_credentials_from_file_service_account_bad_format(tmpdir):
+ filename = tmpdir.join("serivce_account_bad.json")
+ filename.write(json.dumps({"type": "service_account"}))
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_file(str(filename))
+
+ assert excinfo.match(r"Failed to load service account")
+ assert excinfo.match(r"missing fields")
+
+
+def test_load_credentials_from_file_impersonated_with_authorized_user_source():
+ credentials, project_id = _default.load_credentials_from_file(
+ IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE
+ )
+ assert isinstance(credentials, impersonated_credentials.Credentials)
+ assert isinstance(
+ credentials._source_credentials, google.oauth2.credentials.Credentials
+ )
+ assert credentials.service_account_email == "service-account-target@example.com"
+ assert credentials._delegates == ["service-account-delegate@example.com"]
+ assert not credentials._quota_project_id
+ assert not credentials._target_scopes
+ assert project_id is None
+
+
+def test_load_credentials_from_file_impersonated_with_quota_project():
+ credentials, _ = _default.load_credentials_from_file(
+ IMPERSONATED_SERVICE_ACCOUNT_WITH_QUOTA_PROJECT_FILE
+ )
+ assert isinstance(credentials, impersonated_credentials.Credentials)
+ assert credentials._quota_project_id == "quota_project"
+
+
+def test_load_credentials_from_file_impersonated_with_service_account_source():
+ credentials, _ = _default.load_credentials_from_file(
+ IMPERSONATED_SERVICE_ACCOUNT_SERVICE_ACCOUNT_SOURCE_FILE
+ )
+ assert isinstance(credentials, impersonated_credentials.Credentials)
+ assert isinstance(credentials._source_credentials, service_account.Credentials)
+ assert not credentials._quota_project_id
+
+
+def test_load_credentials_from_file_impersonated_passing_quota_project():
+ credentials, _ = _default.load_credentials_from_file(
+ IMPERSONATED_SERVICE_ACCOUNT_SERVICE_ACCOUNT_SOURCE_FILE,
+ quota_project_id="new_quota_project",
+ )
+ assert credentials._quota_project_id == "new_quota_project"
+
+
+def test_load_credentials_from_file_impersonated_passing_scopes():
+ credentials, _ = _default.load_credentials_from_file(
+ IMPERSONATED_SERVICE_ACCOUNT_SERVICE_ACCOUNT_SOURCE_FILE,
+ scopes=["scope1", "scope2"],
+ )
+ assert credentials._target_scopes == ["scope1", "scope2"]
+
+
+def test_load_credentials_from_file_impersonated_wrong_target_principal(tmpdir):
+
+ with open(IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE) as fh:
+ impersonated_credentials_info = json.load(fh)
+ impersonated_credentials_info[
+ "service_account_impersonation_url"
+ ] = "something_wrong"
+
+ jsonfile = tmpdir.join("invalid.json")
+ jsonfile.write(json.dumps(impersonated_credentials_info))
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_file(str(jsonfile))
+
+ assert excinfo.match(r"Cannot extract target principal")
+
+
+def test_load_credentials_from_file_impersonated_wrong_source_type(tmpdir):
+
+ with open(IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE) as fh:
+ impersonated_credentials_info = json.load(fh)
+ impersonated_credentials_info["source_credentials"]["type"] = "external_account"
+
+ jsonfile = tmpdir.join("invalid.json")
+ jsonfile.write(json.dumps(impersonated_credentials_info))
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_file(str(jsonfile))
+
+ assert excinfo.match(r"source credential of type external_account is not supported")
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_identity_pool(
+ get_project_id, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IDENTITY_POOL_DATA))
+ credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ # Since no scopes are specified, the project ID cannot be determined.
+ assert project_id is None
+ assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_aws(get_project_id, tmpdir):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(AWS_DATA))
+ credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+ assert isinstance(credentials, aws.Credentials)
+ # Since no scopes are specified, the project ID cannot be determined.
+ assert project_id is None
+ assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_identity_pool_impersonated(
+ get_project_id, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA))
+ credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert not credentials.is_user
+ assert not credentials.is_workforce_pool
+ # Since no scopes are specified, the project ID cannot be determined.
+ assert project_id is None
+ assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_aws_impersonated(
+ get_project_id, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IMPERSONATED_AWS_DATA))
+ credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+ assert isinstance(credentials, aws.Credentials)
+ assert not credentials.is_user
+ assert not credentials.is_workforce_pool
+ # Since no scopes are specified, the project ID cannot be determined.
+ assert project_id is None
+ assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_workforce(get_project_id, tmpdir):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IDENTITY_POOL_WORKFORCE_DATA))
+ credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert credentials.is_user
+ assert credentials.is_workforce_pool
+ # Since no scopes are specified, the project ID cannot be determined.
+ assert project_id is None
+ assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_workforce_impersonated(
+ get_project_id, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA))
+ credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert not credentials.is_user
+ assert credentials.is_workforce_pool
+ # Since no scopes are specified, the project ID cannot be determined.
+ assert project_id is None
+ assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_with_user_and_default_scopes(
+ get_project_id, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IDENTITY_POOL_DATA))
+ credentials, project_id = _default.load_credentials_from_file(
+ str(config_file),
+ scopes=["https://www.google.com/calendar/feeds"],
+ default_scopes=["https://www.googleapis.com/auth/cloud-platform"],
+ )
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ # Since scopes are specified, the project ID can be determined.
+ assert project_id is mock.sentinel.project_id
+ assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+ assert credentials.default_scopes == [
+ "https://www.googleapis.com/auth/cloud-platform"
+ ]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_with_quota_project(
+ get_project_id, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IDENTITY_POOL_DATA))
+ credentials, project_id = _default.load_credentials_from_file(
+ str(config_file), quota_project_id="project-foo"
+ )
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ # Since no scopes are specified, the project ID cannot be determined.
+ assert project_id is None
+ assert credentials.quota_project_id == "project-foo"
+
+
+def test_load_credentials_from_file_external_account_bad_format(tmpdir):
+ filename = tmpdir.join("external_account_bad.json")
+ filename.write(json.dumps({"type": "external_account"}))
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_file(str(filename))
+
+ assert excinfo.match(
+ "Failed to load external account credentials from {}".format(str(filename))
+ )
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_explicit_request(
+ get_project_id, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IDENTITY_POOL_DATA))
+ credentials, project_id = _default.load_credentials_from_file(
+ str(config_file),
+ request=mock.sentinel.request,
+ scopes=["https://www.googleapis.com/auth/cloud-platform"],
+ )
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ # Since scopes are specified, the project ID can be determined.
+ assert project_id is mock.sentinel.project_id
+ get_project_id.assert_called_with(credentials, request=mock.sentinel.request)
+
+
+@mock.patch.dict(os.environ, {}, clear=True)
+def test__get_explicit_environ_credentials_no_env():
+ assert _default._get_explicit_environ_credentials() == (None, None)
+
+
+def test_load_credentials_from_file_external_account_authorized_user():
+ credentials, project_id = _default.load_credentials_from_file(
+ EXTERNAL_ACCOUNT_AUTHORIZED_USER_FILE, request=mock.sentinel.request
+ )
+
+ assert isinstance(credentials, external_account_authorized_user.Credentials)
+ assert project_id is None
+
+
+def test_load_credentials_from_file_external_account_authorized_user_bad_format(tmpdir):
+ filename = tmpdir.join("external_account_authorized_user_bad.json")
+ filename.write(json.dumps({"type": "external_account_authorized_user"}))
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.load_credentials_from_file(str(filename))
+
+ assert excinfo.match(
+ "Failed to load external account authorized user credentials from {}".format(
+ str(filename)
+ )
+ )
+
+
+@pytest.mark.parametrize("quota_project_id", [None, "project-foo"])
+@LOAD_FILE_PATCH
+def test__get_explicit_environ_credentials(load, quota_project_id, monkeypatch):
+ monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
+
+ credentials, project_id = _default._get_explicit_environ_credentials(
+ quota_project_id=quota_project_id
+ )
+
+ assert credentials is MOCK_CREDENTIALS
+ assert project_id is mock.sentinel.project_id
+ load.assert_called_with("filename", quota_project_id=quota_project_id)
+
+
+@LOAD_FILE_PATCH
+def test__get_explicit_environ_credentials_no_project_id(load, monkeypatch):
+ load.return_value = MOCK_CREDENTIALS, None
+ monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
+
+ credentials, project_id = _default._get_explicit_environ_credentials()
+
+ assert credentials is MOCK_CREDENTIALS
+ assert project_id is None
+
+
+@pytest.mark.parametrize("quota_project_id", [None, "project-foo"])
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+@mock.patch("google.auth._default._get_gcloud_sdk_credentials", autospec=True)
+def test__get_explicit_environ_credentials_fallback_to_gcloud(
+ get_gcloud_creds, get_adc_path, quota_project_id, monkeypatch
+):
+ # Set explicit credentials path to cloud sdk credentials path.
+ get_adc_path.return_value = "filename"
+ monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
+
+ _default._get_explicit_environ_credentials(quota_project_id=quota_project_id)
+
+ # Check we fall back to cloud sdk flow since explicit credentials path is
+ # cloud sdk credentials path
+ get_gcloud_creds.assert_called_with(quota_project_id=quota_project_id)
+
+
+@pytest.mark.parametrize("quota_project_id", [None, "project-foo"])
+@LOAD_FILE_PATCH
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test__get_gcloud_sdk_credentials(get_adc_path, load, quota_project_id):
+ get_adc_path.return_value = SERVICE_ACCOUNT_FILE
+
+ credentials, project_id = _default._get_gcloud_sdk_credentials(
+ quota_project_id=quota_project_id
+ )
+
+ assert credentials is MOCK_CREDENTIALS
+ assert project_id is mock.sentinel.project_id
+ load.assert_called_with(SERVICE_ACCOUNT_FILE, quota_project_id=quota_project_id)
+
+
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test__get_gcloud_sdk_credentials_non_existent(get_adc_path, tmpdir):
+ non_existent = tmpdir.join("non-existent")
+ get_adc_path.return_value = str(non_existent)
+
+ credentials, project_id = _default._get_gcloud_sdk_credentials()
+
+ assert credentials is None
+ assert project_id is None
+
+
+@mock.patch(
+ "google.auth._cloud_sdk.get_project_id",
+ return_value=mock.sentinel.project_id,
+ autospec=True,
+)
+@mock.patch("os.path.isfile", return_value=True, autospec=True)
+@LOAD_FILE_PATCH
+def test__get_gcloud_sdk_credentials_project_id(load, unused_isfile, get_project_id):
+ # Don't return a project ID from load file, make the function check
+ # the Cloud SDK project.
+ load.return_value = MOCK_CREDENTIALS, None
+
+ credentials, project_id = _default._get_gcloud_sdk_credentials()
+
+ assert credentials == MOCK_CREDENTIALS
+ assert project_id == mock.sentinel.project_id
+ assert get_project_id.called
+
+
+@mock.patch("google.auth._cloud_sdk.get_project_id", return_value=None, autospec=True)
+@mock.patch("os.path.isfile", return_value=True)
+@LOAD_FILE_PATCH
+def test__get_gcloud_sdk_credentials_no_project_id(load, unused_isfile, get_project_id):
+ # Don't return a project ID from load file, make the function check
+ # the Cloud SDK project.
+ load.return_value = MOCK_CREDENTIALS, None
+
+ credentials, project_id = _default._get_gcloud_sdk_credentials()
+
+ assert credentials == MOCK_CREDENTIALS
+ assert project_id is None
+ assert get_project_id.called
+
+
+def test__get_gdch_service_account_credentials_invalid_format_version():
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default._get_gdch_service_account_credentials(
+ "file_name", {"format_version": "2"}
+ )
+ assert excinfo.match("Failed to load GDCH service account credentials")
+
+
+def test_get_api_key_credentials():
+ creds = _default.get_api_key_credentials("api_key")
+ assert isinstance(creds, api_key.Credentials)
+ assert creds.token == "api_key"
+
+
+class _AppIdentityModule(object):
+ """The interface of the App Idenity app engine module.
+ See https://cloud.google.com/appengine/docs/standard/python/refdocs\
+ /google.appengine.api.app_identity.app_identity
+ """
+
+ def get_application_id(self):
+ raise NotImplementedError()
+
+
+@pytest.fixture
+def app_identity(monkeypatch):
+ """Mocks the app_identity module for google.auth.app_engine."""
+ app_identity_module = mock.create_autospec(_AppIdentityModule, instance=True)
+ monkeypatch.setattr(app_engine, "app_identity", app_identity_module)
+ yield app_identity_module
+
+
+@mock.patch.dict(os.environ)
+def test__get_gae_credentials_gen1(app_identity):
+ os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
+ app_identity.get_application_id.return_value = mock.sentinel.project
+
+ credentials, project_id = _default._get_gae_credentials()
+
+ assert isinstance(credentials, app_engine.Credentials)
+ assert project_id == mock.sentinel.project
+
+
+@mock.patch.dict(os.environ)
+def test__get_gae_credentials_gen2():
+ os.environ["GAE_RUNTIME"] = "python37"
+ credentials, project_id = _default._get_gae_credentials()
+ assert credentials is None
+ assert project_id is None
+
+
+@mock.patch.dict(os.environ)
+def test__get_gae_credentials_gen2_backwards_compat():
+ # compat helpers may copy GAE_RUNTIME to APPENGINE_RUNTIME
+ # for backwards compatibility with code that relies on it
+ os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python37"
+ os.environ["GAE_RUNTIME"] = "python37"
+ credentials, project_id = _default._get_gae_credentials()
+ assert credentials is None
+ assert project_id is None
+
+
+def test__get_gae_credentials_env_unset():
+ assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
+ assert "GAE_RUNTIME" not in os.environ
+ credentials, project_id = _default._get_gae_credentials()
+ assert credentials is None
+ assert project_id is None
+
+
+@mock.patch.dict(os.environ)
+def test__get_gae_credentials_no_app_engine():
+ # test both with and without LEGACY_APPENGINE_RUNTIME setting
+ assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
+
+ import sys
+
+ with mock.patch.dict(sys.modules, {"google.auth.app_engine": None}):
+ credentials, project_id = _default._get_gae_credentials()
+ assert credentials is None
+ assert project_id is None
+
+ os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
+ credentials, project_id = _default._get_gae_credentials()
+ assert credentials is None
+ assert project_id is None
+
+
+@mock.patch.dict(os.environ)
+@mock.patch.object(app_engine, "app_identity", new=None)
+def test__get_gae_credentials_no_apis():
+ # test both with and without LEGACY_APPENGINE_RUNTIME setting
+ assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
+
+ credentials, project_id = _default._get_gae_credentials()
+ assert credentials is None
+ assert project_id is None
+
+ os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
+ credentials, project_id = _default._get_gae_credentials()
+ assert credentials is None
+ assert project_id is None
+
+
+@mock.patch(
+ "google.auth.compute_engine._metadata.is_on_gce", return_value=True, autospec=True
+)
+@mock.patch(
+ "google.auth.compute_engine._metadata.get_project_id",
+ return_value="example-project",
+ autospec=True,
+)
+def test__get_gce_credentials(unused_get, unused_ping):
+ credentials, project_id = _default._get_gce_credentials()
+
+ assert isinstance(credentials, compute_engine.Credentials)
+ assert project_id == "example-project"
+
+
+@mock.patch(
+ "google.auth.compute_engine._metadata.is_on_gce", return_value=False, autospec=True
+)
+def test__get_gce_credentials_no_ping(unused_ping):
+ credentials, project_id = _default._get_gce_credentials()
+
+ assert credentials is None
+ assert project_id is None
+
+
+@mock.patch(
+ "google.auth.compute_engine._metadata.is_on_gce", return_value=True, autospec=True
+)
+@mock.patch(
+ "google.auth.compute_engine._metadata.get_project_id",
+ side_effect=exceptions.TransportError(),
+ autospec=True,
+)
+def test__get_gce_credentials_no_project_id(unused_get, unused_ping):
+ credentials, project_id = _default._get_gce_credentials()
+
+ assert isinstance(credentials, compute_engine.Credentials)
+ assert project_id is None
+
+
+def test__get_gce_credentials_no_compute_engine():
+ import sys
+
+ with mock.patch.dict("sys.modules"):
+ sys.modules["google.auth.compute_engine"] = None
+ credentials, project_id = _default._get_gce_credentials()
+ assert credentials is None
+ assert project_id is None
+
+
+@mock.patch(
+ "google.auth.compute_engine._metadata.is_on_gce", return_value=False, autospec=True
+)
+def test__get_gce_credentials_explicit_request(ping):
+ _default._get_gce_credentials(mock.sentinel.request)
+ ping.assert_called_with(request=mock.sentinel.request)
+
+
+@mock.patch(
+ "google.auth._default._get_explicit_environ_credentials",
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+ autospec=True,
+)
+def test_default_early_out(unused_get):
+ assert _default.default() == (MOCK_CREDENTIALS, mock.sentinel.project_id)
+
+
+@mock.patch(
+ "google.auth._default._get_explicit_environ_credentials",
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+ autospec=True,
+)
+def test_default_explict_project_id(unused_get, monkeypatch):
+ monkeypatch.setenv(environment_vars.PROJECT, "explicit-env")
+ assert _default.default() == (MOCK_CREDENTIALS, "explicit-env")
+
+
+@mock.patch(
+ "google.auth._default._get_explicit_environ_credentials",
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+ autospec=True,
+)
+def test_default_explict_legacy_project_id(unused_get, monkeypatch):
+ monkeypatch.setenv(environment_vars.LEGACY_PROJECT, "explicit-env")
+ assert _default.default() == (MOCK_CREDENTIALS, "explicit-env")
+
+
+@mock.patch("logging.Logger.warning", autospec=True)
+@mock.patch(
+ "google.auth._default._get_explicit_environ_credentials",
+ return_value=(MOCK_CREDENTIALS, None),
+ autospec=True,
+)
+@mock.patch(
+ "google.auth._default._get_gcloud_sdk_credentials",
+ return_value=(MOCK_CREDENTIALS, None),
+ autospec=True,
+)
+@mock.patch(
+ "google.auth._default._get_gae_credentials",
+ return_value=(MOCK_CREDENTIALS, None),
+ autospec=True,
+)
+@mock.patch(
+ "google.auth._default._get_gce_credentials",
+ return_value=(MOCK_CREDENTIALS, None),
+ autospec=True,
+)
+def test_default_without_project_id(
+ unused_gce, unused_gae, unused_sdk, unused_explicit, logger_warning
+):
+ assert _default.default() == (MOCK_CREDENTIALS, None)
+ logger_warning.assert_called_with(mock.ANY, mock.ANY, mock.ANY)
+
+
+@mock.patch(
+ "google.auth._default._get_explicit_environ_credentials",
+ return_value=(None, None),
+ autospec=True,
+)
+@mock.patch(
+ "google.auth._default._get_gcloud_sdk_credentials",
+ return_value=(None, None),
+ autospec=True,
+)
+@mock.patch(
+ "google.auth._default._get_gae_credentials",
+ return_value=(None, None),
+ autospec=True,
+)
+@mock.patch(
+ "google.auth._default._get_gce_credentials",
+ return_value=(None, None),
+ autospec=True,
+)
+def test_default_fail(unused_gce, unused_gae, unused_sdk, unused_explicit):
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ assert _default.default()
+
+ assert excinfo.match(_default._CLOUD_SDK_MISSING_CREDENTIALS)
+
+
+@mock.patch(
+ "google.auth._default._get_explicit_environ_credentials",
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+ autospec=True,
+)
+@mock.patch(
+ "google.auth.credentials.with_scopes_if_required",
+ return_value=MOCK_CREDENTIALS,
+ autospec=True,
+)
+def test_default_scoped(with_scopes, unused_get):
+ scopes = ["one", "two"]
+
+ credentials, project_id = _default.default(scopes=scopes)
+
+ assert credentials == with_scopes.return_value
+ assert project_id == mock.sentinel.project_id
+ with_scopes.assert_called_once_with(MOCK_CREDENTIALS, scopes, default_scopes=None)
+
+
+@mock.patch(
+ "google.auth._default._get_explicit_environ_credentials",
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+ autospec=True,
+)
+def test_default_quota_project(with_quota_project):
+ credentials, project_id = _default.default(quota_project_id="project-foo")
+
+ MOCK_CREDENTIALS.with_quota_project.assert_called_once_with("project-foo")
+ assert project_id == mock.sentinel.project_id
+
+
+@mock.patch(
+ "google.auth._default._get_explicit_environ_credentials",
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+ autospec=True,
+)
+def test_default_no_app_engine_compute_engine_module(unused_get):
+ """
+ google.auth.compute_engine and google.auth.app_engine are both optional
+ to allow not including them when using this package. This verifies
+ that default fails gracefully if these modules are absent
+ """
+ import sys
+
+ with mock.patch.dict("sys.modules"):
+ sys.modules["google.auth.compute_engine"] = None
+ sys.modules["google.auth.app_engine"] = None
+ assert _default.default() == (MOCK_CREDENTIALS, mock.sentinel.project_id)
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_identity_pool(
+ get_project_id, monkeypatch, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IDENTITY_POOL_DATA))
+ monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+ credentials, project_id = _default.default()
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert not credentials.is_user
+ assert not credentials.is_workforce_pool
+ # Without scopes, project ID cannot be determined.
+ assert project_id is None
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_identity_pool_impersonated(
+ get_project_id, monkeypatch, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA))
+ monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+ credentials, project_id = _default.default(
+ scopes=["https://www.google.com/calendar/feeds"]
+ )
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert not credentials.is_user
+ assert not credentials.is_workforce_pool
+ assert project_id is mock.sentinel.project_id
+ assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+ # The credential.get_project_id should have been used in _get_external_account_credentials and default
+ assert get_project_id.call_count == 2
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+@mock.patch.dict(os.environ)
+def test_default_environ_external_credentials_project_from_env(
+ get_project_id, monkeypatch, tmpdir
+):
+ project_from_env = "project_from_env"
+ os.environ[environment_vars.PROJECT] = project_from_env
+
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA))
+ monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+ credentials, project_id = _default.default(
+ scopes=["https://www.google.com/calendar/feeds"]
+ )
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert not credentials.is_user
+ assert not credentials.is_workforce_pool
+ assert project_id == project_from_env
+ assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+ # The credential.get_project_id should have been used only in _get_external_account_credentials
+ assert get_project_id.call_count == 1
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+@mock.patch.dict(os.environ)
+def test_default_environ_external_credentials_legacy_project_from_env(
+ get_project_id, monkeypatch, tmpdir
+):
+ project_from_env = "project_from_env"
+ os.environ[environment_vars.LEGACY_PROJECT] = project_from_env
+
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA))
+ monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+ credentials, project_id = _default.default(
+ scopes=["https://www.google.com/calendar/feeds"]
+ )
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert not credentials.is_user
+ assert not credentials.is_workforce_pool
+ assert project_id == project_from_env
+ assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+ # The credential.get_project_id should have been used only in _get_external_account_credentials
+ assert get_project_id.call_count == 1
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_aws_impersonated(
+ get_project_id, monkeypatch, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IMPERSONATED_AWS_DATA))
+ monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+ credentials, project_id = _default.default(
+ scopes=["https://www.google.com/calendar/feeds"]
+ )
+
+ assert isinstance(credentials, aws.Credentials)
+ assert not credentials.is_user
+ assert not credentials.is_workforce_pool
+ assert project_id is mock.sentinel.project_id
+ assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_workforce(
+ get_project_id, monkeypatch, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IDENTITY_POOL_WORKFORCE_DATA))
+ monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+ credentials, project_id = _default.default(
+ scopes=["https://www.google.com/calendar/feeds"]
+ )
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert credentials.is_user
+ assert credentials.is_workforce_pool
+ assert project_id is mock.sentinel.project_id
+ assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_workforce_impersonated(
+ get_project_id, monkeypatch, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA))
+ monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+ credentials, project_id = _default.default(
+ scopes=["https://www.google.com/calendar/feeds"]
+ )
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert not credentials.is_user
+ assert credentials.is_workforce_pool
+ assert project_id is mock.sentinel.project_id
+ assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_with_user_and_default_scopes_and_quota_project_id(
+ get_project_id, monkeypatch, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IDENTITY_POOL_DATA))
+ monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+ credentials, project_id = _default.default(
+ scopes=["https://www.google.com/calendar/feeds"],
+ default_scopes=["https://www.googleapis.com/auth/cloud-platform"],
+ quota_project_id="project-foo",
+ )
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert project_id is mock.sentinel.project_id
+ assert credentials.quota_project_id == "project-foo"
+ assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+ assert credentials.default_scopes == [
+ "https://www.googleapis.com/auth/cloud-platform"
+ ]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_explicit_request_with_scopes(
+ get_project_id, monkeypatch, tmpdir
+):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(IDENTITY_POOL_DATA))
+ monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+ credentials, project_id = _default.default(
+ request=mock.sentinel.request,
+ scopes=["https://www.googleapis.com/auth/cloud-platform"],
+ )
+
+ assert isinstance(credentials, identity_pool.Credentials)
+ assert project_id is mock.sentinel.project_id
+ # default() will initialize new credentials via with_scopes_if_required
+ # and potentially with_quota_project.
+ # As a result the caller of get_project_id() will not match the returned
+ # credentials.
+ get_project_id.assert_called_with(mock.ANY, request=mock.sentinel.request)
+
+
+def test_default_environ_external_credentials_bad_format(monkeypatch, tmpdir):
+ filename = tmpdir.join("external_account_bad.json")
+ filename.write(json.dumps({"type": "external_account"}))
+ monkeypatch.setenv(environment_vars.CREDENTIALS, str(filename))
+
+ with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+ _default.default()
+
+ assert excinfo.match(
+ "Failed to load external account credentials from {}".format(str(filename))
+ )
+
+
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_warning_without_quota_project_id_for_user_creds(get_adc_path):
+ get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_FILE
+
+ with pytest.warns(UserWarning, match=_default._CLOUD_SDK_CREDENTIALS_WARNING):
+ credentials, project_id = _default.default(quota_project_id=None)
+
+
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_no_warning_with_quota_project_id_for_user_creds(get_adc_path):
+ get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_FILE
+
+ credentials, project_id = _default.default(quota_project_id="project-foo")
+
+
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_impersonated_service_account(get_adc_path):
+ get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE
+
+ credentials, _ = _default.default()
+
+ assert isinstance(credentials, impersonated_credentials.Credentials)
+ assert isinstance(
+ credentials._source_credentials, google.oauth2.credentials.Credentials
+ )
+ assert credentials.service_account_email == "service-account-target@example.com"
+ assert credentials._delegates == ["service-account-delegate@example.com"]
+ assert not credentials._quota_project_id
+ assert not credentials._target_scopes
+
+
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_impersonated_service_account_set_scopes(get_adc_path):
+ get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE
+ scopes = ["scope1", "scope2"]
+
+ credentials, _ = _default.default(scopes=scopes)
+ assert credentials._target_scopes == scopes
+
+
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_impersonated_service_account_set_default_scopes(get_adc_path):
+ get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE
+ default_scopes = ["scope1", "scope2"]
+
+ credentials, _ = _default.default(default_scopes=default_scopes)
+ assert credentials._target_scopes == default_scopes
+
+
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_impersonated_service_account_set_both_scopes_and_default_scopes(
+ get_adc_path
+):
+ get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE
+ scopes = ["scope1", "scope2"]
+ default_scopes = ["scope3", "scope4"]
+
+ credentials, _ = _default.default(scopes=scopes, default_scopes=default_scopes)
+ assert credentials._target_scopes == scopes
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_external_account_pluggable(get_project_id, tmpdir):
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(PLUGGABLE_DATA))
+ credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+ assert isinstance(credentials, pluggable.Credentials)
+ # Since no scopes are specified, the project ID cannot be determined.
+ assert project_id is None
+ assert get_project_id.called
+
+
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_gdch_service_account_credentials(get_adc_path):
+ get_adc_path.return_value = GDCH_SERVICE_ACCOUNT_FILE
+
+ creds, project = _default.default(quota_project_id="project-foo")
+
+ assert isinstance(creds, gdch_credentials.ServiceAccountCredentials)
+ assert creds._service_identity_name == "service_identity_name"
+ assert creds._audience is None
+ assert creds._token_uri == "https://service-identity.<Domain>/authenticate"
+ assert creds._ca_cert_path == "/path/to/ca/cert"
+ assert project == "project_foo"
+
+
+@mock.patch.dict(os.environ)
+@mock.patch(
+ "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_quota_project_from_environment(get_adc_path):
+ get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE
+
+ credentials, _ = _default.default(quota_project_id=None)
+ assert credentials.quota_project_id == "quota_project_id"
+
+ quota_from_env = "quota_from_env"
+ os.environ[environment_vars.GOOGLE_CLOUD_QUOTA_PROJECT] = quota_from_env
+ credentials, _ = _default.default(quota_project_id=None)
+ assert credentials.quota_project_id == quota_from_env
+
+ explicit_quota = "explicit_quota"
+ credentials, _ = _default.default(quota_project_id=explicit_quota)
+ assert credentials.quota_project_id == explicit_quota
+
+
+@mock.patch(
+ "google.auth.compute_engine._metadata.is_on_gce", return_value=True, autospec=True
+)
+@mock.patch(
+ "google.auth.compute_engine._metadata.get_project_id",
+ return_value="example-project",
+ autospec=True,
+)
+@mock.patch.dict(os.environ)
+def test_quota_gce_credentials(unused_get, unused_ping):
+ # No quota
+ credentials, project_id = _default._get_gce_credentials()
+ assert project_id == "example-project"
+ assert credentials.quota_project_id is None
+
+ # Quota from environment
+ quota_from_env = "quota_from_env"
+ os.environ[environment_vars.GOOGLE_CLOUD_QUOTA_PROJECT] = quota_from_env
+ credentials, project_id = _default._get_gce_credentials()
+ assert credentials.quota_project_id == quota_from_env
+
+ # Explicit quota
+ explicit_quota = "explicit_quota"
+ credentials, project_id = _default._get_gce_credentials(
+ quota_project_id=explicit_quota
+ )
+ assert credentials.quota_project_id == explicit_quota
diff --git a/contrib/python/google-auth/py3/tests/test__exponential_backoff.py b/contrib/python/google-auth/py3/tests/test__exponential_backoff.py
new file mode 100644
index 0000000000..06a54527e6
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test__exponential_backoff.py
@@ -0,0 +1,41 @@
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+
+from google.auth import _exponential_backoff
+
+
+@mock.patch("time.sleep", return_value=None)
+def test_exponential_backoff(mock_time):
+ eb = _exponential_backoff.ExponentialBackoff()
+ curr_wait = eb._current_wait_in_seconds
+ iteration_count = 0
+
+ for attempt in eb:
+ backoff_interval = mock_time.call_args[0][0]
+ jitter = curr_wait * eb._randomization_factor
+
+ assert (curr_wait - jitter) <= backoff_interval <= (curr_wait + jitter)
+ assert attempt == iteration_count + 1
+ assert eb.backoff_count == iteration_count + 1
+ assert eb._current_wait_in_seconds == eb._multiplier ** (iteration_count + 1)
+
+ curr_wait = eb._current_wait_in_seconds
+ iteration_count += 1
+
+ assert eb.total_attempts == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS
+ assert eb.backoff_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS
+ assert iteration_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS
+ assert mock_time.call_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS
diff --git a/contrib/python/google-auth/py3/tests/test__helpers.py b/contrib/python/google-auth/py3/tests/test__helpers.py
new file mode 100644
index 0000000000..c1f1d812e5
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test__helpers.py
@@ -0,0 +1,170 @@
+# Copyright 2016 Google LLC
+#
+# 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 datetime
+import urllib
+
+import pytest # type: ignore
+
+from google.auth import _helpers
+
+
+class SourceClass(object):
+ def func(self): # pragma: NO COVER
+ """example docstring"""
+
+
+def test_copy_docstring_success():
+ def func(): # pragma: NO COVER
+ pass
+
+ _helpers.copy_docstring(SourceClass)(func)
+
+ assert func.__doc__ == SourceClass.func.__doc__
+
+
+def test_copy_docstring_conflict():
+ def func(): # pragma: NO COVER
+ """existing docstring"""
+ pass
+
+ with pytest.raises(ValueError):
+ _helpers.copy_docstring(SourceClass)(func)
+
+
+def test_copy_docstring_non_existing():
+ def func2(): # pragma: NO COVER
+ pass
+
+ with pytest.raises(AttributeError):
+ _helpers.copy_docstring(SourceClass)(func2)
+
+
+def test_utcnow():
+ assert isinstance(_helpers.utcnow(), datetime.datetime)
+
+
+def test_datetime_to_secs():
+ assert _helpers.datetime_to_secs(datetime.datetime(1970, 1, 1)) == 0
+ assert _helpers.datetime_to_secs(datetime.datetime(1990, 5, 29)) == 643939200
+
+
+def test_to_bytes_with_bytes():
+ value = b"bytes-val"
+ assert _helpers.to_bytes(value) == value
+
+
+def test_to_bytes_with_unicode():
+ value = u"string-val"
+ encoded_value = b"string-val"
+ assert _helpers.to_bytes(value) == encoded_value
+
+
+def test_to_bytes_with_nonstring_type():
+ with pytest.raises(ValueError):
+ _helpers.to_bytes(object())
+
+
+def test_from_bytes_with_unicode():
+ value = u"bytes-val"
+ assert _helpers.from_bytes(value) == value
+
+
+def test_from_bytes_with_bytes():
+ value = b"string-val"
+ decoded_value = u"string-val"
+ assert _helpers.from_bytes(value) == decoded_value
+
+
+def test_from_bytes_with_nonstring_type():
+ with pytest.raises(ValueError):
+ _helpers.from_bytes(object())
+
+
+def _assert_query(url, expected):
+ parts = urllib.parse.urlsplit(url)
+ query = urllib.parse.parse_qs(parts.query)
+ assert query == expected
+
+
+def test_update_query_params_no_params():
+ uri = "http://www.google.com"
+ updated = _helpers.update_query(uri, {"a": "b"})
+ assert updated == uri + "?a=b"
+
+
+def test_update_query_existing_params():
+ uri = "http://www.google.com?x=y"
+ updated = _helpers.update_query(uri, {"a": "b", "c": "d&"})
+ _assert_query(updated, {"x": ["y"], "a": ["b"], "c": ["d&"]})
+
+
+def test_update_query_replace_param():
+ base_uri = "http://www.google.com"
+ uri = base_uri + "?x=a"
+ updated = _helpers.update_query(uri, {"x": "b", "y": "c"})
+ _assert_query(updated, {"x": ["b"], "y": ["c"]})
+
+
+def test_update_query_remove_param():
+ base_uri = "http://www.google.com"
+ uri = base_uri + "?x=a"
+ updated = _helpers.update_query(uri, {"y": "c"}, remove=["x"])
+ _assert_query(updated, {"y": ["c"]})
+
+
+def test_scopes_to_string():
+ cases = [
+ ("", ()),
+ ("", []),
+ ("", ("",)),
+ ("", [""]),
+ ("a", ("a",)),
+ ("b", ["b"]),
+ ("a b", ["a", "b"]),
+ ("a b", ("a", "b")),
+ ("a b", (s for s in ["a", "b"])),
+ ]
+ for expected, case in cases:
+ assert _helpers.scopes_to_string(case) == expected
+
+
+def test_string_to_scopes():
+ cases = [("", []), ("a", ["a"]), ("a b c d e f", ["a", "b", "c", "d", "e", "f"])]
+
+ for case, expected in cases:
+ assert _helpers.string_to_scopes(case) == expected
+
+
+def test_padded_urlsafe_b64decode():
+ cases = [
+ ("YQ==", b"a"),
+ ("YQ", b"a"),
+ ("YWE=", b"aa"),
+ ("YWE", b"aa"),
+ ("YWFhYQ==", b"aaaa"),
+ ("YWFhYQ", b"aaaa"),
+ ("YWFhYWE=", b"aaaaa"),
+ ("YWFhYWE", b"aaaaa"),
+ ]
+
+ for case, expected in cases:
+ assert _helpers.padded_urlsafe_b64decode(case) == expected
+
+
+def test_unpadded_urlsafe_b64encode():
+ cases = [(b"", b""), (b"a", b"YQ"), (b"aa", b"YWE"), (b"aaa", b"YWFh")]
+
+ for case, expected in cases:
+ assert _helpers.unpadded_urlsafe_b64encode(case) == expected
diff --git a/contrib/python/google-auth/py3/tests/test__oauth2client.py b/contrib/python/google-auth/py3/tests/test__oauth2client.py
new file mode 100644
index 0000000000..72db6535bc
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test__oauth2client.py
@@ -0,0 +1,178 @@
+# Copyright 2016 Google LLC
+#
+# 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 datetime
+import importlib
+import os
+import sys
+
+import mock
+import pytest # type: ignore
+
+try:
+ import oauth2client.client # type: ignore
+ import oauth2client.contrib.gce # type: ignore
+ import oauth2client.service_account # type: ignore
+except ImportError: # pragma: NO COVER
+ pytest.skip(
+ "Skipping oauth2client tests since oauth2client is not installed.",
+ allow_module_level=True,
+ )
+
+from google.auth import _oauth2client
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+
+def test__convert_oauth2_credentials():
+ old_credentials = oauth2client.client.OAuth2Credentials(
+ "access_token",
+ "client_id",
+ "client_secret",
+ "refresh_token",
+ datetime.datetime.min,
+ "token_uri",
+ "user_agent",
+ scopes="one two",
+ )
+
+ new_credentials = _oauth2client._convert_oauth2_credentials(old_credentials)
+
+ assert new_credentials.token == old_credentials.access_token
+ assert new_credentials._refresh_token == old_credentials.refresh_token
+ assert new_credentials._client_id == old_credentials.client_id
+ assert new_credentials._client_secret == old_credentials.client_secret
+ assert new_credentials._token_uri == old_credentials.token_uri
+ assert new_credentials.scopes == old_credentials.scopes
+
+
+def test__convert_service_account_credentials():
+ old_class = oauth2client.service_account.ServiceAccountCredentials
+ old_credentials = old_class.from_json_keyfile_name(SERVICE_ACCOUNT_JSON_FILE)
+
+ new_credentials = _oauth2client._convert_service_account_credentials(
+ old_credentials
+ )
+
+ assert (
+ new_credentials.service_account_email == old_credentials.service_account_email
+ )
+ assert new_credentials._signer.key_id == old_credentials._private_key_id
+ assert new_credentials._token_uri == old_credentials.token_uri
+
+
+def test__convert_service_account_credentials_with_jwt():
+ old_class = oauth2client.service_account._JWTAccessCredentials
+ old_credentials = old_class.from_json_keyfile_name(SERVICE_ACCOUNT_JSON_FILE)
+
+ new_credentials = _oauth2client._convert_service_account_credentials(
+ old_credentials
+ )
+
+ assert (
+ new_credentials.service_account_email == old_credentials.service_account_email
+ )
+ assert new_credentials._signer.key_id == old_credentials._private_key_id
+ assert new_credentials._token_uri == old_credentials.token_uri
+
+
+def test__convert_gce_app_assertion_credentials():
+ old_credentials = oauth2client.contrib.gce.AppAssertionCredentials(
+ email="some_email"
+ )
+
+ new_credentials = _oauth2client._convert_gce_app_assertion_credentials(
+ old_credentials
+ )
+
+ assert (
+ new_credentials.service_account_email == old_credentials.service_account_email
+ )
+
+
+@pytest.fixture
+def mock_oauth2client_gae_imports(mock_non_existent_module):
+ mock_non_existent_module("google.appengine.api.app_identity")
+ mock_non_existent_module("google.appengine.ext.ndb")
+ mock_non_existent_module("google.appengine.ext.webapp.util")
+ mock_non_existent_module("webapp2")
+
+
+@mock.patch("google.auth.app_engine.app_identity")
+def _test__convert_appengine_app_assertion_credentials(
+ app_identity, mock_oauth2client_gae_imports
+):
+
+ import oauth2client.contrib.appengine # type: ignore
+
+ service_account_id = "service_account_id"
+ old_credentials = oauth2client.contrib.appengine.AppAssertionCredentials(
+ scope="one two", service_account_id=service_account_id
+ )
+
+ new_credentials = _oauth2client._convert_appengine_app_assertion_credentials(
+ old_credentials
+ )
+
+ assert new_credentials.scopes == ["one", "two"]
+ assert new_credentials._service_account_id == old_credentials.service_account_id
+
+
+class FakeCredentials(object):
+ pass
+
+
+def test_convert_success():
+ convert_function = mock.Mock(spec=["__call__"])
+ conversion_map_patch = mock.patch.object(
+ _oauth2client, "_CLASS_CONVERSION_MAP", {FakeCredentials: convert_function}
+ )
+ credentials = FakeCredentials()
+
+ with conversion_map_patch:
+ result = _oauth2client.convert(credentials)
+
+ convert_function.assert_called_once_with(credentials)
+ assert result == convert_function.return_value
+
+
+def test_convert_not_found():
+ with pytest.raises(ValueError) as excinfo:
+ _oauth2client.convert("a string is not a real credentials class")
+
+ assert excinfo.match("Unable to convert")
+
+
+@pytest.fixture
+def reset__oauth2client_module():
+ """Reloads the _oauth2client module after a test."""
+ importlib.reload(_oauth2client)
+
+
+def _test_import_has_app_engine(
+ mock_oauth2client_gae_imports, reset__oauth2client_module
+):
+ importlib.reload(_oauth2client)
+ assert _oauth2client._HAS_APPENGINE
+
+
+def test_import_without_oauth2client(monkeypatch, reset__oauth2client_module):
+ monkeypatch.setitem(sys.modules, "oauth2client", None)
+ with pytest.raises(ImportError) as excinfo:
+ importlib.reload(_oauth2client)
+
+ assert excinfo.match("oauth2client")
diff --git a/contrib/python/google-auth/py3/tests/test__service_account_info.py b/contrib/python/google-auth/py3/tests/test__service_account_info.py
new file mode 100644
index 0000000000..db8106081c
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test__service_account_info.py
@@ -0,0 +1,83 @@
+# Copyright 2016 Google LLC
+#
+# 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 json
+import os
+
+import pytest # type: ignore
+
+from google.auth import _service_account_info
+from google.auth import crypt
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+GDCH_SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "gdch_service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+ SERVICE_ACCOUNT_INFO = json.load(fh)
+
+with open(GDCH_SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+ GDCH_SERVICE_ACCOUNT_INFO = json.load(fh)
+
+
+def test_from_dict():
+ signer = _service_account_info.from_dict(SERVICE_ACCOUNT_INFO)
+ assert isinstance(signer, crypt.RSASigner)
+ assert signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"]
+
+
+def test_from_dict_es256_signer():
+ signer = _service_account_info.from_dict(
+ GDCH_SERVICE_ACCOUNT_INFO, use_rsa_signer=False
+ )
+ assert isinstance(signer, crypt.ES256Signer)
+ assert signer.key_id == GDCH_SERVICE_ACCOUNT_INFO["private_key_id"]
+
+
+def test_from_dict_bad_private_key():
+ info = SERVICE_ACCOUNT_INFO.copy()
+ info["private_key"] = "garbage"
+
+ with pytest.raises(ValueError) as excinfo:
+ _service_account_info.from_dict(info)
+
+ assert excinfo.match(r"key")
+
+
+def test_from_dict_bad_format():
+ with pytest.raises(ValueError) as excinfo:
+ _service_account_info.from_dict({}, require=("meep",))
+
+ assert excinfo.match(r"missing fields")
+
+
+def test_from_filename():
+ info, signer = _service_account_info.from_filename(SERVICE_ACCOUNT_JSON_FILE)
+
+ for key, value in SERVICE_ACCOUNT_INFO.items():
+ assert info[key] == value
+
+ assert isinstance(signer, crypt.RSASigner)
+ assert signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"]
+
+
+def test_from_filename_es256_signer():
+ _, signer = _service_account_info.from_filename(
+ GDCH_SERVICE_ACCOUNT_JSON_FILE, use_rsa_signer=False
+ )
+
+ assert isinstance(signer, crypt.ES256Signer)
+ assert signer.key_id == GDCH_SERVICE_ACCOUNT_INFO["private_key_id"]
diff --git a/contrib/python/google-auth/py3/tests/test_api_key.py b/contrib/python/google-auth/py3/tests/test_api_key.py
new file mode 100644
index 0000000000..9ba7b1426b
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_api_key.py
@@ -0,0 +1,45 @@
+# Copyright 2022 Google LLC
+#
+# 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 pytest # type: ignore
+
+from google.auth import api_key
+
+
+def test_credentials_constructor():
+ with pytest.raises(ValueError) as excinfo:
+ api_key.Credentials("")
+
+ assert excinfo.match(r"Token must be a non-empty API key string")
+
+
+def test_expired_and_valid():
+ credentials = api_key.Credentials("api-key")
+
+ assert credentials.valid
+ assert credentials.token == "api-key"
+ assert not credentials.expired
+
+ credentials.refresh(None)
+ assert credentials.valid
+ assert credentials.token == "api-key"
+ assert not credentials.expired
+
+
+def test_before_request():
+ credentials = api_key.Credentials("api-key")
+ headers = {}
+
+ credentials.before_request(None, "http://example.com", "GET", headers)
+ assert headers["x-goog-api-key"] == "api-key"
diff --git a/contrib/python/google-auth/py3/tests/test_app_engine.py b/contrib/python/google-auth/py3/tests/test_app_engine.py
new file mode 100644
index 0000000000..ca085bd698
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_app_engine.py
@@ -0,0 +1,217 @@
+# Copyright 2016 Google LLC
+#
+# 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 datetime
+
+import mock
+import pytest # type: ignore
+
+from google.auth import app_engine
+
+
+class _AppIdentityModule(object):
+ """The interface of the App Idenity app engine module.
+ See https://cloud.google.com/appengine/docs/standard/python/refdocs
+ /google.appengine.api.app_identity.app_identity
+ """
+
+ def get_application_id(self):
+ raise NotImplementedError()
+
+ def sign_blob(self, bytes_to_sign, deadline=None):
+ raise NotImplementedError()
+
+ def get_service_account_name(self, deadline=None):
+ raise NotImplementedError()
+
+ def get_access_token(self, scopes, service_account_id=None):
+ raise NotImplementedError()
+
+
+@pytest.fixture
+def app_identity(monkeypatch):
+ """Mocks the app_identity module for google.auth.app_engine."""
+ app_identity_module = mock.create_autospec(_AppIdentityModule, instance=True)
+ monkeypatch.setattr(app_engine, "app_identity", app_identity_module)
+ yield app_identity_module
+
+
+def test_get_project_id(app_identity):
+ app_identity.get_application_id.return_value = mock.sentinel.project
+ assert app_engine.get_project_id() == mock.sentinel.project
+
+
+@mock.patch.object(app_engine, "app_identity", new=None)
+def test_get_project_id_missing_apis():
+ with pytest.raises(EnvironmentError) as excinfo:
+ assert app_engine.get_project_id()
+
+ assert excinfo.match(r"App Engine APIs are not available")
+
+
+class TestSigner(object):
+ def test_key_id(self, app_identity):
+ app_identity.sign_blob.return_value = (
+ mock.sentinel.key_id,
+ mock.sentinel.signature,
+ )
+
+ signer = app_engine.Signer()
+
+ assert signer.key_id is None
+
+ def test_sign(self, app_identity):
+ app_identity.sign_blob.return_value = (
+ mock.sentinel.key_id,
+ mock.sentinel.signature,
+ )
+
+ signer = app_engine.Signer()
+ to_sign = b"123"
+
+ signature = signer.sign(to_sign)
+
+ assert signature == mock.sentinel.signature
+ app_identity.sign_blob.assert_called_with(to_sign)
+
+
+class TestCredentials(object):
+ @mock.patch.object(app_engine, "app_identity", new=None)
+ def test_missing_apis(self):
+ with pytest.raises(EnvironmentError) as excinfo:
+ app_engine.Credentials()
+
+ assert excinfo.match(r"App Engine APIs are not available")
+
+ def test_default_state(self, app_identity):
+ credentials = app_engine.Credentials()
+
+ # Not token acquired yet
+ assert not credentials.valid
+ # Expiration hasn't been set yet
+ assert not credentials.expired
+ # Scopes are required
+ assert not credentials.scopes
+ assert not credentials.default_scopes
+ assert credentials.requires_scopes
+ assert not credentials.quota_project_id
+
+ def test_with_scopes(self, app_identity):
+ credentials = app_engine.Credentials()
+
+ assert not credentials.scopes
+ assert credentials.requires_scopes
+
+ scoped_credentials = credentials.with_scopes(["email"])
+
+ assert scoped_credentials.has_scopes(["email"])
+ assert not scoped_credentials.requires_scopes
+
+ def test_with_default_scopes(self, app_identity):
+ credentials = app_engine.Credentials()
+
+ assert not credentials.scopes
+ assert not credentials.default_scopes
+ assert credentials.requires_scopes
+
+ scoped_credentials = credentials.with_scopes(
+ scopes=None, default_scopes=["email"]
+ )
+
+ assert scoped_credentials.has_scopes(["email"])
+ assert not scoped_credentials.requires_scopes
+
+ def test_with_quota_project(self, app_identity):
+ credentials = app_engine.Credentials()
+
+ assert not credentials.scopes
+ assert not credentials.quota_project_id
+
+ quota_project_creds = credentials.with_quota_project("project-foo")
+
+ assert quota_project_creds.quota_project_id == "project-foo"
+
+ def test_service_account_email_implicit(self, app_identity):
+ app_identity.get_service_account_name.return_value = (
+ mock.sentinel.service_account_email
+ )
+ credentials = app_engine.Credentials()
+
+ assert credentials.service_account_email == mock.sentinel.service_account_email
+ assert app_identity.get_service_account_name.called
+
+ def test_service_account_email_explicit(self, app_identity):
+ credentials = app_engine.Credentials(
+ service_account_id=mock.sentinel.service_account_email
+ )
+
+ assert credentials.service_account_email == mock.sentinel.service_account_email
+ assert not app_identity.get_service_account_name.called
+
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh(self, utcnow, app_identity):
+ token = "token"
+ ttl = 643942923
+ app_identity.get_access_token.return_value = token, ttl
+ credentials = app_engine.Credentials(
+ scopes=["email"], default_scopes=["profile"]
+ )
+
+ credentials.refresh(None)
+
+ app_identity.get_access_token.assert_called_with(
+ credentials.scopes, credentials._service_account_id
+ )
+ assert credentials.token == token
+ assert credentials.expiry == datetime.datetime(1990, 5, 29, 1, 2, 3)
+ assert credentials.valid
+ assert not credentials.expired
+
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh_with_default_scopes(self, utcnow, app_identity):
+ token = "token"
+ ttl = 643942923
+ app_identity.get_access_token.return_value = token, ttl
+ credentials = app_engine.Credentials(default_scopes=["email"])
+
+ credentials.refresh(None)
+
+ app_identity.get_access_token.assert_called_with(
+ credentials.default_scopes, credentials._service_account_id
+ )
+ assert credentials.token == token
+ assert credentials.expiry == datetime.datetime(1990, 5, 29, 1, 2, 3)
+ assert credentials.valid
+ assert not credentials.expired
+
+ def test_sign_bytes(self, app_identity):
+ app_identity.sign_blob.return_value = (
+ mock.sentinel.key_id,
+ mock.sentinel.signature,
+ )
+ credentials = app_engine.Credentials()
+ to_sign = b"123"
+
+ signature = credentials.sign_bytes(to_sign)
+
+ assert signature == mock.sentinel.signature
+ app_identity.sign_blob.assert_called_with(to_sign)
+
+ def test_signer(self, app_identity):
+ credentials = app_engine.Credentials()
+ assert isinstance(credentials.signer, app_engine.Signer)
+
+ def test_signer_email(self, app_identity):
+ credentials = app_engine.Credentials()
+ assert credentials.signer_email == credentials.service_account_email
diff --git a/contrib/python/google-auth/py3/tests/test_aws.py b/contrib/python/google-auth/py3/tests/test_aws.py
new file mode 100644
index 0000000000..39138ab12e
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_aws.py
@@ -0,0 +1,2125 @@
+# Copyright 2020 Google LLC
+#
+# 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 datetime
+import http.client as http_client
+import json
+import os
+import urllib.parse
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import aws
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+
+
+IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
+ "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
+)
+
+LANG_LIBRARY_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1"
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password".
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com"
+SERVICE_ACCOUNT_IMPERSONATION_URL_BASE = (
+ "https://us-east1-iamcredentials.googleapis.com"
+)
+SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE = "/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
+ SERVICE_ACCOUNT_EMAIL
+)
+SERVICE_ACCOUNT_IMPERSONATION_URL = (
+ SERVICE_ACCOUNT_IMPERSONATION_URL_BASE + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
+)
+QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID"
+SCOPES = ["scope1", "scope2"]
+TOKEN_URL = "https://sts.googleapis.com/v1/token"
+TOKEN_INFO_URL = "https://sts.googleapis.com/v1/introspect"
+SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request"
+AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
+REGION_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone"
+IMDSV2_SESSION_TOKEN_URL = "http://169.254.169.254/latest/api/token"
+SECURITY_CREDS_URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials"
+REGION_URL_IPV6 = "http://[fd00:ec2::254]/latest/meta-data/placement/availability-zone"
+IMDSV2_SESSION_TOKEN_URL_IPV6 = "http://[fd00:ec2::254]/latest/api/token"
+SECURITY_CREDS_URL_IPV6 = (
+ "http://[fd00:ec2::254]/latest/meta-data/iam/security-credentials"
+)
+CRED_VERIFICATION_URL = (
+ "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
+)
+# Sample fictitious AWS security credentials to be used with tests that require a session token.
+ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
+SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
+TOKEN = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE"
+# To avoid json.dumps() differing behavior from one version to other,
+# the JSON payload is hardcoded.
+REQUEST_PARAMS = '{"KeySchema":[{"KeyType":"HASH","AttributeName":"Id"}],"TableName":"TestTable","AttributeDefinitions":[{"AttributeName":"Id","AttributeType":"S"}],"ProvisionedThroughput":{"WriteCapacityUnits":5,"ReadCapacityUnits":5}}'
+# Each tuple contains the following entries:
+# region, time, credentials, original_request, signed_request
+
+DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
+VALID_TOKEN_URLS = [
+ "https://sts.googleapis.com",
+ "https://us-east-1.sts.googleapis.com",
+ "https://US-EAST-1.sts.googleapis.com",
+ "https://sts.us-east-1.googleapis.com",
+ "https://sts.US-WEST-1.googleapis.com",
+ "https://us-east-1-sts.googleapis.com",
+ "https://US-WEST-1-sts.googleapis.com",
+ "https://us-west-1-sts.googleapis.com/path?query",
+ "https://sts-us-east-1.p.googleapis.com",
+]
+INVALID_TOKEN_URLS = [
+ "https://iamcredentials.googleapis.com",
+ "sts.googleapis.com",
+ "https://",
+ "http://sts.googleapis.com",
+ "https://st.s.googleapis.com",
+ "https://us-eas\t-1.sts.googleapis.com",
+ "https:/us-east-1.sts.googleapis.com",
+ "https://US-WE/ST-1-sts.googleapis.com",
+ "https://sts-us-east-1.googleapis.com",
+ "https://sts-US-WEST-1.googleapis.com",
+ "testhttps://us-east-1.sts.googleapis.com",
+ "https://us-east-1.sts.googleapis.comevil.com",
+ "https://us-east-1.us-east-1.sts.googleapis.com",
+ "https://us-ea.s.t.sts.googleapis.com",
+ "https://sts.googleapis.comevil.com",
+ "hhttps://us-east-1.sts.googleapis.com",
+ "https://us- -1.sts.googleapis.com",
+ "https://-sts.googleapis.com",
+ "https://us-east-1.sts.googleapis.com.evil.com",
+ "https://sts.pgoogleapis.com",
+ "https://p.googleapis.com",
+ "https://sts.p.com",
+ "http://sts.p.googleapis.com",
+ "https://xyz-sts.p.googleapis.com",
+ "https://sts-xyz.123.p.googleapis.com",
+ "https://sts-xyz.p1.googleapis.com",
+ "https://sts-xyz.p.foo.com",
+ "https://sts-xyz.p.foo.googleapis.com",
+]
+VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [
+ "https://iamcredentials.googleapis.com",
+ "https://us-east-1.iamcredentials.googleapis.com",
+ "https://US-EAST-1.iamcredentials.googleapis.com",
+ "https://iamcredentials.us-east-1.googleapis.com",
+ "https://iamcredentials.US-WEST-1.googleapis.com",
+ "https://us-east-1-iamcredentials.googleapis.com",
+ "https://US-WEST-1-iamcredentials.googleapis.com",
+ "https://us-west-1-iamcredentials.googleapis.com/path?query",
+ "https://iamcredentials-us-east-1.p.googleapis.com",
+]
+INVALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [
+ "https://sts.googleapis.com",
+ "iamcredentials.googleapis.com",
+ "https://",
+ "http://iamcredentials.googleapis.com",
+ "https://iamcre.dentials.googleapis.com",
+ "https://us-eas\t-1.iamcredentials.googleapis.com",
+ "https:/us-east-1.iamcredentials.googleapis.com",
+ "https://US-WE/ST-1-iamcredentials.googleapis.com",
+ "https://iamcredentials-us-east-1.googleapis.com",
+ "https://iamcredentials-US-WEST-1.googleapis.com",
+ "testhttps://us-east-1.iamcredentials.googleapis.com",
+ "https://us-east-1.iamcredentials.googleapis.comevil.com",
+ "https://us-east-1.us-east-1.iamcredentials.googleapis.com",
+ "https://us-ea.s.t.iamcredentials.googleapis.com",
+ "https://iamcredentials.googleapis.comevil.com",
+ "hhttps://us-east-1.iamcredentials.googleapis.com",
+ "https://us- -1.iamcredentials.googleapis.com",
+ "https://-iamcredentials.googleapis.com",
+ "https://us-east-1.iamcredentials.googleapis.com.evil.com",
+ "https://iamcredentials.pgoogleapis.com",
+ "https://p.googleapis.com",
+ "https://iamcredentials.p.com",
+ "http://iamcredentials.p.googleapis.com",
+ "https://xyz-iamcredentials.p.googleapis.com",
+ "https://iamcredentials-xyz.123.p.googleapis.com",
+ "https://iamcredentials-xyz.p1.googleapis.com",
+ "https://iamcredentials-xyz.p.foo.com",
+ "https://iamcredentials-xyz.p.foo.googleapis.com",
+]
+TEST_FIXTURES = [
+ # GET request (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "GET",
+ "url": "https://host.foo.com",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ },
+ {
+ "url": "https://host.foo.com",
+ "method": "GET",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ },
+ ),
+ # GET request with relative path (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "GET",
+ "url": "https://host.foo.com/foo/bar/../..",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ },
+ {
+ "url": "https://host.foo.com/foo/bar/../..",
+ "method": "GET",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ },
+ ),
+ # GET request with /./ path (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "GET",
+ "url": "https://host.foo.com/./",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ },
+ {
+ "url": "https://host.foo.com/./",
+ "method": "GET",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ },
+ ),
+ # GET request with pointless dot path (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "GET",
+ "url": "https://host.foo.com/./foo",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ },
+ {
+ "url": "https://host.foo.com/./foo",
+ "method": "GET",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=910e4d6c9abafaf87898e1eb4c929135782ea25bb0279703146455745391e63a",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ },
+ ),
+ # GET request with utf8 path (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "GET",
+ "url": "https://host.foo.com/%E1%88%B4",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ },
+ {
+ "url": "https://host.foo.com/%E1%88%B4",
+ "method": "GET",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=8d6634c189aa8c75c2e51e106b6b5121bed103fdb351f7d7d4381c738823af74",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ },
+ ),
+ # GET request with duplicate query key (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "GET",
+ "url": "https://host.foo.com/?foo=Zoo&foo=aha",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ },
+ {
+ "url": "https://host.foo.com/?foo=Zoo&foo=aha",
+ "method": "GET",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=be7148d34ebccdc6423b19085378aa0bee970bdc61d144bd1a8c48c33079ab09",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ },
+ ),
+ # GET request with duplicate out of order query key (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "GET",
+ "url": "https://host.foo.com/?foo=b&foo=a",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ },
+ {
+ "url": "https://host.foo.com/?foo=b&foo=a",
+ "method": "GET",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=feb926e49e382bec75c9d7dcb2a1b6dc8aa50ca43c25d2bc51143768c0875acc",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ },
+ ),
+ # GET request with utf8 query (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-ut8-query.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-ut8-query.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "GET",
+ "url": "https://host.foo.com/?{}=bar".format(
+ urllib.parse.unquote("%E1%88%B4")
+ ),
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ },
+ {
+ "url": "https://host.foo.com/?{}=bar".format(
+ urllib.parse.unquote("%E1%88%B4")
+ ),
+ "method": "GET",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=6fb359e9a05394cc7074e0feb42573a2601abc0c869a953e8c5c12e4e01f1a8c",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ },
+ ),
+ # POST request with sorted headers (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "POST",
+ "url": "https://host.foo.com/",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "ZOO": "zoobar"},
+ },
+ {
+ "url": "https://host.foo.com/",
+ "method": "POST",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=b7a95a52518abbca0964a999a880429ab734f35ebbf1235bd79a5de87756dc4a",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ "ZOO": "zoobar",
+ },
+ },
+ ),
+ # POST request with upper case header value from AWS Python test harness.
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "POST",
+ "url": "https://host.foo.com/",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "zoo": "ZOOBAR"},
+ },
+ {
+ "url": "https://host.foo.com/",
+ "method": "POST",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=273313af9d0c265c531e11db70bbd653f3ba074c1009239e8559d3987039cad7",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ "zoo": "ZOOBAR",
+ },
+ },
+ ),
+ # POST request with header and no body (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "POST",
+ "url": "https://host.foo.com/",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "p": "phfft"},
+ },
+ {
+ "url": "https://host.foo.com/",
+ "method": "POST",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=debf546796015d6f6ded8626f5ce98597c33b47b9164cf6b17b4642036fcb592",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ "p": "phfft",
+ },
+ },
+ ),
+ # POST request with body and no header (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "POST",
+ "url": "https://host.foo.com/",
+ "headers": {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ "data": "foo=bar",
+ },
+ {
+ "url": "https://host.foo.com/",
+ "method": "POST",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=content-type;date;host, Signature=5a15b22cf462f047318703b92e6f4f38884e4a7ab7b1d6426ca46a8bd1c26cbc",
+ "host": "host.foo.com",
+ "Content-Type": "application/x-www-form-urlencoded",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ "data": "foo=bar",
+ },
+ ),
+ # POST request with querystring (AWS botocore tests).
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.req
+ # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.sreq
+ (
+ "us-east-1",
+ "2011-09-09T23:36:00Z",
+ {
+ "access_key_id": "AKIDEXAMPLE",
+ "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+ },
+ {
+ "method": "POST",
+ "url": "https://host.foo.com/?foo=bar",
+ "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+ },
+ {
+ "url": "https://host.foo.com/?foo=bar",
+ "method": "POST",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b6e3b79003ce0743a491606ba1035a804593b0efb1e20a11cba83f8c25a57a92",
+ "host": "host.foo.com",
+ "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+ },
+ },
+ ),
+ # GET request with session token credentials.
+ (
+ "us-east-2",
+ "2020-08-11T06:55:22Z",
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ },
+ {
+ "method": "GET",
+ "url": "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15",
+ },
+ {
+ "url": "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15",
+ "method": "GET",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential="
+ + ACCESS_KEY_ID
+ + "/20200811/us-east-2/ec2/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=41e226f997bf917ec6c9b2b14218df0874225f13bb153236c247881e614fafc9",
+ "host": "ec2.us-east-2.amazonaws.com",
+ "x-amz-date": "20200811T065522Z",
+ "x-amz-security-token": TOKEN,
+ },
+ },
+ ),
+ # POST request with session token credentials.
+ (
+ "us-east-2",
+ "2020-08-11T06:55:22Z",
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ },
+ {
+ "method": "POST",
+ "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+ },
+ {
+ "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+ "method": "POST",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential="
+ + ACCESS_KEY_ID
+ + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=596aa990b792d763465d73703e684ca273c45536c6d322c31be01a41d02e5b60",
+ "host": "sts.us-east-2.amazonaws.com",
+ "x-amz-date": "20200811T065522Z",
+ "x-amz-security-token": TOKEN,
+ },
+ },
+ ),
+ # POST request with computed x-amz-date and no data.
+ (
+ "us-east-2",
+ "2020-08-11T06:55:22Z",
+ {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY},
+ {
+ "method": "POST",
+ "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+ },
+ {
+ "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+ "method": "POST",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential="
+ + ACCESS_KEY_ID
+ + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date, Signature=9e722e5b7bfa163447e2a14df118b45ebd283c5aea72019bdf921d6e7dc01a9a",
+ "host": "sts.us-east-2.amazonaws.com",
+ "x-amz-date": "20200811T065522Z",
+ },
+ },
+ ),
+ # POST request with session token and additional headers/data.
+ (
+ "us-east-2",
+ "2020-08-11T06:55:22Z",
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ },
+ {
+ "method": "POST",
+ "url": "https://dynamodb.us-east-2.amazonaws.com/",
+ "headers": {
+ "Content-Type": "application/x-amz-json-1.0",
+ "x-amz-target": "DynamoDB_20120810.CreateTable",
+ },
+ "data": REQUEST_PARAMS,
+ },
+ {
+ "url": "https://dynamodb.us-east-2.amazonaws.com/",
+ "method": "POST",
+ "headers": {
+ "Authorization": "AWS4-HMAC-SHA256 Credential="
+ + ACCESS_KEY_ID
+ + "/20200811/us-east-2/dynamodb/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=eb8bce0e63654bba672d4a8acb07e72d69210c1797d56ce024dbbc31beb2a2c7",
+ "host": "dynamodb.us-east-2.amazonaws.com",
+ "x-amz-date": "20200811T065522Z",
+ "Content-Type": "application/x-amz-json-1.0",
+ "x-amz-target": "DynamoDB_20120810.CreateTable",
+ "x-amz-security-token": TOKEN,
+ },
+ "data": REQUEST_PARAMS,
+ },
+ ),
+]
+
+
+class TestRequestSigner(object):
+ @pytest.mark.parametrize(
+ "region, time, credentials, original_request, signed_request", TEST_FIXTURES
+ )
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_get_request_options(
+ self, utcnow, region, time, credentials, original_request, signed_request
+ ):
+ utcnow.return_value = datetime.datetime.strptime(time, "%Y-%m-%dT%H:%M:%SZ")
+ request_signer = aws.RequestSigner(region)
+ actual_signed_request = request_signer.get_request_options(
+ credentials,
+ original_request.get("url"),
+ original_request.get("method"),
+ original_request.get("data"),
+ original_request.get("headers"),
+ )
+
+ assert actual_signed_request == signed_request
+
+ def test_get_request_options_with_missing_scheme_url(self):
+ request_signer = aws.RequestSigner("us-east-2")
+
+ with pytest.raises(ValueError) as excinfo:
+ request_signer.get_request_options(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ },
+ "invalid",
+ "POST",
+ )
+
+ assert excinfo.match(r"Invalid AWS service URL")
+
+ def test_get_request_options_with_invalid_scheme_url(self):
+ request_signer = aws.RequestSigner("us-east-2")
+
+ with pytest.raises(ValueError) as excinfo:
+ request_signer.get_request_options(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ },
+ "http://invalid",
+ "POST",
+ )
+
+ assert excinfo.match(r"Invalid AWS service URL")
+
+ def test_get_request_options_with_missing_hostname_url(self):
+ request_signer = aws.RequestSigner("us-east-2")
+
+ with pytest.raises(ValueError) as excinfo:
+ request_signer.get_request_options(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ },
+ "https://",
+ "POST",
+ )
+
+ assert excinfo.match(r"Invalid AWS service URL")
+
+
+class TestCredentials(object):
+ AWS_REGION = "us-east-2"
+ AWS_ROLE = "gcp-aws-role"
+ AWS_SECURITY_CREDENTIALS_RESPONSE = {
+ "AccessKeyId": ACCESS_KEY_ID,
+ "SecretAccessKey": SECRET_ACCESS_KEY,
+ "Token": TOKEN,
+ }
+ AWS_IMDSV2_SESSION_TOKEN = "awsimdsv2sessiontoken"
+ AWS_SIGNATURE_TIME = "2020-08-11T06:55:22Z"
+ CREDENTIAL_SOURCE = {
+ "environment_id": "aws1",
+ "region_url": REGION_URL,
+ "url": SECURITY_CREDS_URL,
+ "regional_cred_verification_url": CRED_VERIFICATION_URL,
+ }
+ CREDENTIAL_SOURCE_IPV6 = {
+ "environment_id": "aws1",
+ "region_url": REGION_URL_IPV6,
+ "url": SECURITY_CREDS_URL_IPV6,
+ "regional_cred_verification_url": CRED_VERIFICATION_URL,
+ "imdsv2_session_token_url": IMDSV2_SESSION_TOKEN_URL_IPV6,
+ }
+ SUCCESS_RESPONSE = {
+ "access_token": "ACCESS_TOKEN",
+ "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": " ".join(SCOPES),
+ }
+
+ @classmethod
+ def make_serialized_aws_signed_request(
+ cls,
+ aws_security_credentials,
+ region_name="us-east-2",
+ url="https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+ ):
+ """Utility to generate serialize AWS signed requests.
+ This makes it easy to assert generated subject tokens based on the
+ provided AWS security credentials, regions and AWS STS endpoint.
+ """
+ request_signer = aws.RequestSigner(region_name)
+ signed_request = request_signer.get_request_options(
+ aws_security_credentials, url, "POST"
+ )
+ reformatted_signed_request = {
+ "url": signed_request.get("url"),
+ "method": signed_request.get("method"),
+ "headers": [
+ {
+ "key": "Authorization",
+ "value": signed_request.get("headers").get("Authorization"),
+ },
+ {"key": "host", "value": signed_request.get("headers").get("host")},
+ {
+ "key": "x-amz-date",
+ "value": signed_request.get("headers").get("x-amz-date"),
+ },
+ ],
+ }
+ # Include security token if available.
+ if "security_token" in aws_security_credentials:
+ reformatted_signed_request.get("headers").append(
+ {
+ "key": "x-amz-security-token",
+ "value": signed_request.get("headers").get("x-amz-security-token"),
+ }
+ )
+ # Append x-goog-cloud-target-resource header.
+ reformatted_signed_request.get("headers").append(
+ {"key": "x-goog-cloud-target-resource", "value": AUDIENCE}
+ ),
+ return urllib.parse.quote(
+ json.dumps(
+ reformatted_signed_request, separators=(",", ":"), sort_keys=True
+ )
+ )
+
+ @classmethod
+ def make_mock_request(
+ cls,
+ region_status=None,
+ region_name=None,
+ role_status=None,
+ role_name=None,
+ security_credentials_status=None,
+ security_credentials_data=None,
+ token_status=None,
+ token_data=None,
+ impersonation_status=None,
+ impersonation_data=None,
+ imdsv2_session_token_status=None,
+ imdsv2_session_token_data=None,
+ ):
+ """Utility function to generate a mock HTTP request object.
+ This will facilitate testing various edge cases by specify how the
+ various endpoints will respond while generating a Google Access token
+ in an AWS environment.
+ """
+ responses = []
+ if imdsv2_session_token_status:
+ # AWS session token request
+ imdsv2_session_response = mock.create_autospec(
+ transport.Response, instance=True
+ )
+ imdsv2_session_response.status = imdsv2_session_token_status
+ imdsv2_session_response.data = imdsv2_session_token_data
+ responses.append(imdsv2_session_response)
+
+ if region_status:
+ # AWS region request.
+ region_response = mock.create_autospec(transport.Response, instance=True)
+ region_response.status = region_status
+ if region_name:
+ region_response.data = "{}b".format(region_name).encode("utf-8")
+ responses.append(region_response)
+
+ if role_status:
+ # AWS role name request.
+ role_response = mock.create_autospec(transport.Response, instance=True)
+ role_response.status = role_status
+ if role_name:
+ role_response.data = role_name.encode("utf-8")
+ responses.append(role_response)
+
+ if security_credentials_status:
+ # AWS security credentials request.
+ security_credentials_response = mock.create_autospec(
+ transport.Response, instance=True
+ )
+ security_credentials_response.status = security_credentials_status
+ if security_credentials_data:
+ security_credentials_response.data = json.dumps(
+ security_credentials_data
+ ).encode("utf-8")
+ responses.append(security_credentials_response)
+
+ if token_status:
+ # GCP token exchange request.
+ token_response = mock.create_autospec(transport.Response, instance=True)
+ token_response.status = token_status
+ token_response.data = json.dumps(token_data).encode("utf-8")
+ responses.append(token_response)
+
+ if impersonation_status:
+ # Service account impersonation request.
+ impersonation_response = mock.create_autospec(
+ transport.Response, instance=True
+ )
+ impersonation_response.status = impersonation_status
+ impersonation_response.data = json.dumps(impersonation_data).encode("utf-8")
+ responses.append(impersonation_response)
+
+ request = mock.create_autospec(transport.Request)
+ request.side_effect = responses
+
+ return request
+
+ @classmethod
+ def make_credentials(
+ cls,
+ credential_source,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ client_id=None,
+ client_secret=None,
+ quota_project_id=None,
+ scopes=None,
+ default_scopes=None,
+ service_account_impersonation_url=None,
+ ):
+ return aws.Credentials(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=token_url,
+ token_info_url=token_info_url,
+ service_account_impersonation_url=service_account_impersonation_url,
+ credential_source=credential_source,
+ client_id=client_id,
+ client_secret=client_secret,
+ quota_project_id=quota_project_id,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ )
+
+ @classmethod
+ def assert_aws_metadata_request_kwargs(
+ cls, request_kwargs, url, headers=None, method="GET"
+ ):
+ assert request_kwargs["url"] == url
+ # All used AWS metadata server endpoints use GET HTTP method.
+ assert request_kwargs["method"] == method
+ if headers:
+ assert request_kwargs["headers"] == headers
+ else:
+ assert "headers" not in request_kwargs or request_kwargs["headers"] is None
+ # None of the endpoints used require any data in request.
+ assert "body" not in request_kwargs
+
+ @classmethod
+ def assert_token_request_kwargs(
+ cls, request_kwargs, headers, request_data, token_url=TOKEN_URL
+ ):
+ assert request_kwargs["url"] == token_url
+ assert request_kwargs["method"] == "POST"
+ assert request_kwargs["headers"] == headers
+ assert request_kwargs["body"] is not None
+ body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
+ assert len(body_tuples) == len(request_data.keys())
+ for (k, v) in body_tuples:
+ assert v.decode("utf-8") == request_data[k.decode("utf-8")]
+
+ @classmethod
+ def assert_impersonation_request_kwargs(
+ cls,
+ request_kwargs,
+ headers,
+ request_data,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ ):
+ assert request_kwargs["url"] == service_account_impersonation_url
+ assert request_kwargs["method"] == "POST"
+ assert request_kwargs["headers"] == headers
+ assert request_kwargs["body"] is not None
+ body_json = json.loads(request_kwargs["body"].decode("utf-8"))
+ assert body_json == request_data
+
+ @mock.patch.object(aws.Credentials, "__init__", return_value=None)
+ def test_from_info_full_options(self, mock_init):
+ credentials = aws.Credentials.from_info(
+ {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "service_account_impersonation": {"token_lifetime_seconds": 2800},
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ "quota_project_id": QUOTA_PROJECT_ID,
+ "credential_source": self.CREDENTIAL_SOURCE,
+ }
+ )
+
+ # Confirm aws.Credentials instance initialized with the expected parameters.
+ assert isinstance(credentials, aws.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ credential_source=self.CREDENTIAL_SOURCE,
+ quota_project_id=QUOTA_PROJECT_ID,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(aws.Credentials, "__init__", return_value=None)
+ def test_from_info_required_options_only(self, mock_init):
+ credentials = aws.Credentials.from_info(
+ {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE,
+ }
+ )
+
+ # Confirm aws.Credentials instance initialized with the expected parameters.
+ assert isinstance(credentials, aws.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=None,
+ service_account_impersonation_url=None,
+ service_account_impersonation_options={},
+ client_id=None,
+ client_secret=None,
+ credential_source=self.CREDENTIAL_SOURCE,
+ quota_project_id=None,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(aws.Credentials, "__init__", return_value=None)
+ def test_from_file_full_options(self, mock_init, tmpdir):
+ info = {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "service_account_impersonation": {"token_lifetime_seconds": 2800},
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ "quota_project_id": QUOTA_PROJECT_ID,
+ "credential_source": self.CREDENTIAL_SOURCE,
+ "universe_domain": DEFAULT_UNIVERSE_DOMAIN,
+ }
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(info))
+ credentials = aws.Credentials.from_file(str(config_file))
+
+ # Confirm aws.Credentials instance initialized with the expected parameters.
+ assert isinstance(credentials, aws.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ credential_source=self.CREDENTIAL_SOURCE,
+ quota_project_id=QUOTA_PROJECT_ID,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(aws.Credentials, "__init__", return_value=None)
+ def test_from_file_required_options_only(self, mock_init, tmpdir):
+ info = {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE,
+ }
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(info))
+ credentials = aws.Credentials.from_file(str(config_file))
+
+ # Confirm aws.Credentials instance initialized with the expected parameters.
+ assert isinstance(credentials, aws.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=None,
+ service_account_impersonation_url=None,
+ service_account_impersonation_options={},
+ client_id=None,
+ client_secret=None,
+ credential_source=self.CREDENTIAL_SOURCE,
+ quota_project_id=None,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ def test_constructor_invalid_credential_source(self):
+ # Provide invalid credential source.
+ credential_source = {"unsupported": "value"}
+
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(credential_source=credential_source)
+
+ assert excinfo.match(r"No valid AWS 'credential_source' provided")
+
+ def test_constructor_invalid_environment_id(self):
+ # Provide invalid environment_id.
+ credential_source = self.CREDENTIAL_SOURCE.copy()
+ credential_source["environment_id"] = "azure1"
+
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(credential_source=credential_source)
+
+ assert excinfo.match(r"No valid AWS 'credential_source' provided")
+
+ def test_constructor_missing_cred_verification_url(self):
+ # regional_cred_verification_url is a required field.
+ credential_source = self.CREDENTIAL_SOURCE.copy()
+ credential_source.pop("regional_cred_verification_url")
+
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(credential_source=credential_source)
+
+ assert excinfo.match(r"No valid AWS 'credential_source' provided")
+
+ def test_constructor_invalid_environment_id_version(self):
+ # Provide an unsupported version.
+ credential_source = self.CREDENTIAL_SOURCE.copy()
+ credential_source["environment_id"] = "aws3"
+
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(credential_source=credential_source)
+
+ assert excinfo.match(r"aws version '3' is not supported in the current build.")
+
+ def test_info(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE.copy()
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "credential_source": self.CREDENTIAL_SOURCE,
+ "universe_domain": DEFAULT_UNIVERSE_DOMAIN,
+ }
+
+ def test_token_info_url(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE.copy()
+ )
+
+ assert credentials.token_info_url == TOKEN_INFO_URL
+
+ def test_token_info_url_custom(self):
+ for url in VALID_TOKEN_URLS:
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE.copy(),
+ token_info_url=(url + "/introspect"),
+ )
+
+ assert credentials.token_info_url == (url + "/introspect")
+
+ def test_token_info_url_negative(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE.copy(), token_info_url=None
+ )
+
+ assert not credentials.token_info_url
+
+ def test_token_url_custom(self):
+ for url in VALID_TOKEN_URLS:
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE.copy(),
+ token_url=(url + "/token"),
+ )
+
+ assert credentials._token_url == (url + "/token")
+
+ def test_service_account_impersonation_url_custom(self):
+ for url in VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS:
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE.copy(),
+ service_account_impersonation_url=(
+ url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
+ ),
+ )
+
+ assert credentials._service_account_impersonation_url == (
+ url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
+ )
+
+ def test_retrieve_subject_token_missing_region_url(self):
+ # When AWS_REGION envvar is not available, region_url is required for
+ # determining the current AWS region.
+ credential_source = self.CREDENTIAL_SOURCE.copy()
+ credential_source.pop("region_url")
+ credentials = self.make_credentials(credential_source=credential_source)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(r"Unable to determine AWS region")
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_retrieve_subject_token_success_temp_creds_no_environment_vars(
+ self, utcnow
+ ):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ request = self.make_mock_request(
+ region_status=http_client.OK,
+ region_name=self.AWS_REGION,
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ )
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ subject_token = credentials.retrieve_subject_token(request)
+
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+ # Assert region request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[0][1], REGION_URL
+ )
+ # Assert role request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[1][1], SECURITY_CREDS_URL
+ )
+ # Assert security credentials request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[2][1],
+ "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
+ {"Content-Type": "application/json"},
+ )
+
+ # Retrieve subject_token again. Region should not be queried again.
+ new_request = self.make_mock_request(
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ )
+
+ credentials.retrieve_subject_token(new_request)
+
+ # Only 3 requests should be sent as the region is cached.
+ assert len(new_request.call_args_list) == 2
+ # Assert role request.
+ self.assert_aws_metadata_request_kwargs(
+ new_request.call_args_list[0][1], SECURITY_CREDS_URL
+ )
+ # Assert security credentials request.
+ self.assert_aws_metadata_request_kwargs(
+ new_request.call_args_list[1][1],
+ "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
+ {"Content-Type": "application/json"},
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ @mock.patch.dict(os.environ, {})
+ def test_retrieve_subject_token_success_temp_creds_no_environment_vars_idmsv2(
+ self, utcnow
+ ):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ request = self.make_mock_request(
+ region_status=http_client.OK,
+ region_name=self.AWS_REGION,
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ imdsv2_session_token_status=http_client.OK,
+ imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN,
+ )
+ credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
+ credential_source_token_url[
+ "imdsv2_session_token_url"
+ ] = IMDSV2_SESSION_TOKEN_URL
+ credentials = self.make_credentials(
+ credential_source=credential_source_token_url
+ )
+
+ subject_token = credentials.retrieve_subject_token(request)
+
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+ # Assert session token request
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[0][1],
+ IMDSV2_SESSION_TOKEN_URL,
+ {"X-aws-ec2-metadata-token-ttl-seconds": "300"},
+ "PUT",
+ )
+ # Assert region request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[1][1],
+ REGION_URL,
+ {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN},
+ )
+ # Assert role request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[2][1],
+ SECURITY_CREDS_URL,
+ {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN},
+ )
+ # Assert security credentials request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[3][1],
+ "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
+ {
+ "Content-Type": "application/json",
+ "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN,
+ },
+ )
+
+ # Retrieve subject_token again. Region should not be queried again.
+ new_request = self.make_mock_request(
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ imdsv2_session_token_status=http_client.OK,
+ imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN,
+ )
+
+ credentials.retrieve_subject_token(new_request)
+
+ # Only 3 requests should be sent as the region is cached.
+ assert len(new_request.call_args_list) == 3
+ # Assert session token request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[0][1],
+ IMDSV2_SESSION_TOKEN_URL,
+ {"X-aws-ec2-metadata-token-ttl-seconds": "300"},
+ "PUT",
+ )
+ # Assert role request.
+ self.assert_aws_metadata_request_kwargs(
+ new_request.call_args_list[1][1],
+ SECURITY_CREDS_URL,
+ {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN},
+ )
+ # Assert security credentials request.
+ self.assert_aws_metadata_request_kwargs(
+ new_request.call_args_list[2][1],
+ "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
+ {
+ "Content-Type": "application/json",
+ "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN,
+ },
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ @mock.patch.dict(
+ os.environ,
+ {
+ environment_vars.AWS_REGION: AWS_REGION,
+ environment_vars.AWS_ACCESS_KEY_ID: ACCESS_KEY_ID,
+ },
+ )
+ def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_secret_access_key_idmsv2(
+ self, utcnow
+ ):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ request = self.make_mock_request(
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ imdsv2_session_token_status=http_client.OK,
+ imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN,
+ )
+ credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
+ credential_source_token_url[
+ "imdsv2_session_token_url"
+ ] = IMDSV2_SESSION_TOKEN_URL
+ credentials = self.make_credentials(
+ credential_source=credential_source_token_url
+ )
+
+ subject_token = credentials.retrieve_subject_token(request)
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+ # Assert session token request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[0][1],
+ IMDSV2_SESSION_TOKEN_URL,
+ {"X-aws-ec2-metadata-token-ttl-seconds": "300"},
+ "PUT",
+ )
+ # Assert role request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[1][1],
+ SECURITY_CREDS_URL,
+ {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN},
+ )
+ # Assert security credentials request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[2][1],
+ "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
+ {
+ "Content-Type": "application/json",
+ "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN,
+ },
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ @mock.patch.dict(
+ os.environ,
+ {
+ environment_vars.AWS_REGION: AWS_REGION,
+ environment_vars.AWS_SECRET_ACCESS_KEY: SECRET_ACCESS_KEY,
+ },
+ )
+ def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_access_key_id_idmsv2(
+ self, utcnow
+ ):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ request = self.make_mock_request(
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ imdsv2_session_token_status=http_client.OK,
+ imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN,
+ )
+ credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
+ credential_source_token_url[
+ "imdsv2_session_token_url"
+ ] = IMDSV2_SESSION_TOKEN_URL
+ credentials = self.make_credentials(
+ credential_source=credential_source_token_url
+ )
+
+ subject_token = credentials.retrieve_subject_token(request)
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+ # Assert session token request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[0][1],
+ IMDSV2_SESSION_TOKEN_URL,
+ {"X-aws-ec2-metadata-token-ttl-seconds": "300"},
+ "PUT",
+ )
+ # Assert role request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[1][1],
+ SECURITY_CREDS_URL,
+ {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN},
+ )
+ # Assert security credentials request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[2][1],
+ "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
+ {
+ "Content-Type": "application/json",
+ "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN,
+ },
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ @mock.patch.dict(os.environ, {environment_vars.AWS_REGION: AWS_REGION})
+ def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_creds_idmsv2(
+ self, utcnow
+ ):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ request = self.make_mock_request(
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ imdsv2_session_token_status=http_client.OK,
+ imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN,
+ )
+ credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
+ credential_source_token_url[
+ "imdsv2_session_token_url"
+ ] = IMDSV2_SESSION_TOKEN_URL
+ credentials = self.make_credentials(
+ credential_source=credential_source_token_url
+ )
+
+ subject_token = credentials.retrieve_subject_token(request)
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+ # Assert session token request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[0][1],
+ IMDSV2_SESSION_TOKEN_URL,
+ {"X-aws-ec2-metadata-token-ttl-seconds": "300"},
+ "PUT",
+ )
+ # Assert role request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[1][1],
+ SECURITY_CREDS_URL,
+ {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN},
+ )
+ # Assert security credentials request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[2][1],
+ "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
+ {
+ "Content-Type": "application/json",
+ "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN,
+ },
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ @mock.patch.dict(
+ os.environ,
+ {
+ environment_vars.AWS_REGION: AWS_REGION,
+ environment_vars.AWS_ACCESS_KEY_ID: ACCESS_KEY_ID,
+ environment_vars.AWS_SECRET_ACCESS_KEY: SECRET_ACCESS_KEY,
+ },
+ )
+ def test_retrieve_subject_token_success_temp_creds_idmsv2(self, utcnow):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ request = self.make_mock_request(
+ role_status=http_client.OK, role_name=self.AWS_ROLE
+ )
+ credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
+ credential_source_token_url[
+ "imdsv2_session_token_url"
+ ] = IMDSV2_SESSION_TOKEN_URL
+ credentials = self.make_credentials(
+ credential_source=credential_source_token_url
+ )
+
+ credentials.retrieve_subject_token(request)
+ assert not request.called
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_retrieve_subject_token_success_ipv6(self, utcnow):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ request = self.make_mock_request(
+ region_status=http_client.OK,
+ region_name=self.AWS_REGION,
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ imdsv2_session_token_status=http_client.OK,
+ imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN,
+ )
+ credential_source_token_url = self.CREDENTIAL_SOURCE_IPV6.copy()
+ credentials = self.make_credentials(
+ credential_source=credential_source_token_url
+ )
+
+ subject_token = credentials.retrieve_subject_token(request)
+
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+ # Assert session token request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[0][1],
+ IMDSV2_SESSION_TOKEN_URL_IPV6,
+ {"X-aws-ec2-metadata-token-ttl-seconds": "300"},
+ "PUT",
+ )
+ # Assert region request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[1][1],
+ REGION_URL_IPV6,
+ {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN},
+ )
+ # Assert role request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[2][1],
+ SECURITY_CREDS_URL_IPV6,
+ {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN},
+ )
+ # Assert security credentials request.
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[3][1],
+ "{}/{}".format(SECURITY_CREDS_URL_IPV6, self.AWS_ROLE),
+ {
+ "Content-Type": "application/json",
+ "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN,
+ },
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_retrieve_subject_token_session_error_idmsv2(self, utcnow):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ request = self.make_mock_request(
+ imdsv2_session_token_status=http_client.UNAUTHORIZED,
+ imdsv2_session_token_data="unauthorized",
+ )
+ credential_source_token_url = self.CREDENTIAL_SOURCE.copy()
+ credential_source_token_url[
+ "imdsv2_session_token_url"
+ ] = IMDSV2_SESSION_TOKEN_URL
+ credentials = self.make_credentials(
+ credential_source=credential_source_token_url
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(request)
+
+ assert excinfo.match(r"Unable to retrieve AWS Session Token")
+
+ # Assert session token request
+ self.assert_aws_metadata_request_kwargs(
+ request.call_args_list[0][1],
+ IMDSV2_SESSION_TOKEN_URL,
+ {"X-aws-ec2-metadata-token-ttl-seconds": "300"},
+ "PUT",
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_retrieve_subject_token_success_permanent_creds_no_environment_vars(
+ self, utcnow
+ ):
+ # Simualte a permanent credential without a session token is
+ # returned by the security-credentials endpoint.
+ security_creds_response = self.AWS_SECURITY_CREDENTIALS_RESPONSE.copy()
+ security_creds_response.pop("Token")
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ request = self.make_mock_request(
+ region_status=http_client.OK,
+ region_name=self.AWS_REGION,
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=security_creds_response,
+ )
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ subject_token = credentials.retrieve_subject_token(request)
+
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY}
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_retrieve_subject_token_success_environment_vars(self, utcnow, monkeypatch):
+ monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
+ monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
+ monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
+ monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION)
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_retrieve_subject_token_success_environment_vars_with_default_region(
+ self, utcnow, monkeypatch
+ ):
+ monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
+ monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
+ monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
+ monkeypatch.setenv(environment_vars.AWS_DEFAULT_REGION, self.AWS_REGION)
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_retrieve_subject_token_success_environment_vars_with_both_regions_set(
+ self, utcnow, monkeypatch
+ ):
+ monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
+ monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
+ monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
+ monkeypatch.setenv(environment_vars.AWS_DEFAULT_REGION, "Malformed AWS Region")
+ # This test makes sure that the AWS_REGION gets used over AWS_DEFAULT_REGION,
+ # So, AWS_DEFAULT_REGION is set to something that would cause the test to fail,
+ # And AWS_REGION is set to the a valid value, and it should succeed
+ monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION)
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_retrieve_subject_token_success_environment_vars_no_session_token(
+ self, utcnow, monkeypatch
+ ):
+ monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
+ monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
+ monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION)
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY}
+ )
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_retrieve_subject_token_success_environment_vars_except_region(
+ self, utcnow, monkeypatch
+ ):
+ monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
+ monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
+ monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ # Region will be queried since it is not found in envvars.
+ request = self.make_mock_request(
+ region_status=http_client.OK, region_name=self.AWS_REGION
+ )
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ subject_token = credentials.retrieve_subject_token(request)
+
+ assert subject_token == self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+
+ def test_retrieve_subject_token_error_determining_aws_region(self):
+ # Simulate error in retrieving the AWS region.
+ request = self.make_mock_request(region_status=http_client.BAD_REQUEST)
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(request)
+
+ assert excinfo.match(r"Unable to retrieve AWS region")
+
+ def test_retrieve_subject_token_error_determining_aws_role(self):
+ # Simulate error in retrieving the AWS role name.
+ request = self.make_mock_request(
+ region_status=http_client.OK,
+ region_name=self.AWS_REGION,
+ role_status=http_client.BAD_REQUEST,
+ )
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(request)
+
+ assert excinfo.match(r"Unable to retrieve AWS role name")
+
+ def test_retrieve_subject_token_error_determining_security_creds_url(self):
+ # Simulate the security-credentials url is missing. This is needed for
+ # determining the AWS security credentials when not found in envvars.
+ credential_source = self.CREDENTIAL_SOURCE.copy()
+ credential_source.pop("url")
+ request = self.make_mock_request(
+ region_status=http_client.OK, region_name=self.AWS_REGION
+ )
+ credentials = self.make_credentials(credential_source=credential_source)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(request)
+
+ assert excinfo.match(
+ r"Unable to determine the AWS metadata server security credentials endpoint"
+ )
+
+ def test_retrieve_subject_token_error_determining_aws_security_creds(self):
+ # Simulate error in retrieving the AWS security credentials.
+ request = self.make_mock_request(
+ region_status=http_client.OK,
+ region_name=self.AWS_REGION,
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.BAD_REQUEST,
+ )
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(request)
+
+ assert excinfo.match(r"Unable to retrieve AWS security credentials")
+
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_refresh_success_without_impersonation_ignore_default_scopes(
+ self, utcnow, mock_auth_lib_value
+ ):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ expected_subject_token = self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false source/aws",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "scope": " ".join(SCOPES),
+ "subject_token": expected_subject_token,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ }
+ request = self.make_mock_request(
+ region_status=http_client.OK,
+ region_name=self.AWS_REGION,
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ token_status=http_client.OK,
+ token_data=self.SUCCESS_RESPONSE,
+ )
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ credential_source=self.CREDENTIAL_SOURCE,
+ quota_project_id=QUOTA_PROJECT_ID,
+ scopes=SCOPES,
+ # Default scopes should be ignored.
+ default_scopes=["ignored"],
+ )
+
+ credentials.refresh(request)
+
+ assert len(request.call_args_list) == 4
+ # Fourth request should be sent to GCP STS endpoint.
+ self.assert_token_request_kwargs(
+ request.call_args_list[3][1], token_headers, token_request_data
+ )
+ assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+ assert credentials.quota_project_id == QUOTA_PROJECT_ID
+ assert credentials.scopes == SCOPES
+ assert credentials.default_scopes == ["ignored"]
+
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_refresh_success_without_impersonation_use_default_scopes(
+ self, utcnow, mock_auth_lib_value
+ ):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ expected_subject_token = self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false source/aws",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "scope": " ".join(SCOPES),
+ "subject_token": expected_subject_token,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ }
+ request = self.make_mock_request(
+ region_status=http_client.OK,
+ region_name=self.AWS_REGION,
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ token_status=http_client.OK,
+ token_data=self.SUCCESS_RESPONSE,
+ )
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ credential_source=self.CREDENTIAL_SOURCE,
+ quota_project_id=QUOTA_PROJECT_ID,
+ scopes=None,
+ # Default scopes should be used since user specified scopes are none.
+ default_scopes=SCOPES,
+ )
+
+ credentials.refresh(request)
+
+ assert len(request.call_args_list) == 4
+ # Fourth request should be sent to GCP STS endpoint.
+ self.assert_token_request_kwargs(
+ request.call_args_list[3][1], token_headers, token_request_data
+ )
+ assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+ assert credentials.quota_project_id == QUOTA_PROJECT_ID
+ assert credentials.scopes is None
+ assert credentials.default_scopes == SCOPES
+
+ @mock.patch(
+ "google.auth.metrics.token_request_access_token_impersonate",
+ return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ )
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_refresh_success_with_impersonation_ignore_default_scopes(
+ self, utcnow, mock_metrics_header_value, mock_auth_lib_value
+ ):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+ ).isoformat("T") + "Z"
+ expected_subject_token = self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false source/aws",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "scope": "https://www.googleapis.com/auth/iam",
+ "subject_token": expected_subject_token,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ }
+ # Service account impersonation request/response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ impersonation_headers = {
+ "Content-Type": "application/json",
+ "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+ "x-goog-user-project": QUOTA_PROJECT_ID,
+ "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ "x-identity-trust-boundary": "0",
+ }
+ impersonation_request_data = {
+ "delegates": None,
+ "scope": SCOPES,
+ "lifetime": "3600s",
+ }
+ request = self.make_mock_request(
+ region_status=http_client.OK,
+ region_name=self.AWS_REGION,
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ token_status=http_client.OK,
+ token_data=self.SUCCESS_RESPONSE,
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ credential_source=self.CREDENTIAL_SOURCE,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ quota_project_id=QUOTA_PROJECT_ID,
+ scopes=SCOPES,
+ # Default scopes should be ignored.
+ default_scopes=["ignored"],
+ )
+
+ credentials.refresh(request)
+
+ assert len(request.call_args_list) == 5
+ # Fourth request should be sent to GCP STS endpoint.
+ self.assert_token_request_kwargs(
+ request.call_args_list[3][1], token_headers, token_request_data
+ )
+ # Fifth request should be sent to iamcredentials endpoint for service
+ # account impersonation.
+ self.assert_impersonation_request_kwargs(
+ request.call_args_list[4][1],
+ impersonation_headers,
+ impersonation_request_data,
+ )
+ assert credentials.token == impersonation_response["accessToken"]
+ assert credentials.quota_project_id == QUOTA_PROJECT_ID
+ assert credentials.scopes == SCOPES
+ assert credentials.default_scopes == ["ignored"]
+
+ @mock.patch(
+ "google.auth.metrics.token_request_access_token_impersonate",
+ return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ )
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_refresh_success_with_impersonation_use_default_scopes(
+ self, utcnow, mock_metrics_header_value, mock_auth_lib_value
+ ):
+ utcnow.return_value = datetime.datetime.strptime(
+ self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+ )
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+ ).isoformat("T") + "Z"
+ expected_subject_token = self.make_serialized_aws_signed_request(
+ {
+ "access_key_id": ACCESS_KEY_ID,
+ "secret_access_key": SECRET_ACCESS_KEY,
+ "security_token": TOKEN,
+ }
+ )
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false source/aws",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "scope": "https://www.googleapis.com/auth/iam",
+ "subject_token": expected_subject_token,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ }
+ # Service account impersonation request/response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ impersonation_headers = {
+ "Content-Type": "application/json",
+ "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+ "x-goog-user-project": QUOTA_PROJECT_ID,
+ "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ "x-identity-trust-boundary": "0",
+ }
+ impersonation_request_data = {
+ "delegates": None,
+ "scope": SCOPES,
+ "lifetime": "3600s",
+ }
+ request = self.make_mock_request(
+ region_status=http_client.OK,
+ region_name=self.AWS_REGION,
+ role_status=http_client.OK,
+ role_name=self.AWS_ROLE,
+ security_credentials_status=http_client.OK,
+ security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+ token_status=http_client.OK,
+ token_data=self.SUCCESS_RESPONSE,
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ credential_source=self.CREDENTIAL_SOURCE,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ quota_project_id=QUOTA_PROJECT_ID,
+ scopes=None,
+ # Default scopes should be used since user specified scopes are none.
+ default_scopes=SCOPES,
+ )
+
+ credentials.refresh(request)
+
+ assert len(request.call_args_list) == 5
+ # Fourth request should be sent to GCP STS endpoint.
+ self.assert_token_request_kwargs(
+ request.call_args_list[3][1], token_headers, token_request_data
+ )
+ # Fifth request should be sent to iamcredentials endpoint for service
+ # account impersonation.
+ self.assert_impersonation_request_kwargs(
+ request.call_args_list[4][1],
+ impersonation_headers,
+ impersonation_request_data,
+ )
+ assert credentials.token == impersonation_response["accessToken"]
+ assert credentials.quota_project_id == QUOTA_PROJECT_ID
+ assert credentials.scopes is None
+ assert credentials.default_scopes == SCOPES
+
+ def test_refresh_with_retrieve_subject_token_error(self):
+ request = self.make_mock_request(region_status=http_client.BAD_REQUEST)
+ credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.refresh(request)
+
+ assert excinfo.match(r"Unable to retrieve AWS region")
diff --git a/contrib/python/google-auth/py3/tests/test_credentials.py b/contrib/python/google-auth/py3/tests/test_credentials.py
new file mode 100644
index 0000000000..99235cda61
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_credentials.py
@@ -0,0 +1,224 @@
+# Copyright 2016 Google LLC
+#
+# 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 datetime
+
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import credentials
+
+
+class CredentialsImpl(credentials.Credentials):
+ def refresh(self, request):
+ self.token = request
+
+ def with_quota_project(self, quota_project_id):
+ raise NotImplementedError()
+
+
+class CredentialsImplWithMetrics(credentials.Credentials):
+ def refresh(self, request):
+ self.token = request
+
+ def _metric_header_for_usage(self):
+ return "foo"
+
+
+def test_credentials_constructor():
+ credentials = CredentialsImpl()
+ assert not credentials.token
+ assert not credentials.expiry
+ assert not credentials.expired
+ assert not credentials.valid
+ assert credentials.universe_domain == "googleapis.com"
+
+
+def test_expired_and_valid():
+ credentials = CredentialsImpl()
+ credentials.token = "token"
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ # Set the expiration to one second more than now plus the clock skew
+ # accomodation. These credentials should be valid.
+ credentials.expiry = (
+ datetime.datetime.utcnow()
+ + _helpers.REFRESH_THRESHOLD
+ + datetime.timedelta(seconds=1)
+ )
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ # Set the credentials expiration to now. Because of the clock skew
+ # accomodation, these credentials should report as expired.
+ credentials.expiry = datetime.datetime.utcnow()
+
+ assert not credentials.valid
+ assert credentials.expired
+
+
+def test_before_request():
+ credentials = CredentialsImpl()
+ request = "token"
+ headers = {}
+
+ # First call should call refresh, setting the token.
+ credentials.before_request(request, "http://example.com", "GET", headers)
+ assert credentials.valid
+ assert credentials.token == "token"
+ assert headers["authorization"] == "Bearer token"
+ assert "x-identity-trust-boundary" not in headers
+
+ request = "token2"
+ headers = {}
+
+ # Second call shouldn't call refresh.
+ credentials.before_request(request, "http://example.com", "GET", headers)
+ assert credentials.valid
+ assert credentials.token == "token"
+ assert headers["authorization"] == "Bearer token"
+ assert "x-identity-trust-boundary" not in headers
+
+
+def test_before_request_with_trust_boundary():
+ DUMMY_BOUNDARY = "00110101"
+ credentials = CredentialsImpl()
+ credentials._trust_boundary = DUMMY_BOUNDARY
+ request = "token"
+ headers = {}
+
+ # First call should call refresh, setting the token.
+ credentials.before_request(request, "http://example.com", "GET", headers)
+ assert credentials.valid
+ assert credentials.token == "token"
+ assert headers["authorization"] == "Bearer token"
+ assert headers["x-identity-trust-boundary"] == DUMMY_BOUNDARY
+
+ request = "token2"
+ headers = {}
+
+ # Second call shouldn't call refresh.
+ credentials.before_request(request, "http://example.com", "GET", headers)
+ assert credentials.valid
+ assert credentials.token == "token"
+ assert headers["authorization"] == "Bearer token"
+ assert headers["x-identity-trust-boundary"] == DUMMY_BOUNDARY
+
+
+def test_before_request_metrics():
+ credentials = CredentialsImplWithMetrics()
+ request = "token"
+ headers = {}
+
+ credentials.before_request(request, "http://example.com", "GET", headers)
+ assert headers["x-goog-api-client"] == "foo"
+
+
+def test_anonymous_credentials_ctor():
+ anon = credentials.AnonymousCredentials()
+ assert anon.token is None
+ assert anon.expiry is None
+ assert not anon.expired
+ assert anon.valid
+
+
+def test_anonymous_credentials_refresh():
+ anon = credentials.AnonymousCredentials()
+ request = object()
+ with pytest.raises(ValueError):
+ anon.refresh(request)
+
+
+def test_anonymous_credentials_apply_default():
+ anon = credentials.AnonymousCredentials()
+ headers = {}
+ anon.apply(headers)
+ assert headers == {}
+ with pytest.raises(ValueError):
+ anon.apply(headers, token="TOKEN")
+
+
+def test_anonymous_credentials_before_request():
+ anon = credentials.AnonymousCredentials()
+ request = object()
+ method = "GET"
+ url = "https://example.com/api/endpoint"
+ headers = {}
+ anon.before_request(request, method, url, headers)
+ assert headers == {}
+
+
+class ReadOnlyScopedCredentialsImpl(credentials.ReadOnlyScoped, CredentialsImpl):
+ @property
+ def requires_scopes(self):
+ return super(ReadOnlyScopedCredentialsImpl, self).requires_scopes
+
+
+def test_readonly_scoped_credentials_constructor():
+ credentials = ReadOnlyScopedCredentialsImpl()
+ assert credentials._scopes is None
+
+
+def test_readonly_scoped_credentials_scopes():
+ credentials = ReadOnlyScopedCredentialsImpl()
+ credentials._scopes = ["one", "two"]
+ assert credentials.scopes == ["one", "two"]
+ assert credentials.has_scopes(["one"])
+ assert credentials.has_scopes(["two"])
+ assert credentials.has_scopes(["one", "two"])
+ assert not credentials.has_scopes(["three"])
+
+
+def test_readonly_scoped_credentials_requires_scopes():
+ credentials = ReadOnlyScopedCredentialsImpl()
+ assert not credentials.requires_scopes
+
+
+class RequiresScopedCredentialsImpl(credentials.Scoped, CredentialsImpl):
+ def __init__(self, scopes=None, default_scopes=None):
+ super(RequiresScopedCredentialsImpl, self).__init__()
+ self._scopes = scopes
+ self._default_scopes = default_scopes
+
+ @property
+ def requires_scopes(self):
+ return not self.scopes
+
+ def with_scopes(self, scopes, default_scopes=None):
+ return RequiresScopedCredentialsImpl(
+ scopes=scopes, default_scopes=default_scopes
+ )
+
+
+def test_create_scoped_if_required_scoped():
+ unscoped_credentials = RequiresScopedCredentialsImpl()
+ scoped_credentials = credentials.with_scopes_if_required(
+ unscoped_credentials, ["one", "two"]
+ )
+
+ assert scoped_credentials is not unscoped_credentials
+ assert not scoped_credentials.requires_scopes
+ assert scoped_credentials.has_scopes(["one", "two"])
+
+
+def test_create_scoped_if_required_not_scopes():
+ unscoped_credentials = CredentialsImpl()
+ scoped_credentials = credentials.with_scopes_if_required(
+ unscoped_credentials, ["one", "two"]
+ )
+
+ assert scoped_credentials is unscoped_credentials
diff --git a/contrib/python/google-auth/py3/tests/test_downscoped.py b/contrib/python/google-auth/py3/tests/test_downscoped.py
new file mode 100644
index 0000000000..b011380bdb
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_downscoped.py
@@ -0,0 +1,696 @@
+# Copyright 2021 Google LLC
+#
+# 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 datetime
+import http.client as http_client
+import json
+import urllib
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import downscoped
+from google.auth import exceptions
+from google.auth import transport
+
+
+EXPRESSION = (
+ "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
+)
+TITLE = "customer-a-objects"
+DESCRIPTION = (
+ "Condition to make permissions available for objects starting with customer-a"
+)
+AVAILABLE_RESOURCE = "//storage.googleapis.com/projects/_/buckets/example-bucket"
+AVAILABLE_PERMISSIONS = ["inRole:roles/storage.objectViewer"]
+
+OTHER_EXPRESSION = (
+ "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-b')"
+)
+OTHER_TITLE = "customer-b-objects"
+OTHER_DESCRIPTION = (
+ "Condition to make permissions available for objects starting with customer-b"
+)
+OTHER_AVAILABLE_RESOURCE = "//storage.googleapis.com/projects/_/buckets/other-bucket"
+OTHER_AVAILABLE_PERMISSIONS = ["inRole:roles/storage.objectCreator"]
+QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID"
+GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
+REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+TOKEN_EXCHANGE_ENDPOINT = "https://sts.googleapis.com/v1/token"
+SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+SUCCESS_RESPONSE = {
+ "access_token": "ACCESS_TOKEN",
+ "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+}
+ERROR_RESPONSE = {
+ "error": "invalid_grant",
+ "error_description": "Subject token is invalid.",
+ "error_uri": "https://tools.ietf.org/html/rfc6749",
+}
+CREDENTIAL_ACCESS_BOUNDARY_JSON = {
+ "accessBoundary": {
+ "accessBoundaryRules": [
+ {
+ "availablePermissions": AVAILABLE_PERMISSIONS,
+ "availableResource": AVAILABLE_RESOURCE,
+ "availabilityCondition": {
+ "expression": EXPRESSION,
+ "title": TITLE,
+ "description": DESCRIPTION,
+ },
+ }
+ ]
+ }
+}
+
+
+class SourceCredentials(credentials.Credentials):
+ def __init__(self, raise_error=False, expires_in=3600):
+ super(SourceCredentials, self).__init__()
+ self._counter = 0
+ self._raise_error = raise_error
+ self._expires_in = expires_in
+
+ def refresh(self, request):
+ if self._raise_error:
+ raise exceptions.RefreshError(
+ "Failed to refresh access token in source credentials."
+ )
+ now = _helpers.utcnow()
+ self._counter += 1
+ self.token = "ACCESS_TOKEN_{}".format(self._counter)
+ self.expiry = now + datetime.timedelta(seconds=self._expires_in)
+
+
+def make_availability_condition(expression, title=None, description=None):
+ return downscoped.AvailabilityCondition(expression, title, description)
+
+
+def make_access_boundary_rule(
+ available_resource, available_permissions, availability_condition=None
+):
+ return downscoped.AccessBoundaryRule(
+ available_resource, available_permissions, availability_condition
+ )
+
+
+def make_credential_access_boundary(rules):
+ return downscoped.CredentialAccessBoundary(rules)
+
+
+class TestAvailabilityCondition(object):
+ def test_constructor(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+
+ assert availability_condition.expression == EXPRESSION
+ assert availability_condition.title == TITLE
+ assert availability_condition.description == DESCRIPTION
+
+ def test_constructor_required_params_only(self):
+ availability_condition = make_availability_condition(EXPRESSION)
+
+ assert availability_condition.expression == EXPRESSION
+ assert availability_condition.title is None
+ assert availability_condition.description is None
+
+ def test_setters(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ availability_condition.expression = OTHER_EXPRESSION
+ availability_condition.title = OTHER_TITLE
+ availability_condition.description = OTHER_DESCRIPTION
+
+ assert availability_condition.expression == OTHER_EXPRESSION
+ assert availability_condition.title == OTHER_TITLE
+ assert availability_condition.description == OTHER_DESCRIPTION
+
+ def test_invalid_expression_type(self):
+ with pytest.raises(TypeError) as excinfo:
+ make_availability_condition([EXPRESSION], TITLE, DESCRIPTION)
+
+ assert excinfo.match("The provided expression is not a string.")
+
+ def test_invalid_title_type(self):
+ with pytest.raises(TypeError) as excinfo:
+ make_availability_condition(EXPRESSION, False, DESCRIPTION)
+
+ assert excinfo.match("The provided title is not a string or None.")
+
+ def test_invalid_description_type(self):
+ with pytest.raises(TypeError) as excinfo:
+ make_availability_condition(EXPRESSION, TITLE, False)
+
+ assert excinfo.match("The provided description is not a string or None.")
+
+ def test_to_json_required_params_only(self):
+ availability_condition = make_availability_condition(EXPRESSION)
+
+ assert availability_condition.to_json() == {"expression": EXPRESSION}
+
+ def test_to_json_(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+
+ assert availability_condition.to_json() == {
+ "expression": EXPRESSION,
+ "title": TITLE,
+ "description": DESCRIPTION,
+ }
+
+
+class TestAccessBoundaryRule(object):
+ def test_constructor(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+
+ assert access_boundary_rule.available_resource == AVAILABLE_RESOURCE
+ assert access_boundary_rule.available_permissions == tuple(
+ AVAILABLE_PERMISSIONS
+ )
+ assert access_boundary_rule.availability_condition == availability_condition
+
+ def test_constructor_required_params_only(self):
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS
+ )
+
+ assert access_boundary_rule.available_resource == AVAILABLE_RESOURCE
+ assert access_boundary_rule.available_permissions == tuple(
+ AVAILABLE_PERMISSIONS
+ )
+ assert access_boundary_rule.availability_condition is None
+
+ def test_setters(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ other_availability_condition = make_availability_condition(
+ OTHER_EXPRESSION, OTHER_TITLE, OTHER_DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+ access_boundary_rule.available_resource = OTHER_AVAILABLE_RESOURCE
+ access_boundary_rule.available_permissions = OTHER_AVAILABLE_PERMISSIONS
+ access_boundary_rule.availability_condition = other_availability_condition
+
+ assert access_boundary_rule.available_resource == OTHER_AVAILABLE_RESOURCE
+ assert access_boundary_rule.available_permissions == tuple(
+ OTHER_AVAILABLE_PERMISSIONS
+ )
+ assert (
+ access_boundary_rule.availability_condition == other_availability_condition
+ )
+
+ def test_invalid_available_resource_type(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ with pytest.raises(TypeError) as excinfo:
+ make_access_boundary_rule(
+ None, AVAILABLE_PERMISSIONS, availability_condition
+ )
+
+ assert excinfo.match("The provided available_resource is not a string.")
+
+ def test_invalid_available_permissions_type(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ with pytest.raises(TypeError) as excinfo:
+ make_access_boundary_rule(
+ AVAILABLE_RESOURCE, [0, 1, 2], availability_condition
+ )
+
+ assert excinfo.match(
+ "Provided available_permissions are not a list of strings."
+ )
+
+ def test_invalid_available_permissions_value(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ with pytest.raises(ValueError) as excinfo:
+ make_access_boundary_rule(
+ AVAILABLE_RESOURCE,
+ ["roles/storage.objectViewer"],
+ availability_condition,
+ )
+
+ assert excinfo.match("available_permissions must be prefixed with 'inRole:'.")
+
+ def test_invalid_availability_condition_type(self):
+ with pytest.raises(TypeError) as excinfo:
+ make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, {"foo": "bar"}
+ )
+
+ assert excinfo.match(
+ "The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None."
+ )
+
+ def test_to_json(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+
+ assert access_boundary_rule.to_json() == {
+ "availablePermissions": AVAILABLE_PERMISSIONS,
+ "availableResource": AVAILABLE_RESOURCE,
+ "availabilityCondition": {
+ "expression": EXPRESSION,
+ "title": TITLE,
+ "description": DESCRIPTION,
+ },
+ }
+
+ def test_to_json_required_params_only(self):
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS
+ )
+
+ assert access_boundary_rule.to_json() == {
+ "availablePermissions": AVAILABLE_PERMISSIONS,
+ "availableResource": AVAILABLE_RESOURCE,
+ }
+
+
+class TestCredentialAccessBoundary(object):
+ def test_constructor(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+ rules = [access_boundary_rule]
+ credential_access_boundary = make_credential_access_boundary(rules)
+
+ assert credential_access_boundary.rules == tuple(rules)
+
+ def test_setters(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+ rules = [access_boundary_rule]
+ other_availability_condition = make_availability_condition(
+ OTHER_EXPRESSION, OTHER_TITLE, OTHER_DESCRIPTION
+ )
+ other_access_boundary_rule = make_access_boundary_rule(
+ OTHER_AVAILABLE_RESOURCE,
+ OTHER_AVAILABLE_PERMISSIONS,
+ other_availability_condition,
+ )
+ other_rules = [other_access_boundary_rule]
+ credential_access_boundary = make_credential_access_boundary(rules)
+ credential_access_boundary.rules = other_rules
+
+ assert credential_access_boundary.rules == tuple(other_rules)
+
+ def test_add_rule(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+ rules = [access_boundary_rule] * 9
+ credential_access_boundary = make_credential_access_boundary(rules)
+
+ # Add one more rule. This should not raise an error.
+ additional_access_boundary_rule = make_access_boundary_rule(
+ OTHER_AVAILABLE_RESOURCE, OTHER_AVAILABLE_PERMISSIONS
+ )
+ credential_access_boundary.add_rule(additional_access_boundary_rule)
+
+ assert len(credential_access_boundary.rules) == 10
+ assert credential_access_boundary.rules[9] == additional_access_boundary_rule
+
+ def test_add_rule_invalid_value(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+ rules = [access_boundary_rule] * 10
+ credential_access_boundary = make_credential_access_boundary(rules)
+
+ # Add one more rule to exceed maximum allowed rules.
+ with pytest.raises(ValueError) as excinfo:
+ credential_access_boundary.add_rule(access_boundary_rule)
+
+ assert excinfo.match(
+ "Credential access boundary rules can have a maximum of 10 rules."
+ )
+ assert len(credential_access_boundary.rules) == 10
+
+ def test_add_rule_invalid_type(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+ rules = [access_boundary_rule]
+ credential_access_boundary = make_credential_access_boundary(rules)
+
+ # Add an invalid rule to exceed maximum allowed rules.
+ with pytest.raises(TypeError) as excinfo:
+ credential_access_boundary.add_rule("invalid")
+
+ assert excinfo.match(
+ "The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
+ )
+ assert len(credential_access_boundary.rules) == 1
+ assert credential_access_boundary.rules[0] == access_boundary_rule
+
+ def test_invalid_rules_type(self):
+ with pytest.raises(TypeError) as excinfo:
+ make_credential_access_boundary(["invalid"])
+
+ assert excinfo.match(
+ "List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
+ )
+
+ def test_invalid_rules_value(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+ too_many_rules = [access_boundary_rule] * 11
+ with pytest.raises(ValueError) as excinfo:
+ make_credential_access_boundary(too_many_rules)
+
+ assert excinfo.match(
+ "Credential access boundary rules can have a maximum of 10 rules."
+ )
+
+ def test_to_json(self):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+ rules = [access_boundary_rule]
+ credential_access_boundary = make_credential_access_boundary(rules)
+
+ assert credential_access_boundary.to_json() == {
+ "accessBoundary": {
+ "accessBoundaryRules": [
+ {
+ "availablePermissions": AVAILABLE_PERMISSIONS,
+ "availableResource": AVAILABLE_RESOURCE,
+ "availabilityCondition": {
+ "expression": EXPRESSION,
+ "title": TITLE,
+ "description": DESCRIPTION,
+ },
+ }
+ ]
+ }
+ }
+
+
+class TestCredentials(object):
+ @staticmethod
+ def make_credentials(source_credentials=SourceCredentials(), quota_project_id=None):
+ availability_condition = make_availability_condition(
+ EXPRESSION, TITLE, DESCRIPTION
+ )
+ access_boundary_rule = make_access_boundary_rule(
+ AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+ )
+ rules = [access_boundary_rule]
+ credential_access_boundary = make_credential_access_boundary(rules)
+
+ return downscoped.Credentials(
+ source_credentials, credential_access_boundary, quota_project_id
+ )
+
+ @staticmethod
+ def make_mock_request(data, status=http_client.OK):
+ response = mock.create_autospec(transport.Response, instance=True)
+ response.status = status
+ response.data = json.dumps(data).encode("utf-8")
+
+ request = mock.create_autospec(transport.Request)
+ request.return_value = response
+
+ return request
+
+ @staticmethod
+ def assert_request_kwargs(request_kwargs, headers, request_data):
+ """Asserts the request was called with the expected parameters.
+ """
+ assert request_kwargs["url"] == TOKEN_EXCHANGE_ENDPOINT
+ assert request_kwargs["method"] == "POST"
+ assert request_kwargs["headers"] == headers
+ assert request_kwargs["body"] is not None
+ body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
+ for (k, v) in body_tuples:
+ assert v.decode("utf-8") == request_data[k.decode("utf-8")]
+ assert len(body_tuples) == len(request_data.keys())
+
+ def test_default_state(self):
+ credentials = self.make_credentials()
+
+ # No token acquired yet.
+ assert not credentials.token
+ assert not credentials.valid
+ # Expiration hasn't been set yet.
+ assert not credentials.expiry
+ assert not credentials.expired
+ # No quota project ID set.
+ assert not credentials.quota_project_id
+
+ def test_with_quota_project(self):
+ credentials = self.make_credentials()
+
+ assert not credentials.quota_project_id
+
+ quota_project_creds = credentials.with_quota_project("project-foo")
+
+ assert quota_project_creds.quota_project_id == "project-foo"
+
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh(self, unused_utcnow):
+ response = SUCCESS_RESPONSE.copy()
+ # Test custom expiration to confirm expiry is set correctly.
+ response["expires_in"] = 2800
+ expected_expiry = datetime.datetime.min + datetime.timedelta(
+ seconds=response["expires_in"]
+ )
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
+ request_data = {
+ "grant_type": GRANT_TYPE,
+ "subject_token": "ACCESS_TOKEN_1",
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "requested_token_type": REQUESTED_TOKEN_TYPE,
+ "options": urllib.parse.quote(json.dumps(CREDENTIAL_ACCESS_BOUNDARY_JSON)),
+ }
+ request = self.make_mock_request(status=http_client.OK, data=response)
+ source_credentials = SourceCredentials()
+ credentials = self.make_credentials(source_credentials=source_credentials)
+
+ # Spy on calls to source credentials refresh to confirm the expected request
+ # instance is used.
+ with mock.patch.object(
+ source_credentials, "refresh", wraps=source_credentials.refresh
+ ) as wrapped_souce_cred_refresh:
+ credentials.refresh(request)
+
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == response["access_token"]
+ # Confirm source credentials called with the same request instance.
+ wrapped_souce_cred_refresh.assert_called_with(request)
+
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh_without_response_expires_in(self, unused_utcnow):
+ response = SUCCESS_RESPONSE.copy()
+ # Simulate the response is missing the expires_in field.
+ # The downscoped token expiration should match the source credentials
+ # expiration.
+ del response["expires_in"]
+ expected_expires_in = 1800
+ # Simulate the source credentials generates a token with 1800 second
+ # expiration time. The generated downscoped token should have the same
+ # expiration time.
+ source_credentials = SourceCredentials(expires_in=expected_expires_in)
+ expected_expiry = datetime.datetime.min + datetime.timedelta(
+ seconds=expected_expires_in
+ )
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
+ request_data = {
+ "grant_type": GRANT_TYPE,
+ "subject_token": "ACCESS_TOKEN_1",
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "requested_token_type": REQUESTED_TOKEN_TYPE,
+ "options": urllib.parse.quote(json.dumps(CREDENTIAL_ACCESS_BOUNDARY_JSON)),
+ }
+ request = self.make_mock_request(status=http_client.OK, data=response)
+ credentials = self.make_credentials(source_credentials=source_credentials)
+
+ # Spy on calls to source credentials refresh to confirm the expected request
+ # instance is used.
+ with mock.patch.object(
+ source_credentials, "refresh", wraps=source_credentials.refresh
+ ) as wrapped_souce_cred_refresh:
+ credentials.refresh(request)
+
+ self.assert_request_kwargs(request.call_args[1], headers, request_data)
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == response["access_token"]
+ # Confirm source credentials called with the same request instance.
+ wrapped_souce_cred_refresh.assert_called_with(request)
+
+ def test_refresh_token_exchange_error(self):
+ request = self.make_mock_request(
+ status=http_client.BAD_REQUEST, data=ERROR_RESPONSE
+ )
+ credentials = self.make_credentials()
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ credentials.refresh(request)
+
+ assert excinfo.match(
+ r"Error code invalid_grant: Subject token is invalid. - https://tools.ietf.org/html/rfc6749"
+ )
+ assert not credentials.expired
+ assert credentials.token is None
+
+ def test_refresh_source_credentials_refresh_error(self):
+ # Initialize downscoped credentials with source credentials that raise
+ # an error on refresh.
+ credentials = self.make_credentials(
+ source_credentials=SourceCredentials(raise_error=True)
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.refresh(mock.sentinel.request)
+
+ assert excinfo.match(r"Failed to refresh access token in source credentials.")
+ assert not credentials.expired
+ assert credentials.token is None
+
+ def test_apply_without_quota_project_id(self):
+ headers = {}
+ request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE)
+ credentials = self.make_credentials()
+
+ credentials.refresh(request)
+ credentials.apply(headers)
+
+ assert headers == {
+ "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"])
+ }
+
+ def test_apply_with_quota_project_id(self):
+ headers = {"other": "header-value"}
+ request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE)
+ credentials = self.make_credentials(quota_project_id=QUOTA_PROJECT_ID)
+
+ credentials.refresh(request)
+ credentials.apply(headers)
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]),
+ "x-goog-user-project": QUOTA_PROJECT_ID,
+ }
+
+ def test_before_request(self):
+ headers = {"other": "header-value"}
+ request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE)
+ credentials = self.make_credentials()
+
+ # First call should call refresh, setting the token.
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]),
+ }
+
+ # Second call shouldn't call refresh (request should be untouched).
+ credentials.before_request(
+ mock.sentinel.request, "POST", "https://example.com/api", headers
+ )
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]),
+ }
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_before_request_expired(self, utcnow):
+ headers = {}
+ request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE)
+ credentials = self.make_credentials()
+ credentials.token = "token"
+ utcnow.return_value = datetime.datetime.min
+ # Set the expiration to one second more than now plus the clock skew
+ # accommodation. These credentials should be valid.
+ credentials.expiry = (
+ datetime.datetime.min
+ + _helpers.REFRESH_THRESHOLD
+ + datetime.timedelta(seconds=1)
+ )
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ # Cached token should be used.
+ assert headers == {"authorization": "Bearer token"}
+
+ # Next call should simulate 1 second passed.
+ utcnow.return_value = datetime.datetime.min + datetime.timedelta(seconds=1)
+
+ assert not credentials.valid
+ assert credentials.expired
+
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ # New token should be retrieved.
+ assert headers == {
+ "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"])
+ }
diff --git a/contrib/python/google-auth/py3/tests/test_exceptions.py b/contrib/python/google-auth/py3/tests/test_exceptions.py
new file mode 100644
index 0000000000..6f542498fc
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_exceptions.py
@@ -0,0 +1,55 @@
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest # type: ignore
+
+from google.auth import exceptions # type:ignore
+
+
+@pytest.fixture(
+ params=[
+ exceptions.GoogleAuthError,
+ exceptions.TransportError,
+ exceptions.RefreshError,
+ exceptions.UserAccessTokenError,
+ exceptions.DefaultCredentialsError,
+ exceptions.MutualTLSChannelError,
+ exceptions.OAuthError,
+ exceptions.ReauthFailError,
+ exceptions.ReauthSamlChallengeFailError,
+ ]
+)
+def retryable_exception(request):
+ return request.param
+
+
+@pytest.fixture(params=[exceptions.ClientCertError])
+def non_retryable_exception(request):
+ return request.param
+
+
+def test_default_retryable_exceptions(retryable_exception):
+ assert not retryable_exception().retryable
+
+
+@pytest.mark.parametrize("retryable", [True, False])
+def test_retryable_exceptions(retryable_exception, retryable):
+ retryable_exception = retryable_exception(retryable=retryable)
+ assert retryable_exception.retryable == retryable
+
+
+@pytest.mark.parametrize("retryable", [True, False])
+def test_non_retryable_exceptions(non_retryable_exception, retryable):
+ non_retryable_exception = non_retryable_exception(retryable=retryable)
+ assert not non_retryable_exception.retryable
diff --git a/contrib/python/google-auth/py3/tests/test_external_account.py b/contrib/python/google-auth/py3/tests/test_external_account.py
new file mode 100644
index 0000000000..0b165bc70b
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_external_account.py
@@ -0,0 +1,1900 @@
+# Copyright 2020 Google LLC
+#
+# 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 datetime
+import http.client as http_client
+import json
+import urllib
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import external_account
+from google.auth import transport
+
+
+IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
+ "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
+)
+LANG_LIBRARY_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1"
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password"
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com"
+# List of valid workforce pool audiences.
+TEST_USER_AUDIENCES = [
+ "//iam.googleapis.com/locations/global/workforcePools/pool-id/providers/provider-id",
+ "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
+ "//iam.googleapis.com/locations/eu/workforcePools/workloadIdentityPools/providers/provider-id",
+]
+# Workload identity pool audiences or invalid workforce pool audiences.
+TEST_NON_USER_AUDIENCES = [
+ # Legacy K8s audience format.
+ "identitynamespace:1f12345:my_provider",
+ (
+ "//iam.googleapis.com/projects/123456/locations/"
+ "global/workloadIdentityPools/pool-id/providers/"
+ "provider-id"
+ ),
+ (
+ "//iam.googleapis.com/projects/123456/locations/"
+ "eu/workloadIdentityPools/pool-id/providers/"
+ "provider-id"
+ ),
+ # Pool ID with workforcePools string.
+ (
+ "//iam.googleapis.com/projects/123456/locations/"
+ "global/workloadIdentityPools/workforcePools/providers/"
+ "provider-id"
+ ),
+ # Unrealistic / incorrect workforce pool audiences.
+ "//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
+ "//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id",
+ "//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id",
+ "//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id",
+ "//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id",
+]
+
+
+class CredentialsImpl(external_account.Credentials):
+ def __init__(self, **kwargs):
+ super(CredentialsImpl, self).__init__(**kwargs)
+ self._counter = 0
+
+ def retrieve_subject_token(self, request):
+ counter = self._counter
+ self._counter += 1
+ return "subject_token_{}".format(counter)
+
+
+class TestCredentials(object):
+ TOKEN_URL = "https://sts.googleapis.com/v1/token"
+ TOKEN_INFO_URL = "https://sts.googleapis.com/v1/introspect"
+ PROJECT_NUMBER = "123456"
+ POOL_ID = "POOL_ID"
+ PROVIDER_ID = "PROVIDER_ID"
+ AUDIENCE = (
+ "//iam.googleapis.com/projects/{}"
+ "/locations/global/workloadIdentityPools/{}"
+ "/providers/{}"
+ ).format(PROJECT_NUMBER, POOL_ID, PROVIDER_ID)
+ WORKFORCE_AUDIENCE = (
+ "//iam.googleapis.com/locations/global/workforcePools/{}/providers/{}"
+ ).format(POOL_ID, PROVIDER_ID)
+ WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER"
+ SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
+ WORKFORCE_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token"
+ CREDENTIAL_SOURCE = {"file": "/var/run/secrets/goog.id/token"}
+ SUCCESS_RESPONSE = {
+ "access_token": "ACCESS_TOKEN",
+ "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "scope1 scope2",
+ }
+ ERROR_RESPONSE = {
+ "error": "invalid_request",
+ "error_description": "Invalid subject token",
+ "error_uri": "https://tools.ietf.org/html/rfc6749",
+ }
+ QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID"
+ SERVICE_ACCOUNT_IMPERSONATION_URL = (
+ "https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
+ + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
+ )
+ SCOPES = ["scope1", "scope2"]
+ IMPERSONATION_ERROR_RESPONSE = {
+ "error": {
+ "code": 400,
+ "message": "Request contains an invalid argument",
+ "status": "INVALID_ARGUMENT",
+ }
+ }
+ PROJECT_ID = "my-proj-id"
+ CLOUD_RESOURCE_MANAGER_URL = (
+ "https://cloudresourcemanager.googleapis.com/v1/projects/"
+ )
+ CLOUD_RESOURCE_MANAGER_SUCCESS_RESPONSE = {
+ "projectNumber": PROJECT_NUMBER,
+ "projectId": PROJECT_ID,
+ "lifecycleState": "ACTIVE",
+ "name": "project-name",
+ "createTime": "2018-11-06T04:42:54.109Z",
+ "parent": {"type": "folder", "id": "12345678901"},
+ }
+
+ @classmethod
+ def make_credentials(
+ cls,
+ client_id=None,
+ client_secret=None,
+ quota_project_id=None,
+ token_info_url=None,
+ scopes=None,
+ default_scopes=None,
+ service_account_impersonation_url=None,
+ service_account_impersonation_options={},
+ universe_domain=external_account._DEFAULT_UNIVERSE_DOMAIN,
+ ):
+ return CredentialsImpl(
+ audience=cls.AUDIENCE,
+ subject_token_type=cls.SUBJECT_TOKEN_TYPE,
+ token_url=cls.TOKEN_URL,
+ token_info_url=token_info_url,
+ service_account_impersonation_url=service_account_impersonation_url,
+ service_account_impersonation_options=service_account_impersonation_options,
+ credential_source=cls.CREDENTIAL_SOURCE,
+ client_id=client_id,
+ client_secret=client_secret,
+ quota_project_id=quota_project_id,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ universe_domain=universe_domain,
+ )
+
+ @classmethod
+ def make_workforce_pool_credentials(
+ cls,
+ client_id=None,
+ client_secret=None,
+ quota_project_id=None,
+ scopes=None,
+ default_scopes=None,
+ service_account_impersonation_url=None,
+ workforce_pool_user_project=None,
+ ):
+ return CredentialsImpl(
+ audience=cls.WORKFORCE_AUDIENCE,
+ subject_token_type=cls.WORKFORCE_SUBJECT_TOKEN_TYPE,
+ token_url=cls.TOKEN_URL,
+ service_account_impersonation_url=service_account_impersonation_url,
+ credential_source=cls.CREDENTIAL_SOURCE,
+ client_id=client_id,
+ client_secret=client_secret,
+ quota_project_id=quota_project_id,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ workforce_pool_user_project=workforce_pool_user_project,
+ )
+
+ @classmethod
+ def make_mock_request(
+ cls,
+ status=http_client.OK,
+ data=None,
+ impersonation_status=None,
+ impersonation_data=None,
+ cloud_resource_manager_status=None,
+ cloud_resource_manager_data=None,
+ ):
+ # STS token exchange request.
+ token_response = mock.create_autospec(transport.Response, instance=True)
+ token_response.status = status
+ token_response.data = json.dumps(data).encode("utf-8")
+ responses = [token_response]
+
+ # If service account impersonation is requested, mock the expected response.
+ if impersonation_status:
+ impersonation_response = mock.create_autospec(
+ transport.Response, instance=True
+ )
+ impersonation_response.status = impersonation_status
+ impersonation_response.data = json.dumps(impersonation_data).encode("utf-8")
+ responses.append(impersonation_response)
+
+ # If cloud resource manager is requested, mock the expected response.
+ if cloud_resource_manager_status:
+ cloud_resource_manager_response = mock.create_autospec(
+ transport.Response, instance=True
+ )
+ cloud_resource_manager_response.status = cloud_resource_manager_status
+ cloud_resource_manager_response.data = json.dumps(
+ cloud_resource_manager_data
+ ).encode("utf-8")
+ responses.append(cloud_resource_manager_response)
+
+ request = mock.create_autospec(transport.Request)
+ request.side_effect = responses
+
+ return request
+
+ @classmethod
+ def assert_token_request_kwargs(cls, request_kwargs, headers, request_data):
+ assert request_kwargs["url"] == cls.TOKEN_URL
+ assert request_kwargs["method"] == "POST"
+ assert request_kwargs["headers"] == headers
+ assert request_kwargs["body"] is not None
+ body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
+ for (k, v) in body_tuples:
+ assert v.decode("utf-8") == request_data[k.decode("utf-8")]
+ assert len(body_tuples) == len(request_data.keys())
+
+ @classmethod
+ def assert_impersonation_request_kwargs(cls, request_kwargs, headers, request_data):
+ assert request_kwargs["url"] == cls.SERVICE_ACCOUNT_IMPERSONATION_URL
+ assert request_kwargs["method"] == "POST"
+ assert request_kwargs["headers"] == headers
+ assert request_kwargs["body"] is not None
+ body_json = json.loads(request_kwargs["body"].decode("utf-8"))
+ assert body_json == request_data
+
+ @classmethod
+ def assert_resource_manager_request_kwargs(
+ cls, request_kwargs, project_number, headers
+ ):
+ assert request_kwargs["url"] == cls.CLOUD_RESOURCE_MANAGER_URL + project_number
+ assert request_kwargs["method"] == "GET"
+ assert request_kwargs["headers"] == headers
+ assert "body" not in request_kwargs
+
+ def test_default_state(self):
+ credentials = self.make_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL
+ )
+
+ # Token url and service account impersonation url should be set
+ assert credentials._token_url
+ assert credentials._service_account_impersonation_url
+ # Not token acquired yet
+ assert not credentials.token
+ assert not credentials.valid
+ # Expiration hasn't been set yet
+ assert not credentials.expiry
+ assert not credentials.expired
+ # Scopes are required
+ assert not credentials.scopes
+ assert credentials.requires_scopes
+ assert not credentials.quota_project_id
+ # Token info url not set yet
+ assert not credentials.token_info_url
+
+ def test_nonworkforce_with_workforce_pool_user_project(self):
+ with pytest.raises(ValueError) as excinfo:
+ CredentialsImpl(
+ audience=self.AUDIENCE,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ assert excinfo.match(
+ "workforce_pool_user_project should not be set for non-workforce "
+ "pool credentials"
+ )
+
+ def test_with_scopes(self):
+ credentials = self.make_credentials()
+
+ assert not credentials.scopes
+ assert credentials.requires_scopes
+
+ scoped_credentials = credentials.with_scopes(["email"])
+
+ assert scoped_credentials.has_scopes(["email"])
+ assert not scoped_credentials.requires_scopes
+
+ def test_with_scopes_workforce_pool(self):
+ credentials = self.make_workforce_pool_credentials(
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+ )
+
+ assert not credentials.scopes
+ assert credentials.requires_scopes
+
+ scoped_credentials = credentials.with_scopes(["email"])
+
+ assert scoped_credentials.has_scopes(["email"])
+ assert not scoped_credentials.requires_scopes
+ assert (
+ scoped_credentials.info.get("workforce_pool_user_project")
+ == self.WORKFORCE_POOL_USER_PROJECT
+ )
+
+ def test_with_scopes_using_user_and_default_scopes(self):
+ credentials = self.make_credentials()
+
+ assert not credentials.scopes
+ assert credentials.requires_scopes
+
+ scoped_credentials = credentials.with_scopes(
+ ["email"], default_scopes=["profile"]
+ )
+
+ assert scoped_credentials.has_scopes(["email"])
+ assert not scoped_credentials.has_scopes(["profile"])
+ assert not scoped_credentials.requires_scopes
+ assert scoped_credentials.scopes == ["email"]
+ assert scoped_credentials.default_scopes == ["profile"]
+
+ def test_with_scopes_using_default_scopes_only(self):
+ credentials = self.make_credentials()
+
+ assert not credentials.scopes
+ assert credentials.requires_scopes
+
+ scoped_credentials = credentials.with_scopes(None, default_scopes=["profile"])
+
+ assert scoped_credentials.has_scopes(["profile"])
+ assert not scoped_credentials.requires_scopes
+
+ def test_with_scopes_full_options_propagated(self):
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ quota_project_id=self.QUOTA_PROJECT_ID,
+ scopes=self.SCOPES,
+ token_info_url=self.TOKEN_INFO_URL,
+ default_scopes=["default1"],
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ )
+
+ with mock.patch.object(
+ external_account.Credentials, "__init__", return_value=None
+ ) as mock_init:
+ credentials.with_scopes(["email"], ["default2"])
+
+ # Confirm with_scopes initialized the credential with the expected
+ # parameters and scopes.
+ mock_init.assert_called_once_with(
+ audience=self.AUDIENCE,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ token_info_url=self.TOKEN_INFO_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ quota_project_id=self.QUOTA_PROJECT_ID,
+ scopes=["email"],
+ default_scopes=["default2"],
+ universe_domain=external_account._DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ def test_with_token_uri(self):
+ credentials = self.make_credentials()
+ new_token_uri = "https://eu-sts.googleapis.com/v1/token"
+
+ assert credentials._token_url == self.TOKEN_URL
+
+ creds_with_new_token_uri = credentials.with_token_uri(new_token_uri)
+
+ assert creds_with_new_token_uri._token_url == new_token_uri
+
+ def test_with_token_uri_workforce_pool(self):
+ credentials = self.make_workforce_pool_credentials(
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+ )
+
+ new_token_uri = "https://eu-sts.googleapis.com/v1/token"
+
+ assert credentials._token_url == self.TOKEN_URL
+
+ creds_with_new_token_uri = credentials.with_token_uri(new_token_uri)
+
+ assert creds_with_new_token_uri._token_url == new_token_uri
+ assert (
+ creds_with_new_token_uri.info.get("workforce_pool_user_project")
+ == self.WORKFORCE_POOL_USER_PROJECT
+ )
+
+ def test_with_quota_project(self):
+ credentials = self.make_credentials()
+
+ assert not credentials.scopes
+ assert not credentials.quota_project_id
+
+ quota_project_creds = credentials.with_quota_project("project-foo")
+
+ assert quota_project_creds.quota_project_id == "project-foo"
+
+ def test_with_quota_project_workforce_pool(self):
+ credentials = self.make_workforce_pool_credentials(
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+ )
+
+ assert not credentials.scopes
+ assert not credentials.quota_project_id
+
+ quota_project_creds = credentials.with_quota_project("project-foo")
+
+ assert quota_project_creds.quota_project_id == "project-foo"
+ assert (
+ quota_project_creds.info.get("workforce_pool_user_project")
+ == self.WORKFORCE_POOL_USER_PROJECT
+ )
+
+ def test_with_quota_project_full_options_propagated(self):
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ token_info_url=self.TOKEN_INFO_URL,
+ quota_project_id=self.QUOTA_PROJECT_ID,
+ scopes=self.SCOPES,
+ default_scopes=["default1"],
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ )
+
+ with mock.patch.object(
+ external_account.Credentials, "__init__", return_value=None
+ ) as mock_init:
+ credentials.with_quota_project("project-foo")
+
+ # Confirm with_quota_project initialized the credential with the
+ # expected parameters and quota project ID.
+ mock_init.assert_called_once_with(
+ audience=self.AUDIENCE,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ token_info_url=self.TOKEN_INFO_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ quota_project_id="project-foo",
+ scopes=self.SCOPES,
+ default_scopes=["default1"],
+ universe_domain=external_account._DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ def test_with_invalid_impersonation_target_principal(self):
+ invalid_url = "https://iamcredentials.googleapis.com/v1/invalid"
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ self.make_credentials(service_account_impersonation_url=invalid_url)
+
+ assert excinfo.match(
+ r"Unable to determine target principal from service account impersonation URL."
+ )
+
+ def test_info(self):
+ credentials = self.make_credentials(universe_domain="dummy_universe.com")
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": self.AUDIENCE,
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "token_url": self.TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE.copy(),
+ "universe_domain": "dummy_universe.com",
+ }
+
+ def test_universe_domain(self):
+ credentials = self.make_credentials(universe_domain="dummy_universe.com")
+ assert credentials.universe_domain == "dummy_universe.com"
+
+ credentials = self.make_credentials()
+ assert credentials.universe_domain == external_account._DEFAULT_UNIVERSE_DOMAIN
+
+ def test_info_workforce_pool(self):
+ credentials = self.make_workforce_pool_credentials(
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": self.WORKFORCE_AUDIENCE,
+ "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+ "token_url": self.TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE.copy(),
+ "workforce_pool_user_project": self.WORKFORCE_POOL_USER_PROJECT,
+ "universe_domain": external_account._DEFAULT_UNIVERSE_DOMAIN,
+ }
+
+ def test_info_with_full_options(self):
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ quota_project_id=self.QUOTA_PROJECT_ID,
+ token_info_url=self.TOKEN_INFO_URL,
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": self.AUDIENCE,
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "token_url": self.TOKEN_URL,
+ "token_info_url": self.TOKEN_INFO_URL,
+ "service_account_impersonation_url": self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "service_account_impersonation": {"token_lifetime_seconds": 2800},
+ "credential_source": self.CREDENTIAL_SOURCE.copy(),
+ "quota_project_id": self.QUOTA_PROJECT_ID,
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ "universe_domain": external_account._DEFAULT_UNIVERSE_DOMAIN,
+ }
+
+ def test_service_account_email_without_impersonation(self):
+ credentials = self.make_credentials()
+
+ assert credentials.service_account_email is None
+
+ def test_service_account_email_with_impersonation(self):
+ credentials = self.make_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL
+ )
+
+ assert credentials.service_account_email == SERVICE_ACCOUNT_EMAIL
+
+ @pytest.mark.parametrize("audience", TEST_NON_USER_AUDIENCES)
+ def test_is_user_with_non_users(self, audience):
+ credentials = CredentialsImpl(
+ audience=audience,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ )
+
+ assert credentials.is_user is False
+
+ @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES)
+ def test_is_user_with_users(self, audience):
+ credentials = CredentialsImpl(
+ audience=audience,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ )
+
+ assert credentials.is_user is True
+
+ @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES)
+ def test_is_user_with_users_and_impersonation(self, audience):
+ # Initialize the credentials with service account impersonation.
+ credentials = CredentialsImpl(
+ audience=audience,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ )
+
+ # Even though the audience is for a workforce pool, since service account
+ # impersonation is used, the credentials will represent a service account and
+ # not a user.
+ assert credentials.is_user is False
+
+ @pytest.mark.parametrize("audience", TEST_NON_USER_AUDIENCES)
+ def test_is_workforce_pool_with_non_users(self, audience):
+ credentials = CredentialsImpl(
+ audience=audience,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ )
+
+ assert credentials.is_workforce_pool is False
+
+ @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES)
+ def test_is_workforce_pool_with_users(self, audience):
+ credentials = CredentialsImpl(
+ audience=audience,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ )
+
+ assert credentials.is_workforce_pool is True
+
+ @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES)
+ def test_is_workforce_pool_with_users_and_impersonation(self, audience):
+ # Initialize the credentials with workforce audience and service account
+ # impersonation.
+ credentials = CredentialsImpl(
+ audience=audience,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ )
+
+ # Even though impersonation is used, is_workforce_pool should still return True.
+ assert credentials.is_workforce_pool is True
+
+ @pytest.mark.parametrize("mock_expires_in", [2800, "2800"])
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh_without_client_auth_success(
+ self, unused_utcnow, mock_auth_lib_value, mock_expires_in
+ ):
+ response = self.SUCCESS_RESPONSE.copy()
+ # Test custom expiration to confirm expiry is set correctly.
+ response["expires_in"] = mock_expires_in
+ expected_expiry = datetime.datetime.min + datetime.timedelta(
+ seconds=int(mock_expires_in)
+ )
+ headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false",
+ }
+ request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ }
+ request = self.make_mock_request(status=http_client.OK, data=response)
+ credentials = self.make_credentials()
+
+ credentials.refresh(request)
+
+ self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == response["access_token"]
+
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh_workforce_without_client_auth_success(
+ self, unused_utcnow, test_auth_lib_value
+ ):
+ response = self.SUCCESS_RESPONSE.copy()
+ # Test custom expiration to confirm expiry is set correctly.
+ response["expires_in"] = 2800
+ expected_expiry = datetime.datetime.min + datetime.timedelta(
+ seconds=response["expires_in"]
+ )
+ headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false",
+ }
+ request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.WORKFORCE_AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+ "options": urllib.parse.quote(
+ json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT})
+ ),
+ }
+ request = self.make_mock_request(status=http_client.OK, data=response)
+ credentials = self.make_workforce_pool_credentials(
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+ )
+
+ credentials.refresh(request)
+
+ self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == response["access_token"]
+
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh_workforce_with_client_auth_success(
+ self, unused_utcnow, mock_auth_lib_value
+ ):
+ response = self.SUCCESS_RESPONSE.copy()
+ # Test custom expiration to confirm expiry is set correctly.
+ response["expires_in"] = 2800
+ expected_expiry = datetime.datetime.min + datetime.timedelta(
+ seconds=response["expires_in"]
+ )
+ headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false",
+ }
+ request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.WORKFORCE_AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+ }
+ request = self.make_mock_request(status=http_client.OK, data=response)
+ # Client Auth will have higher priority over workforce_pool_user_project.
+ credentials = self.make_workforce_pool_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ credentials.refresh(request)
+
+ self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == response["access_token"]
+
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+ def test_refresh_workforce_with_client_auth_and_no_workforce_project_success(
+ self, unused_utcnow, mock_lib_version_value
+ ):
+ response = self.SUCCESS_RESPONSE.copy()
+ # Test custom expiration to confirm expiry is set correctly.
+ response["expires_in"] = 2800
+ expected_expiry = datetime.datetime.min + datetime.timedelta(
+ seconds=response["expires_in"]
+ )
+ headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false",
+ }
+ request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.WORKFORCE_AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+ }
+ request = self.make_mock_request(status=http_client.OK, data=response)
+ # Client Auth will be sufficient for user project determination.
+ credentials = self.make_workforce_pool_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ workforce_pool_user_project=None,
+ )
+
+ credentials.refresh(request)
+
+ self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == response["access_token"]
+
+ @mock.patch(
+ "google.auth.metrics.token_request_access_token_impersonate",
+ return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ )
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ def test_refresh_impersonation_without_client_auth_success(
+ self, mock_metrics_header_value, mock_auth_lib_value
+ ):
+ # Simulate service account access token expires in 2800 seconds.
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
+ ).isoformat("T") + "Z"
+ expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+ # STS token exchange request/response.
+ token_response = self.SUCCESS_RESPONSE.copy()
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "scope": "https://www.googleapis.com/auth/iam",
+ }
+ # Service account impersonation request/response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ impersonation_headers = {
+ "Content-Type": "application/json",
+ "authorization": "Bearer {}".format(token_response["access_token"]),
+ "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ "x-identity-trust-boundary": "0",
+ }
+ impersonation_request_data = {
+ "delegates": None,
+ "scope": self.SCOPES,
+ "lifetime": "3600s",
+ }
+ # Initialize mock request to handle token exchange and service account
+ # impersonation request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=token_response,
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ # Initialize credentials with service account impersonation.
+ credentials = self.make_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=self.SCOPES,
+ )
+
+ credentials.refresh(request)
+
+ # Only 2 requests should be processed.
+ assert len(request.call_args_list) == 2
+ # Verify token exchange request parameters.
+ self.assert_token_request_kwargs(
+ request.call_args_list[0][1], token_headers, token_request_data
+ )
+ # Verify service account impersonation request parameters.
+ self.assert_impersonation_request_kwargs(
+ request.call_args_list[1][1],
+ impersonation_headers,
+ impersonation_request_data,
+ )
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == impersonation_response["accessToken"]
+
+ @mock.patch(
+ "google.auth.metrics.token_request_access_token_impersonate",
+ return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ )
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ def test_refresh_workforce_impersonation_without_client_auth_success(
+ self, mock_metrics_header_value, mock_auth_lib_value
+ ):
+ # Simulate service account access token expires in 2800 seconds.
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
+ ).isoformat("T") + "Z"
+ expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+ # STS token exchange request/response.
+ token_response = self.SUCCESS_RESPONSE.copy()
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.WORKFORCE_AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+ "scope": "https://www.googleapis.com/auth/iam",
+ "options": urllib.parse.quote(
+ json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT})
+ ),
+ }
+ # Service account impersonation request/response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ impersonation_headers = {
+ "Content-Type": "application/json",
+ "authorization": "Bearer {}".format(token_response["access_token"]),
+ "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ "x-identity-trust-boundary": "0",
+ }
+ impersonation_request_data = {
+ "delegates": None,
+ "scope": self.SCOPES,
+ "lifetime": "3600s",
+ }
+ # Initialize mock request to handle token exchange and service account
+ # impersonation request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=token_response,
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ # Initialize credentials with service account impersonation.
+ credentials = self.make_workforce_pool_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=self.SCOPES,
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ credentials.refresh(request)
+
+ # Only 2 requests should be processed.
+ assert len(request.call_args_list) == 2
+ # Verify token exchange request parameters.
+ self.assert_token_request_kwargs(
+ request.call_args_list[0][1], token_headers, token_request_data
+ )
+ # Verify service account impersonation request parameters.
+ self.assert_impersonation_request_kwargs(
+ request.call_args_list[1][1],
+ impersonation_headers,
+ impersonation_request_data,
+ )
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == impersonation_response["accessToken"]
+
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ def test_refresh_without_client_auth_success_explicit_user_scopes_ignore_default_scopes(
+ self, mock_auth_lib_value
+ ):
+ headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false",
+ }
+ request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "scope": "scope1 scope2",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ }
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+ credentials = self.make_credentials(
+ scopes=["scope1", "scope2"],
+ # Default scopes will be ignored in favor of user scopes.
+ default_scopes=["ignored"],
+ )
+
+ credentials.refresh(request)
+
+ self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+ assert credentials.valid
+ assert not credentials.expired
+ assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+ assert credentials.has_scopes(["scope1", "scope2"])
+ assert not credentials.has_scopes(["ignored"])
+
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ def test_refresh_without_client_auth_success_explicit_default_scopes_only(
+ self, mock_auth_lib_value
+ ):
+ headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false",
+ }
+ request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "scope": "scope1 scope2",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ }
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+ credentials = self.make_credentials(
+ scopes=None,
+ # Default scopes will be used since user scopes are none.
+ default_scopes=["scope1", "scope2"],
+ )
+
+ credentials.refresh(request)
+
+ self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+ assert credentials.valid
+ assert not credentials.expired
+ assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+ assert credentials.has_scopes(["scope1", "scope2"])
+
+ def test_refresh_without_client_auth_error(self):
+ request = self.make_mock_request(
+ status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE
+ )
+ credentials = self.make_credentials()
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ credentials.refresh(request)
+
+ assert excinfo.match(
+ r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+ )
+ assert not credentials.expired
+ assert credentials.token is None
+
+ def test_refresh_impersonation_without_client_auth_error(self):
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=self.SUCCESS_RESPONSE,
+ impersonation_status=http_client.BAD_REQUEST,
+ impersonation_data=self.IMPERSONATION_ERROR_RESPONSE,
+ )
+ credentials = self.make_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=self.SCOPES,
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.refresh(request)
+
+ assert excinfo.match(r"Unable to acquire impersonated credentials")
+ assert not credentials.expired
+ assert credentials.token is None
+
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ def test_refresh_with_client_auth_success(self, mock_auth_lib_value):
+ headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false",
+ }
+ request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ }
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID, client_secret=CLIENT_SECRET
+ )
+
+ credentials.refresh(request)
+
+ self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+ assert credentials.valid
+ assert not credentials.expired
+ assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+
+ @mock.patch(
+ "google.auth.metrics.token_request_access_token_impersonate",
+ return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ )
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ def test_refresh_impersonation_with_client_auth_success_ignore_default_scopes(
+ self, mock_metrics_header_value, mock_auth_lib_value
+ ):
+ # Simulate service account access token expires in 2800 seconds.
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
+ ).isoformat("T") + "Z"
+ expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+ # STS token exchange request/response.
+ token_response = self.SUCCESS_RESPONSE.copy()
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "scope": "https://www.googleapis.com/auth/iam",
+ }
+ # Service account impersonation request/response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ impersonation_headers = {
+ "Content-Type": "application/json",
+ "authorization": "Bearer {}".format(token_response["access_token"]),
+ "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ "x-identity-trust-boundary": "0",
+ }
+ impersonation_request_data = {
+ "delegates": None,
+ "scope": self.SCOPES,
+ "lifetime": "3600s",
+ }
+ # Initialize mock request to handle token exchange and service account
+ # impersonation request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=token_response,
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ # Initialize credentials with service account impersonation and basic auth.
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=self.SCOPES,
+ # Default scopes will be ignored since user scopes are specified.
+ default_scopes=["ignored"],
+ )
+
+ credentials.refresh(request)
+
+ # Only 2 requests should be processed.
+ assert len(request.call_args_list) == 2
+ # Verify token exchange request parameters.
+ self.assert_token_request_kwargs(
+ request.call_args_list[0][1], token_headers, token_request_data
+ )
+ # Verify service account impersonation request parameters.
+ self.assert_impersonation_request_kwargs(
+ request.call_args_list[1][1],
+ impersonation_headers,
+ impersonation_request_data,
+ )
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == impersonation_response["accessToken"]
+
+ @mock.patch(
+ "google.auth.metrics.token_request_access_token_impersonate",
+ return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ )
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ def test_refresh_impersonation_with_client_auth_success_use_default_scopes(
+ self, mock_metrics_header_value, mock_auth_lib_value
+ ):
+ # Simulate service account access token expires in 2800 seconds.
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
+ ).isoformat("T") + "Z"
+ expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+ # STS token exchange request/response.
+ token_response = self.SUCCESS_RESPONSE.copy()
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "scope": "https://www.googleapis.com/auth/iam",
+ }
+ # Service account impersonation request/response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ impersonation_headers = {
+ "Content-Type": "application/json",
+ "authorization": "Bearer {}".format(token_response["access_token"]),
+ "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ "x-identity-trust-boundary": "0",
+ }
+ impersonation_request_data = {
+ "delegates": None,
+ "scope": self.SCOPES,
+ "lifetime": "3600s",
+ }
+ # Initialize mock request to handle token exchange and service account
+ # impersonation request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=token_response,
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ # Initialize credentials with service account impersonation and basic auth.
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=None,
+ # Default scopes will be used since user specified scopes are none.
+ default_scopes=self.SCOPES,
+ )
+
+ credentials.refresh(request)
+
+ # Only 2 requests should be processed.
+ assert len(request.call_args_list) == 2
+ # Verify token exchange request parameters.
+ self.assert_token_request_kwargs(
+ request.call_args_list[0][1], token_headers, token_request_data
+ )
+ # Verify service account impersonation request parameters.
+ self.assert_impersonation_request_kwargs(
+ request.call_args_list[1][1],
+ impersonation_headers,
+ impersonation_request_data,
+ )
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == impersonation_response["accessToken"]
+
+ def test_apply_without_quota_project_id(self):
+ headers = {}
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+ credentials = self.make_credentials()
+
+ credentials.refresh(request)
+ credentials.apply(headers)
+
+ assert headers == {
+ "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ def test_apply_workforce_without_quota_project_id(self):
+ headers = {}
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+ credentials = self.make_workforce_pool_credentials(
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+ )
+
+ credentials.refresh(request)
+ credentials.apply(headers)
+
+ assert headers == {
+ "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ def test_apply_impersonation_without_quota_project_id(self):
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+ ).isoformat("T") + "Z"
+ # Service account impersonation response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ # Initialize mock request to handle token exchange and service account
+ # impersonation request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=self.SUCCESS_RESPONSE.copy(),
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ # Initialize credentials with service account impersonation.
+ credentials = self.make_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=self.SCOPES,
+ )
+ headers = {}
+
+ credentials.refresh(request)
+ credentials.apply(headers)
+
+ assert headers == {
+ "authorization": "Bearer {}".format(impersonation_response["accessToken"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ def test_apply_with_quota_project_id(self):
+ headers = {"other": "header-value"}
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+ credentials = self.make_credentials(quota_project_id=self.QUOTA_PROJECT_ID)
+
+ credentials.refresh(request)
+ credentials.apply(headers)
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+ "x-goog-user-project": self.QUOTA_PROJECT_ID,
+ "x-identity-trust-boundary": "0",
+ }
+
+ def test_apply_impersonation_with_quota_project_id(self):
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+ ).isoformat("T") + "Z"
+ # Service account impersonation response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ # Initialize mock request to handle token exchange and service account
+ # impersonation request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=self.SUCCESS_RESPONSE.copy(),
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ # Initialize credentials with service account impersonation.
+ credentials = self.make_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=self.SCOPES,
+ quota_project_id=self.QUOTA_PROJECT_ID,
+ )
+ headers = {"other": "header-value"}
+
+ credentials.refresh(request)
+ credentials.apply(headers)
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(impersonation_response["accessToken"]),
+ "x-goog-user-project": self.QUOTA_PROJECT_ID,
+ "x-identity-trust-boundary": "0",
+ }
+
+ def test_before_request(self):
+ headers = {"other": "header-value"}
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+ credentials = self.make_credentials()
+
+ # First call should call refresh, setting the token.
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ # Second call shouldn't call refresh.
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ def test_before_request_workforce(self):
+ headers = {"other": "header-value"}
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+ credentials = self.make_workforce_pool_credentials(
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+ )
+
+ # First call should call refresh, setting the token.
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ # Second call shouldn't call refresh.
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ def test_before_request_impersonation(self):
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+ ).isoformat("T") + "Z"
+ # Service account impersonation response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ # Initialize mock request to handle token exchange and service account
+ # impersonation request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=self.SUCCESS_RESPONSE.copy(),
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ headers = {"other": "header-value"}
+ credentials = self.make_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL
+ )
+
+ # First call should call refresh, setting the token.
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(impersonation_response["accessToken"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ # Second call shouldn't call refresh.
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ assert headers == {
+ "other": "header-value",
+ "authorization": "Bearer {}".format(impersonation_response["accessToken"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_before_request_expired(self, utcnow):
+ headers = {}
+ request = self.make_mock_request(
+ status=http_client.OK, data=self.SUCCESS_RESPONSE
+ )
+ credentials = self.make_credentials()
+ credentials.token = "token"
+ utcnow.return_value = datetime.datetime.min
+ # Set the expiration to one second more than now plus the clock skew
+ # accomodation. These credentials should be valid.
+ credentials.expiry = (
+ datetime.datetime.min
+ + _helpers.REFRESH_THRESHOLD
+ + datetime.timedelta(seconds=1)
+ )
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ # Cached token should be used.
+ assert headers == {
+ "authorization": "Bearer token",
+ "x-identity-trust-boundary": "0",
+ }
+
+ # Next call should simulate 1 second passed.
+ utcnow.return_value = datetime.datetime.min + datetime.timedelta(seconds=1)
+
+ assert not credentials.valid
+ assert credentials.expired
+
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ # New token should be retrieved.
+ assert headers == {
+ "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ @mock.patch("google.auth._helpers.utcnow")
+ def test_before_request_impersonation_expired(self, utcnow):
+ headers = {}
+ expire_time = (
+ datetime.datetime.min + datetime.timedelta(seconds=3601)
+ ).isoformat("T") + "Z"
+ # Service account impersonation response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ # Initialize mock request to handle token exchange and service account
+ # impersonation request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=self.SUCCESS_RESPONSE.copy(),
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ credentials = self.make_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL
+ )
+ credentials.token = "token"
+ utcnow.return_value = datetime.datetime.min
+ # Set the expiration to one second more than now plus the clock skew
+ # accomodation. These credentials should be valid.
+ credentials.expiry = (
+ datetime.datetime.min
+ + _helpers.REFRESH_THRESHOLD
+ + datetime.timedelta(seconds=1)
+ )
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ # Cached token should be used.
+ assert headers == {
+ "authorization": "Bearer token",
+ "x-identity-trust-boundary": "0",
+ }
+
+ # Next call should simulate 1 second passed. This will trigger the expiration
+ # threshold.
+ utcnow.return_value = datetime.datetime.min + datetime.timedelta(seconds=1)
+
+ assert not credentials.valid
+ assert credentials.expired
+
+ credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+ # New token should be retrieved.
+ assert headers == {
+ "authorization": "Bearer {}".format(impersonation_response["accessToken"]),
+ "x-identity-trust-boundary": "0",
+ }
+
+ @pytest.mark.parametrize(
+ "audience",
+ [
+ # Legacy K8s audience format.
+ "identitynamespace:1f12345:my_provider",
+ # Unrealistic audiences.
+ "//iam.googleapis.com/projects",
+ "//iam.googleapis.com/projects/",
+ "//iam.googleapis.com/project/123456",
+ "//iam.googleapis.com/projects//123456",
+ "//iam.googleapis.com/prefix_projects/123456",
+ "//iam.googleapis.com/projects_suffix/123456",
+ ],
+ )
+ def test_project_number_indeterminable(self, audience):
+ credentials = CredentialsImpl(
+ audience=audience,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ )
+
+ assert credentials.project_number is None
+ assert credentials.get_project_id(None) is None
+
+ def test_project_number_determinable(self):
+ credentials = CredentialsImpl(
+ audience=self.AUDIENCE,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ )
+
+ assert credentials.project_number == self.PROJECT_NUMBER
+
+ def test_project_number_workforce(self):
+ credentials = CredentialsImpl(
+ audience=self.WORKFORCE_AUDIENCE,
+ subject_token_type=self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ assert credentials.project_number is None
+
+ def test_project_id_without_scopes(self):
+ # Initialize credentials with no scopes.
+ credentials = CredentialsImpl(
+ audience=self.AUDIENCE,
+ subject_token_type=self.SUBJECT_TOKEN_TYPE,
+ token_url=self.TOKEN_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ )
+
+ assert credentials.get_project_id(None) is None
+
+ @mock.patch(
+ "google.auth.metrics.token_request_access_token_impersonate",
+ return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ )
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ def test_get_project_id_cloud_resource_manager_success(
+ self, mock_metrics_header_value, mock_auth_lib_value
+ ):
+ # STS token exchange request/response.
+ token_response = self.SUCCESS_RESPONSE.copy()
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "scope": "https://www.googleapis.com/auth/iam",
+ }
+ # Service account impersonation request/response.
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+ ).isoformat("T") + "Z"
+ expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ impersonation_headers = {
+ "Content-Type": "application/json",
+ "x-goog-user-project": self.QUOTA_PROJECT_ID,
+ "authorization": "Bearer {}".format(token_response["access_token"]),
+ "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ "x-identity-trust-boundary": "0",
+ }
+ impersonation_request_data = {
+ "delegates": None,
+ "scope": self.SCOPES,
+ "lifetime": "3600s",
+ }
+ # Initialize mock request to handle token exchange, service account
+ # impersonation and cloud resource manager request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=self.SUCCESS_RESPONSE.copy(),
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ cloud_resource_manager_status=http_client.OK,
+ cloud_resource_manager_data=self.CLOUD_RESOURCE_MANAGER_SUCCESS_RESPONSE,
+ )
+ credentials = self.make_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=self.SCOPES,
+ quota_project_id=self.QUOTA_PROJECT_ID,
+ )
+
+ # Expected project ID from cloud resource manager response should be returned.
+ project_id = credentials.get_project_id(request)
+
+ assert project_id == self.PROJECT_ID
+ # 3 requests should be processed.
+ assert len(request.call_args_list) == 3
+ # Verify token exchange request parameters.
+ self.assert_token_request_kwargs(
+ request.call_args_list[0][1], token_headers, token_request_data
+ )
+ # Verify service account impersonation request parameters.
+ self.assert_impersonation_request_kwargs(
+ request.call_args_list[1][1],
+ impersonation_headers,
+ impersonation_request_data,
+ )
+ # In the process of getting project ID, an access token should be
+ # retrieved.
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == impersonation_response["accessToken"]
+ # Verify cloud resource manager request parameters.
+ self.assert_resource_manager_request_kwargs(
+ request.call_args_list[2][1],
+ self.PROJECT_NUMBER,
+ {
+ "x-goog-user-project": self.QUOTA_PROJECT_ID,
+ "authorization": "Bearer {}".format(
+ impersonation_response["accessToken"]
+ ),
+ "x-identity-trust-boundary": "0",
+ },
+ )
+
+ # Calling get_project_id again should return the cached project_id.
+ project_id = credentials.get_project_id(request)
+
+ assert project_id == self.PROJECT_ID
+ # No additional requests.
+ assert len(request.call_args_list) == 3
+
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ def test_workforce_pool_get_project_id_cloud_resource_manager_success(
+ self, mock_auth_lib_value
+ ):
+ # STS token exchange request/response.
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.WORKFORCE_AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+ "scope": "scope1 scope2",
+ "options": urllib.parse.quote(
+ json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT})
+ ),
+ }
+ # Initialize mock request to handle token exchange and cloud resource
+ # manager request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=self.SUCCESS_RESPONSE.copy(),
+ cloud_resource_manager_status=http_client.OK,
+ cloud_resource_manager_data=self.CLOUD_RESOURCE_MANAGER_SUCCESS_RESPONSE,
+ )
+ credentials = self.make_workforce_pool_credentials(
+ scopes=self.SCOPES,
+ quota_project_id=self.QUOTA_PROJECT_ID,
+ workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ # Expected project ID from cloud resource manager response should be returned.
+ project_id = credentials.get_project_id(request)
+
+ assert project_id == self.PROJECT_ID
+ # 2 requests should be processed.
+ assert len(request.call_args_list) == 2
+ # Verify token exchange request parameters.
+ self.assert_token_request_kwargs(
+ request.call_args_list[0][1], token_headers, token_request_data
+ )
+ # In the process of getting project ID, an access token should be
+ # retrieved.
+ assert credentials.valid
+ assert not credentials.expired
+ assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+ # Verify cloud resource manager request parameters.
+ self.assert_resource_manager_request_kwargs(
+ request.call_args_list[1][1],
+ self.WORKFORCE_POOL_USER_PROJECT,
+ {
+ "x-goog-user-project": self.QUOTA_PROJECT_ID,
+ "authorization": "Bearer {}".format(
+ self.SUCCESS_RESPONSE["access_token"]
+ ),
+ "x-identity-trust-boundary": "0",
+ },
+ )
+
+ # Calling get_project_id again should return the cached project_id.
+ project_id = credentials.get_project_id(request)
+
+ assert project_id == self.PROJECT_ID
+ # No additional requests.
+ assert len(request.call_args_list) == 2
+
+ @mock.patch(
+ "google.auth.metrics.token_request_access_token_impersonate",
+ return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ )
+ @mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
+ )
+ def test_refresh_impersonation_with_lifetime(
+ self, mock_metrics_header_value, mock_auth_lib_value
+ ):
+ # Simulate service account access token expires in 2800 seconds.
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
+ ).isoformat("T") + "Z"
+ expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+ # STS token exchange request/response.
+ token_response = self.SUCCESS_RESPONSE.copy()
+ token_headers = {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/true",
+ }
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": self.AUDIENCE,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "subject_token": "subject_token_0",
+ "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+ "scope": "https://www.googleapis.com/auth/iam",
+ }
+ # Service account impersonation request/response.
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ impersonation_headers = {
+ "Content-Type": "application/json",
+ "authorization": "Bearer {}".format(token_response["access_token"]),
+ "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ "x-identity-trust-boundary": "0",
+ }
+ impersonation_request_data = {
+ "delegates": None,
+ "scope": self.SCOPES,
+ "lifetime": "2800s",
+ }
+ # Initialize mock request to handle token exchange and service account
+ # impersonation request.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=token_response,
+ impersonation_status=http_client.OK,
+ impersonation_data=impersonation_response,
+ )
+ # Initialize credentials with service account impersonation.
+ credentials = self.make_credentials(
+ service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ scopes=self.SCOPES,
+ )
+
+ credentials.refresh(request)
+
+ # Only 2 requests should be processed.
+ assert len(request.call_args_list) == 2
+ # Verify token exchange request parameters.
+ self.assert_token_request_kwargs(
+ request.call_args_list[0][1], token_headers, token_request_data
+ )
+ # Verify service account impersonation request parameters.
+ self.assert_impersonation_request_kwargs(
+ request.call_args_list[1][1],
+ impersonation_headers,
+ impersonation_request_data,
+ )
+ assert credentials.valid
+ assert credentials.expiry == expected_expiry
+ assert not credentials.expired
+ assert credentials.token == impersonation_response["accessToken"]
+
+ def test_get_project_id_cloud_resource_manager_error(self):
+ # Simulate resource doesn't have sufficient permissions to access
+ # cloud resource manager.
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data=self.SUCCESS_RESPONSE.copy(),
+ cloud_resource_manager_status=http_client.UNAUTHORIZED,
+ )
+ credentials = self.make_credentials(scopes=self.SCOPES)
+
+ project_id = credentials.get_project_id(request)
+
+ assert project_id is None
+ # Only 2 requests to STS and cloud resource manager should be sent.
+ assert len(request.call_args_list) == 2
diff --git a/contrib/python/google-auth/py3/tests/test_external_account_authorized_user.py b/contrib/python/google-auth/py3/tests/test_external_account_authorized_user.py
new file mode 100644
index 0000000000..7ffd5078c8
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_external_account_authorized_user.py
@@ -0,0 +1,512 @@
+# Copyright 2022 Google LLC
+#
+# 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 datetime
+import http.client as http_client
+import json
+
+import mock
+import pytest # type: ignore
+
+from google.auth import exceptions
+from google.auth import external_account_authorized_user
+from google.auth import transport
+
+TOKEN_URL = "https://sts.googleapis.com/v1/token"
+TOKEN_INFO_URL = "https://sts.googleapis.com/v1/introspect"
+REVOKE_URL = "https://sts.googleapis.com/v1/revoke"
+PROJECT_NUMBER = "123456"
+QUOTA_PROJECT_ID = "654321"
+POOL_ID = "POOL_ID"
+PROVIDER_ID = "PROVIDER_ID"
+AUDIENCE = (
+ "//iam.googleapis.com/projects/{}"
+ "/locations/global/workloadIdentityPools/{}"
+ "/providers/{}"
+).format(PROJECT_NUMBER, POOL_ID, PROVIDER_ID)
+REFRESH_TOKEN = "REFRESH_TOKEN"
+NEW_REFRESH_TOKEN = "NEW_REFRESH_TOKEN"
+ACCESS_TOKEN = "ACCESS_TOKEN"
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password".
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+SCOPES = ["email", "profile"]
+NOW = datetime.datetime(1990, 8, 27, 6, 54, 30)
+
+
+class TestCredentials(object):
+ @classmethod
+ def make_credentials(
+ cls,
+ audience=AUDIENCE,
+ refresh_token=REFRESH_TOKEN,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ **kwargs
+ ):
+ return external_account_authorized_user.Credentials(
+ audience=audience,
+ refresh_token=refresh_token,
+ token_url=token_url,
+ token_info_url=token_info_url,
+ client_id=client_id,
+ client_secret=client_secret,
+ **kwargs
+ )
+
+ @classmethod
+ def make_mock_request(cls, status=http_client.OK, data=None):
+ # STS token exchange request.
+ token_response = mock.create_autospec(transport.Response, instance=True)
+ token_response.status = status
+ token_response.data = json.dumps(data).encode("utf-8")
+ responses = [token_response]
+
+ request = mock.create_autospec(transport.Request)
+ request.side_effect = responses
+
+ return request
+
+ def test_default_state(self):
+ creds = self.make_credentials()
+
+ assert not creds.expiry
+ assert not creds.expired
+ assert not creds.token
+ assert not creds.valid
+ assert not creds.requires_scopes
+ assert not creds.scopes
+ assert not creds.revoke_url
+ assert creds.token_info_url
+ assert creds.client_id
+ assert creds.client_secret
+ assert creds.is_user
+ assert creds.refresh_token == REFRESH_TOKEN
+ assert creds.audience == AUDIENCE
+ assert creds.token_url == TOKEN_URL
+
+ def test_basic_create(self):
+ creds = external_account_authorized_user.Credentials(
+ token=ACCESS_TOKEN,
+ expiry=datetime.datetime.max,
+ scopes=SCOPES,
+ revoke_url=REVOKE_URL,
+ )
+
+ assert creds.expiry == datetime.datetime.max
+ assert not creds.expired
+ assert creds.token == ACCESS_TOKEN
+ assert creds.valid
+ assert not creds.requires_scopes
+ assert creds.scopes == SCOPES
+ assert creds.is_user
+ assert creds.revoke_url == REVOKE_URL
+
+ def test_stunted_create_no_refresh_token(self):
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(token=None, refresh_token=None)
+
+ assert excinfo.match(
+ r"Token should be created with fields to make it valid \(`token` and "
+ r"`expiry`\), or fields to allow it to refresh \(`refresh_token`, "
+ r"`token_url`, `client_id`, `client_secret`\)\."
+ )
+
+ def test_stunted_create_no_token_url(self):
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(token=None, token_url=None)
+
+ assert excinfo.match(
+ r"Token should be created with fields to make it valid \(`token` and "
+ r"`expiry`\), or fields to allow it to refresh \(`refresh_token`, "
+ r"`token_url`, `client_id`, `client_secret`\)\."
+ )
+
+ def test_stunted_create_no_client_id(self):
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(token=None, client_id=None)
+
+ assert excinfo.match(
+ r"Token should be created with fields to make it valid \(`token` and "
+ r"`expiry`\), or fields to allow it to refresh \(`refresh_token`, "
+ r"`token_url`, `client_id`, `client_secret`\)\."
+ )
+
+ def test_stunted_create_no_client_secret(self):
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(token=None, client_secret=None)
+
+ assert excinfo.match(
+ r"Token should be created with fields to make it valid \(`token` and "
+ r"`expiry`\), or fields to allow it to refresh \(`refresh_token`, "
+ r"`token_url`, `client_id`, `client_secret`\)\."
+ )
+
+ @mock.patch("google.auth._helpers.utcnow", return_value=NOW)
+ def test_refresh_auth_success(self, utcnow):
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data={"access_token": ACCESS_TOKEN, "expires_in": 3600},
+ )
+ creds = self.make_credentials()
+
+ creds.refresh(request)
+
+ assert creds.expiry == utcnow() + datetime.timedelta(seconds=3600)
+ assert not creds.expired
+ assert creds.token == ACCESS_TOKEN
+ assert creds.valid
+ assert not creds.requires_scopes
+ assert creds.is_user
+ assert creds._refresh_token == REFRESH_TOKEN
+
+ request.assert_called_once_with(
+ url=TOKEN_URL,
+ method="POST",
+ headers={
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+ },
+ body=("grant_type=refresh_token&refresh_token=" + REFRESH_TOKEN).encode(
+ "UTF-8"
+ ),
+ )
+
+ @mock.patch("google.auth._helpers.utcnow", return_value=NOW)
+ def test_refresh_auth_success_new_refresh_token(self, utcnow):
+ request = self.make_mock_request(
+ status=http_client.OK,
+ data={
+ "access_token": ACCESS_TOKEN,
+ "expires_in": 3600,
+ "refresh_token": NEW_REFRESH_TOKEN,
+ },
+ )
+ creds = self.make_credentials()
+
+ creds.refresh(request)
+
+ assert creds.expiry == utcnow() + datetime.timedelta(seconds=3600)
+ assert not creds.expired
+ assert creds.token == ACCESS_TOKEN
+ assert creds.valid
+ assert not creds.requires_scopes
+ assert creds.is_user
+ assert creds._refresh_token == NEW_REFRESH_TOKEN
+
+ request.assert_called_once_with(
+ url=TOKEN_URL,
+ method="POST",
+ headers={
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+ },
+ body=("grant_type=refresh_token&refresh_token=" + REFRESH_TOKEN).encode(
+ "UTF-8"
+ ),
+ )
+
+ def test_refresh_auth_failure(self):
+ request = self.make_mock_request(
+ status=http_client.BAD_REQUEST,
+ data={
+ "error": "invalid_request",
+ "error_description": "Invalid subject token",
+ "error_uri": "https://tools.ietf.org/html/rfc6749",
+ },
+ )
+ creds = self.make_credentials()
+
+ with pytest.raises(exceptions.OAuthError) as excinfo:
+ creds.refresh(request)
+
+ assert excinfo.match(
+ r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+ )
+
+ assert not creds.expiry
+ assert not creds.expired
+ assert not creds.token
+ assert not creds.valid
+ assert not creds.requires_scopes
+ assert creds.is_user
+
+ request.assert_called_once_with(
+ url=TOKEN_URL,
+ method="POST",
+ headers={
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+ },
+ body=("grant_type=refresh_token&refresh_token=" + REFRESH_TOKEN).encode(
+ "UTF-8"
+ ),
+ )
+
+ def test_refresh_without_refresh_token(self):
+ request = self.make_mock_request()
+ creds = self.make_credentials(refresh_token=None, token=ACCESS_TOKEN)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ creds.refresh(request)
+
+ assert excinfo.match(
+ r"The credentials do not contain the necessary fields need to refresh the access token. You must specify refresh_token, token_url, client_id, and client_secret."
+ )
+
+ assert not creds.expiry
+ assert not creds.expired
+ assert not creds.requires_scopes
+ assert creds.is_user
+
+ request.assert_not_called()
+
+ def test_refresh_without_token_url(self):
+ request = self.make_mock_request()
+ creds = self.make_credentials(token_url=None, token=ACCESS_TOKEN)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ creds.refresh(request)
+
+ assert excinfo.match(
+ r"The credentials do not contain the necessary fields need to refresh the access token. You must specify refresh_token, token_url, client_id, and client_secret."
+ )
+
+ assert not creds.expiry
+ assert not creds.expired
+ assert not creds.requires_scopes
+ assert creds.is_user
+
+ request.assert_not_called()
+
+ def test_refresh_without_client_id(self):
+ request = self.make_mock_request()
+ creds = self.make_credentials(client_id=None, token=ACCESS_TOKEN)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ creds.refresh(request)
+
+ assert excinfo.match(
+ r"The credentials do not contain the necessary fields need to refresh the access token. You must specify refresh_token, token_url, client_id, and client_secret."
+ )
+
+ assert not creds.expiry
+ assert not creds.expired
+ assert not creds.requires_scopes
+ assert creds.is_user
+
+ request.assert_not_called()
+
+ def test_refresh_without_client_secret(self):
+ request = self.make_mock_request()
+ creds = self.make_credentials(client_secret=None, token=ACCESS_TOKEN)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ creds.refresh(request)
+
+ assert excinfo.match(
+ r"The credentials do not contain the necessary fields need to refresh the access token. You must specify refresh_token, token_url, client_id, and client_secret."
+ )
+
+ assert not creds.expiry
+ assert not creds.expired
+ assert not creds.requires_scopes
+ assert creds.is_user
+
+ request.assert_not_called()
+
+ def test_info(self):
+ creds = self.make_credentials()
+ info = creds.info
+
+ assert info["audience"] == AUDIENCE
+ assert info["refresh_token"] == REFRESH_TOKEN
+ assert info["token_url"] == TOKEN_URL
+ assert info["token_info_url"] == TOKEN_INFO_URL
+ assert info["client_id"] == CLIENT_ID
+ assert info["client_secret"] == CLIENT_SECRET
+ assert "token" not in info
+ assert "expiry" not in info
+ assert "revoke_url" not in info
+ assert "quota_project_id" not in info
+
+ def test_info_full(self):
+ creds = self.make_credentials(
+ token=ACCESS_TOKEN,
+ expiry=NOW,
+ revoke_url=REVOKE_URL,
+ quota_project_id=QUOTA_PROJECT_ID,
+ )
+ info = creds.info
+
+ assert info["audience"] == AUDIENCE
+ assert info["refresh_token"] == REFRESH_TOKEN
+ assert info["token_url"] == TOKEN_URL
+ assert info["token_info_url"] == TOKEN_INFO_URL
+ assert info["client_id"] == CLIENT_ID
+ assert info["client_secret"] == CLIENT_SECRET
+ assert info["token"] == ACCESS_TOKEN
+ assert info["expiry"] == NOW.isoformat() + "Z"
+ assert info["revoke_url"] == REVOKE_URL
+ assert info["quota_project_id"] == QUOTA_PROJECT_ID
+
+ def test_to_json(self):
+ creds = self.make_credentials()
+ json_info = creds.to_json()
+ info = json.loads(json_info)
+
+ assert info["audience"] == AUDIENCE
+ assert info["refresh_token"] == REFRESH_TOKEN
+ assert info["token_url"] == TOKEN_URL
+ assert info["token_info_url"] == TOKEN_INFO_URL
+ assert info["client_id"] == CLIENT_ID
+ assert info["client_secret"] == CLIENT_SECRET
+ assert "token" not in info
+ assert "expiry" not in info
+ assert "revoke_url" not in info
+ assert "quota_project_id" not in info
+
+ def test_to_json_full(self):
+ creds = self.make_credentials(
+ token=ACCESS_TOKEN,
+ expiry=NOW,
+ revoke_url=REVOKE_URL,
+ quota_project_id=QUOTA_PROJECT_ID,
+ )
+ json_info = creds.to_json()
+ info = json.loads(json_info)
+
+ assert info["audience"] == AUDIENCE
+ assert info["refresh_token"] == REFRESH_TOKEN
+ assert info["token_url"] == TOKEN_URL
+ assert info["token_info_url"] == TOKEN_INFO_URL
+ assert info["client_id"] == CLIENT_ID
+ assert info["client_secret"] == CLIENT_SECRET
+ assert info["token"] == ACCESS_TOKEN
+ assert info["expiry"] == NOW.isoformat() + "Z"
+ assert info["revoke_url"] == REVOKE_URL
+ assert info["quota_project_id"] == QUOTA_PROJECT_ID
+
+ def test_to_json_full_with_strip(self):
+ creds = self.make_credentials(
+ token=ACCESS_TOKEN,
+ expiry=NOW,
+ revoke_url=REVOKE_URL,
+ quota_project_id=QUOTA_PROJECT_ID,
+ )
+ json_info = creds.to_json(strip=["token", "expiry"])
+ info = json.loads(json_info)
+
+ assert info["audience"] == AUDIENCE
+ assert info["refresh_token"] == REFRESH_TOKEN
+ assert info["token_url"] == TOKEN_URL
+ assert info["token_info_url"] == TOKEN_INFO_URL
+ assert info["client_id"] == CLIENT_ID
+ assert info["client_secret"] == CLIENT_SECRET
+ assert "token" not in info
+ assert "expiry" not in info
+ assert info["revoke_url"] == REVOKE_URL
+ assert info["quota_project_id"] == QUOTA_PROJECT_ID
+
+ def test_get_project_id(self):
+ creds = self.make_credentials()
+ request = mock.create_autospec(transport.Request)
+
+ assert creds.get_project_id(request) is None
+ request.assert_not_called()
+
+ def test_with_quota_project(self):
+ creds = self.make_credentials(
+ token=ACCESS_TOKEN,
+ expiry=NOW,
+ revoke_url=REVOKE_URL,
+ quota_project_id=QUOTA_PROJECT_ID,
+ )
+ new_creds = creds.with_quota_project(QUOTA_PROJECT_ID)
+ assert new_creds._audience == creds._audience
+ assert new_creds._refresh_token == creds._refresh_token
+ assert new_creds._token_url == creds._token_url
+ assert new_creds._token_info_url == creds._token_info_url
+ assert new_creds._client_id == creds._client_id
+ assert new_creds._client_secret == creds._client_secret
+ assert new_creds.token == creds.token
+ assert new_creds.expiry == creds.expiry
+ assert new_creds._revoke_url == creds._revoke_url
+ assert new_creds._quota_project_id == QUOTA_PROJECT_ID
+
+ def test_with_token_uri(self):
+ creds = self.make_credentials(
+ token=ACCESS_TOKEN,
+ expiry=NOW,
+ revoke_url=REVOKE_URL,
+ quota_project_id=QUOTA_PROJECT_ID,
+ )
+ new_creds = creds.with_token_uri("https://google.com")
+ assert new_creds._audience == creds._audience
+ assert new_creds._refresh_token == creds._refresh_token
+ assert new_creds._token_url == "https://google.com"
+ assert new_creds._token_info_url == creds._token_info_url
+ assert new_creds._client_id == creds._client_id
+ assert new_creds._client_secret == creds._client_secret
+ assert new_creds.token == creds.token
+ assert new_creds.expiry == creds.expiry
+ assert new_creds._revoke_url == creds._revoke_url
+ assert new_creds._quota_project_id == creds._quota_project_id
+
+ def test_from_file_required_options_only(self, tmpdir):
+ from_creds = self.make_credentials()
+ config_file = tmpdir.join("config.json")
+ config_file.write(from_creds.to_json())
+ creds = external_account_authorized_user.Credentials.from_file(str(config_file))
+
+ assert isinstance(creds, external_account_authorized_user.Credentials)
+ assert creds.audience == AUDIENCE
+ assert creds.refresh_token == REFRESH_TOKEN
+ assert creds.token_url == TOKEN_URL
+ assert creds.token_info_url == TOKEN_INFO_URL
+ assert creds.client_id == CLIENT_ID
+ assert creds.client_secret == CLIENT_SECRET
+ assert creds.token is None
+ assert creds.expiry is None
+ assert creds.scopes is None
+ assert creds._revoke_url is None
+ assert creds._quota_project_id is None
+
+ def test_from_file_full_options(self, tmpdir):
+ from_creds = self.make_credentials(
+ token=ACCESS_TOKEN,
+ expiry=NOW,
+ revoke_url=REVOKE_URL,
+ quota_project_id=QUOTA_PROJECT_ID,
+ scopes=SCOPES,
+ )
+ config_file = tmpdir.join("config.json")
+ config_file.write(from_creds.to_json())
+ creds = external_account_authorized_user.Credentials.from_file(str(config_file))
+
+ assert isinstance(creds, external_account_authorized_user.Credentials)
+ assert creds.audience == AUDIENCE
+ assert creds.refresh_token == REFRESH_TOKEN
+ assert creds.token_url == TOKEN_URL
+ assert creds.token_info_url == TOKEN_INFO_URL
+ assert creds.client_id == CLIENT_ID
+ assert creds.client_secret == CLIENT_SECRET
+ assert creds.token == ACCESS_TOKEN
+ assert creds.expiry == NOW
+ assert creds.scopes == SCOPES
+ assert creds._revoke_url == REVOKE_URL
+ assert creds._quota_project_id == QUOTA_PROJECT_ID
diff --git a/contrib/python/google-auth/py3/tests/test_iam.py b/contrib/python/google-auth/py3/tests/test_iam.py
new file mode 100644
index 0000000000..6706afb4b5
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_iam.py
@@ -0,0 +1,102 @@
+# Copyright 2017 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import datetime
+import http.client as http_client
+import json
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import iam
+from google.auth import transport
+import google.auth.credentials
+
+
+def make_request(status, data=None):
+ response = mock.create_autospec(transport.Response, instance=True)
+ response.status = status
+
+ if data is not None:
+ response.data = json.dumps(data).encode("utf-8")
+
+ request = mock.create_autospec(transport.Request)
+ request.return_value = response
+ return request
+
+
+def make_credentials():
+ class CredentialsImpl(google.auth.credentials.Credentials):
+ def __init__(self):
+ super(CredentialsImpl, self).__init__()
+ self.token = "token"
+ # Force refresh
+ self.expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD
+
+ def refresh(self, request):
+ pass
+
+ def with_quota_project(self, quota_project_id):
+ raise NotImplementedError()
+
+ return CredentialsImpl()
+
+
+class TestSigner(object):
+ def test_constructor(self):
+ request = mock.sentinel.request
+ credentials = mock.create_autospec(
+ google.auth.credentials.Credentials, instance=True
+ )
+
+ signer = iam.Signer(request, credentials, mock.sentinel.service_account_email)
+
+ assert signer._request == mock.sentinel.request
+ assert signer._credentials == credentials
+ assert signer._service_account_email == mock.sentinel.service_account_email
+
+ def test_key_id(self):
+ signer = iam.Signer(
+ mock.sentinel.request,
+ mock.sentinel.credentials,
+ mock.sentinel.service_account_email,
+ )
+
+ assert signer.key_id is None
+
+ def test_sign_bytes(self):
+ signature = b"DEADBEEF"
+ encoded_signature = base64.b64encode(signature).decode("utf-8")
+ request = make_request(http_client.OK, data={"signedBlob": encoded_signature})
+ credentials = make_credentials()
+
+ signer = iam.Signer(request, credentials, mock.sentinel.service_account_email)
+
+ returned_signature = signer.sign("123")
+
+ assert returned_signature == signature
+ kwargs = request.call_args[1]
+ assert kwargs["headers"]["Content-Type"] == "application/json"
+
+ def test_sign_bytes_failure(self):
+ request = make_request(http_client.UNAUTHORIZED)
+ credentials = make_credentials()
+
+ signer = iam.Signer(request, credentials, mock.sentinel.service_account_email)
+
+ with pytest.raises(exceptions.TransportError):
+ signer.sign("123")
diff --git a/contrib/python/google-auth/py3/tests/test_identity_pool.py b/contrib/python/google-auth/py3/tests/test_identity_pool.py
new file mode 100644
index 0000000000..d126a579bd
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_identity_pool.py
@@ -0,0 +1,1302 @@
+# Copyright 2020 Google LLC
+#
+# 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 datetime
+import http.client as http_client
+import json
+import os
+import urllib
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import identity_pool
+from google.auth import metrics
+from google.auth import transport
+
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password".
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com"
+SERVICE_ACCOUNT_IMPERSONATION_URL_BASE = (
+ "https://us-east1-iamcredentials.googleapis.com"
+)
+SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE = "/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
+ SERVICE_ACCOUNT_EMAIL
+)
+SERVICE_ACCOUNT_IMPERSONATION_URL = (
+ SERVICE_ACCOUNT_IMPERSONATION_URL_BASE + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
+)
+
+QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID"
+SCOPES = ["scope1", "scope2"]
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+SUBJECT_TOKEN_TEXT_FILE = os.path.join(DATA_DIR, "external_subject_token.txt")
+SUBJECT_TOKEN_JSON_FILE = os.path.join(DATA_DIR, "external_subject_token.json")
+SUBJECT_TOKEN_FIELD_NAME = "access_token"
+
+with open(SUBJECT_TOKEN_TEXT_FILE) as fh:
+ TEXT_FILE_SUBJECT_TOKEN = fh.read()
+
+with open(SUBJECT_TOKEN_JSON_FILE) as fh:
+ JSON_FILE_CONTENT = json.load(fh)
+ JSON_FILE_SUBJECT_TOKEN = JSON_FILE_CONTENT.get(SUBJECT_TOKEN_FIELD_NAME)
+
+TOKEN_URL = "https://sts.googleapis.com/v1/token"
+TOKEN_INFO_URL = "https://sts.googleapis.com/v1/introspect"
+SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
+AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
+WORKFORCE_AUDIENCE = (
+ "//iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID"
+)
+WORKFORCE_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token"
+WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER"
+
+DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
+
+VALID_TOKEN_URLS = [
+ "https://sts.googleapis.com",
+ "https://us-east-1.sts.googleapis.com",
+ "https://US-EAST-1.sts.googleapis.com",
+ "https://sts.us-east-1.googleapis.com",
+ "https://sts.US-WEST-1.googleapis.com",
+ "https://us-east-1-sts.googleapis.com",
+ "https://US-WEST-1-sts.googleapis.com",
+ "https://us-west-1-sts.googleapis.com/path?query",
+ "https://sts-us-east-1.p.googleapis.com",
+]
+INVALID_TOKEN_URLS = [
+ "https://iamcredentials.googleapis.com",
+ "sts.googleapis.com",
+ "https://",
+ "http://sts.googleapis.com",
+ "https://st.s.googleapis.com",
+ "https://us-eas\t-1.sts.googleapis.com",
+ "https:/us-east-1.sts.googleapis.com",
+ "https://US-WE/ST-1-sts.googleapis.com",
+ "https://sts-us-east-1.googleapis.com",
+ "https://sts-US-WEST-1.googleapis.com",
+ "testhttps://us-east-1.sts.googleapis.com",
+ "https://us-east-1.sts.googleapis.comevil.com",
+ "https://us-east-1.us-east-1.sts.googleapis.com",
+ "https://us-ea.s.t.sts.googleapis.com",
+ "https://sts.googleapis.comevil.com",
+ "hhttps://us-east-1.sts.googleapis.com",
+ "https://us- -1.sts.googleapis.com",
+ "https://-sts.googleapis.com",
+ "https://us-east-1.sts.googleapis.com.evil.com",
+ "https://sts.pgoogleapis.com",
+ "https://p.googleapis.com",
+ "https://sts.p.com",
+ "http://sts.p.googleapis.com",
+ "https://xyz-sts.p.googleapis.com",
+ "https://sts-xyz.123.p.googleapis.com",
+ "https://sts-xyz.p1.googleapis.com",
+ "https://sts-xyz.p.foo.com",
+ "https://sts-xyz.p.foo.googleapis.com",
+]
+VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [
+ "https://iamcredentials.googleapis.com",
+ "https://us-east-1.iamcredentials.googleapis.com",
+ "https://US-EAST-1.iamcredentials.googleapis.com",
+ "https://iamcredentials.us-east-1.googleapis.com",
+ "https://iamcredentials.US-WEST-1.googleapis.com",
+ "https://us-east-1-iamcredentials.googleapis.com",
+ "https://US-WEST-1-iamcredentials.googleapis.com",
+ "https://us-west-1-iamcredentials.googleapis.com/path?query",
+ "https://iamcredentials-us-east-1.p.googleapis.com",
+]
+INVALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [
+ "https://sts.googleapis.com",
+ "iamcredentials.googleapis.com",
+ "https://",
+ "http://iamcredentials.googleapis.com",
+ "https://iamcre.dentials.googleapis.com",
+ "https://us-eas\t-1.iamcredentials.googleapis.com",
+ "https:/us-east-1.iamcredentials.googleapis.com",
+ "https://US-WE/ST-1-iamcredentials.googleapis.com",
+ "https://iamcredentials-us-east-1.googleapis.com",
+ "https://iamcredentials-US-WEST-1.googleapis.com",
+ "testhttps://us-east-1.iamcredentials.googleapis.com",
+ "https://us-east-1.iamcredentials.googleapis.comevil.com",
+ "https://us-east-1.us-east-1.iamcredentials.googleapis.com",
+ "https://us-ea.s.t.iamcredentials.googleapis.com",
+ "https://iamcredentials.googleapis.comevil.com",
+ "hhttps://us-east-1.iamcredentials.googleapis.com",
+ "https://us- -1.iamcredentials.googleapis.com",
+ "https://-iamcredentials.googleapis.com",
+ "https://us-east-1.iamcredentials.googleapis.com.evil.com",
+ "https://iamcredentials.pgoogleapis.com",
+ "https://p.googleapis.com",
+ "https://iamcredentials.p.com",
+ "http://iamcredentials.p.googleapis.com",
+ "https://xyz-iamcredentials.p.googleapis.com",
+ "https://iamcredentials-xyz.123.p.googleapis.com",
+ "https://iamcredentials-xyz.p1.googleapis.com",
+ "https://iamcredentials-xyz.p.foo.com",
+ "https://iamcredentials-xyz.p.foo.googleapis.com",
+]
+
+
+class TestCredentials(object):
+ CREDENTIAL_SOURCE_TEXT = {"file": SUBJECT_TOKEN_TEXT_FILE}
+ CREDENTIAL_SOURCE_JSON = {
+ "file": SUBJECT_TOKEN_JSON_FILE,
+ "format": {"type": "json", "subject_token_field_name": "access_token"},
+ }
+ CREDENTIAL_URL = "http://fakeurl.com"
+ CREDENTIAL_SOURCE_TEXT_URL = {"url": CREDENTIAL_URL}
+ CREDENTIAL_SOURCE_JSON_URL = {
+ "url": CREDENTIAL_URL,
+ "format": {"type": "json", "subject_token_field_name": "access_token"},
+ }
+ SUCCESS_RESPONSE = {
+ "access_token": "ACCESS_TOKEN",
+ "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": " ".join(SCOPES),
+ }
+
+ @classmethod
+ def make_mock_response(cls, status, data):
+ response = mock.create_autospec(transport.Response, instance=True)
+ response.status = status
+ if isinstance(data, dict):
+ response.data = json.dumps(data).encode("utf-8")
+ else:
+ response.data = data
+ return response
+
+ @classmethod
+ def make_mock_request(
+ cls, token_status=http_client.OK, token_data=None, *extra_requests
+ ):
+ responses = []
+ responses.append(cls.make_mock_response(token_status, token_data))
+
+ while len(extra_requests) > 0:
+ # If service account impersonation is requested, mock the expected response.
+ status, data, extra_requests = (
+ extra_requests[0],
+ extra_requests[1],
+ extra_requests[2:],
+ )
+ responses.append(cls.make_mock_response(status, data))
+
+ request = mock.create_autospec(transport.Request)
+ request.side_effect = responses
+
+ return request
+
+ @classmethod
+ def assert_credential_request_kwargs(
+ cls, request_kwargs, headers, url=CREDENTIAL_URL
+ ):
+ assert request_kwargs["url"] == url
+ assert request_kwargs["method"] == "GET"
+ assert request_kwargs["headers"] == headers
+ assert request_kwargs.get("body", None) is None
+
+ @classmethod
+ def assert_token_request_kwargs(
+ cls, request_kwargs, headers, request_data, token_url=TOKEN_URL
+ ):
+ assert request_kwargs["url"] == token_url
+ assert request_kwargs["method"] == "POST"
+ assert request_kwargs["headers"] == headers
+ assert request_kwargs["body"] is not None
+ body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
+ assert len(body_tuples) == len(request_data.keys())
+ for (k, v) in body_tuples:
+ assert v.decode("utf-8") == request_data[k.decode("utf-8")]
+
+ @classmethod
+ def assert_impersonation_request_kwargs(
+ cls,
+ request_kwargs,
+ headers,
+ request_data,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ ):
+ assert request_kwargs["url"] == service_account_impersonation_url
+ assert request_kwargs["method"] == "POST"
+ assert request_kwargs["headers"] == headers
+ assert request_kwargs["body"] is not None
+ body_json = json.loads(request_kwargs["body"].decode("utf-8"))
+ assert body_json == request_data
+
+ @classmethod
+ def assert_underlying_credentials_refresh(
+ cls,
+ credentials,
+ audience,
+ subject_token,
+ subject_token_type,
+ token_url,
+ service_account_impersonation_url=None,
+ basic_auth_encoding=None,
+ quota_project_id=None,
+ used_scopes=None,
+ credential_data=None,
+ scopes=None,
+ default_scopes=None,
+ workforce_pool_user_project=None,
+ ):
+ """Utility to assert that a credentials are initialized with the expected
+ attributes by calling refresh functionality and confirming response matches
+ expected one and that the underlying requests were populated with the
+ expected parameters.
+ """
+ # STS token exchange request/response.
+ token_response = cls.SUCCESS_RESPONSE.copy()
+ token_headers = {"Content-Type": "application/x-www-form-urlencoded"}
+ if basic_auth_encoding:
+ token_headers["Authorization"] = "Basic " + basic_auth_encoding
+
+ metrics_options = {}
+ if credentials._service_account_impersonation_url:
+ metrics_options["sa-impersonation"] = "true"
+ else:
+ metrics_options["sa-impersonation"] = "false"
+ metrics_options["config-lifetime"] = "false"
+ if credentials._credential_source_file:
+ metrics_options["source"] = "file"
+ else:
+ metrics_options["source"] = "url"
+
+ token_headers["x-goog-api-client"] = metrics.byoid_metrics_header(
+ metrics_options
+ )
+
+ if service_account_impersonation_url:
+ token_scopes = "https://www.googleapis.com/auth/iam"
+ else:
+ token_scopes = " ".join(used_scopes or [])
+
+ token_request_data = {
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+ "audience": audience,
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+ "scope": token_scopes,
+ "subject_token": subject_token,
+ "subject_token_type": subject_token_type,
+ }
+ if workforce_pool_user_project:
+ token_request_data["options"] = urllib.parse.quote(
+ json.dumps({"userProject": workforce_pool_user_project})
+ )
+
+ metrics_header_value = (
+ "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
+ )
+ if service_account_impersonation_url:
+ # Service account impersonation request/response.
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0)
+ + datetime.timedelta(seconds=3600)
+ ).isoformat("T") + "Z"
+ impersonation_response = {
+ "accessToken": "SA_ACCESS_TOKEN",
+ "expireTime": expire_time,
+ }
+ impersonation_headers = {
+ "Content-Type": "application/json",
+ "authorization": "Bearer {}".format(token_response["access_token"]),
+ "x-goog-api-client": metrics_header_value,
+ "x-identity-trust-boundary": "0",
+ }
+ impersonation_request_data = {
+ "delegates": None,
+ "scope": used_scopes,
+ "lifetime": "3600s",
+ }
+
+ # Initialize mock request to handle token retrieval, token exchange and
+ # service account impersonation request.
+ requests = []
+ if credential_data:
+ requests.append((http_client.OK, credential_data))
+
+ token_request_index = len(requests)
+ requests.append((http_client.OK, token_response))
+
+ if service_account_impersonation_url:
+ impersonation_request_index = len(requests)
+ requests.append((http_client.OK, impersonation_response))
+
+ request = cls.make_mock_request(*[el for req in requests for el in req])
+
+ with mock.patch(
+ "google.auth.metrics.token_request_access_token_impersonate",
+ return_value=metrics_header_value,
+ ):
+ credentials.refresh(request)
+
+ assert len(request.call_args_list) == len(requests)
+ if credential_data:
+ cls.assert_credential_request_kwargs(request.call_args_list[0][1], None)
+ # Verify token exchange request parameters.
+ cls.assert_token_request_kwargs(
+ request.call_args_list[token_request_index][1],
+ token_headers,
+ token_request_data,
+ token_url,
+ )
+ # Verify service account impersonation request parameters if the request
+ # is processed.
+ if service_account_impersonation_url:
+ cls.assert_impersonation_request_kwargs(
+ request.call_args_list[impersonation_request_index][1],
+ impersonation_headers,
+ impersonation_request_data,
+ service_account_impersonation_url,
+ )
+ assert credentials.token == impersonation_response["accessToken"]
+ else:
+ assert credentials.token == token_response["access_token"]
+ assert credentials.quota_project_id == quota_project_id
+ assert credentials.scopes == scopes
+ assert credentials.default_scopes == default_scopes
+
+ @classmethod
+ def make_credentials(
+ cls,
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ client_id=None,
+ client_secret=None,
+ quota_project_id=None,
+ scopes=None,
+ default_scopes=None,
+ service_account_impersonation_url=None,
+ credential_source=None,
+ workforce_pool_user_project=None,
+ ):
+ return identity_pool.Credentials(
+ audience=audience,
+ subject_token_type=subject_token_type,
+ token_url=token_url,
+ token_info_url=token_info_url,
+ service_account_impersonation_url=service_account_impersonation_url,
+ credential_source=credential_source,
+ client_id=client_id,
+ client_secret=client_secret,
+ quota_project_id=quota_project_id,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ workforce_pool_user_project=workforce_pool_user_project,
+ )
+
+ @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+ def test_from_info_full_options(self, mock_init):
+ credentials = identity_pool.Credentials.from_info(
+ {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "service_account_impersonation": {"token_lifetime_seconds": 2800},
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ "quota_project_id": QUOTA_PROJECT_ID,
+ "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+ }
+ )
+
+ # Confirm identity_pool.Credentials instantiated with expected attributes.
+ assert isinstance(credentials, identity_pool.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ quota_project_id=QUOTA_PROJECT_ID,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+ def test_from_info_required_options_only(self, mock_init):
+ credentials = identity_pool.Credentials.from_info(
+ {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+ }
+ )
+
+ # Confirm identity_pool.Credentials instantiated with expected attributes.
+ assert isinstance(credentials, identity_pool.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=None,
+ service_account_impersonation_url=None,
+ service_account_impersonation_options={},
+ client_id=None,
+ client_secret=None,
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ quota_project_id=None,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+ def test_from_info_workforce_pool(self, mock_init):
+ credentials = identity_pool.Credentials.from_info(
+ {
+ "audience": WORKFORCE_AUDIENCE,
+ "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+ "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
+ }
+ )
+
+ # Confirm identity_pool.Credentials instantiated with expected attributes.
+ assert isinstance(credentials, identity_pool.Credentials)
+ mock_init.assert_called_once_with(
+ audience=WORKFORCE_AUDIENCE,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=None,
+ service_account_impersonation_url=None,
+ service_account_impersonation_options={},
+ client_id=None,
+ client_secret=None,
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ quota_project_id=None,
+ workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+ def test_from_file_full_options(self, mock_init, tmpdir):
+ info = {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "service_account_impersonation": {"token_lifetime_seconds": 2800},
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ "quota_project_id": QUOTA_PROJECT_ID,
+ "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+ }
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(info))
+ credentials = identity_pool.Credentials.from_file(str(config_file))
+
+ # Confirm identity_pool.Credentials instantiated with expected attributes.
+ assert isinstance(credentials, identity_pool.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ quota_project_id=QUOTA_PROJECT_ID,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+ def test_from_file_required_options_only(self, mock_init, tmpdir):
+ info = {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+ }
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(info))
+ credentials = identity_pool.Credentials.from_file(str(config_file))
+
+ # Confirm identity_pool.Credentials instantiated with expected attributes.
+ assert isinstance(credentials, identity_pool.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=None,
+ service_account_impersonation_url=None,
+ service_account_impersonation_options={},
+ client_id=None,
+ client_secret=None,
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ quota_project_id=None,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+ def test_from_file_workforce_pool(self, mock_init, tmpdir):
+ info = {
+ "audience": WORKFORCE_AUDIENCE,
+ "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+ "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
+ }
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(info))
+ credentials = identity_pool.Credentials.from_file(str(config_file))
+
+ # Confirm identity_pool.Credentials instantiated with expected attributes.
+ assert isinstance(credentials, identity_pool.Credentials)
+ mock_init.assert_called_once_with(
+ audience=WORKFORCE_AUDIENCE,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=None,
+ service_account_impersonation_url=None,
+ service_account_impersonation_options={},
+ client_id=None,
+ client_secret=None,
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ quota_project_id=None,
+ workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ def test_constructor_nonworkforce_with_workforce_pool_user_project(self):
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(
+ audience=AUDIENCE,
+ workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ assert excinfo.match(
+ "workforce_pool_user_project should not be set for non-workforce "
+ "pool credentials"
+ )
+
+ def test_constructor_invalid_options(self):
+ credential_source = {"unsupported": "value"}
+
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(credential_source=credential_source)
+
+ assert excinfo.match(r"Missing credential_source")
+
+ def test_constructor_invalid_options_url_and_file(self):
+ credential_source = {
+ "url": self.CREDENTIAL_URL,
+ "file": SUBJECT_TOKEN_TEXT_FILE,
+ }
+
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(credential_source=credential_source)
+
+ assert excinfo.match(r"Ambiguous credential_source")
+
+ def test_constructor_invalid_options_environment_id(self):
+ credential_source = {"url": self.CREDENTIAL_URL, "environment_id": "aws1"}
+
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(credential_source=credential_source)
+
+ assert excinfo.match(
+ r"Invalid Identity Pool credential_source field 'environment_id'"
+ )
+
+ def test_constructor_invalid_credential_source(self):
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(credential_source="non-dict")
+
+ assert excinfo.match(r"Missing credential_source")
+
+ def test_constructor_invalid_credential_source_format_type(self):
+ credential_source = {"format": {"type": "xml"}}
+
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(credential_source=credential_source)
+
+ assert excinfo.match(r"Invalid credential_source format 'xml'")
+
+ def test_constructor_missing_subject_token_field_name(self):
+ credential_source = {"format": {"type": "json"}}
+
+ with pytest.raises(ValueError) as excinfo:
+ self.make_credentials(credential_source=credential_source)
+
+ assert excinfo.match(
+ r"Missing subject_token_field_name for JSON credential_source format"
+ )
+
+ def test_info_with_workforce_pool_user_project(self):
+ credentials = self.make_credentials(
+ audience=WORKFORCE_AUDIENCE,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ credential_source=self.CREDENTIAL_SOURCE_TEXT_URL.copy(),
+ workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": WORKFORCE_AUDIENCE,
+ "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "credential_source": self.CREDENTIAL_SOURCE_TEXT_URL,
+ "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
+ "universe_domain": DEFAULT_UNIVERSE_DOMAIN,
+ }
+
+ def test_info_with_file_credential_source(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_TEXT_URL.copy()
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "credential_source": self.CREDENTIAL_SOURCE_TEXT_URL,
+ "universe_domain": DEFAULT_UNIVERSE_DOMAIN,
+ }
+
+ def test_info_with_url_credential_source(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_JSON_URL.copy()
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "credential_source": self.CREDENTIAL_SOURCE_JSON_URL,
+ "universe_domain": DEFAULT_UNIVERSE_DOMAIN,
+ }
+
+ def test_retrieve_subject_token_missing_subject_token(self, tmpdir):
+ # Provide empty text file.
+ empty_file = tmpdir.join("empty.txt")
+ empty_file.write("")
+ credential_source = {"file": str(empty_file)}
+ credentials = self.make_credentials(credential_source=credential_source)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(r"Missing subject_token in the credential_source file")
+
+ def test_retrieve_subject_token_text_file(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_TEXT
+ )
+
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == TEXT_FILE_SUBJECT_TOKEN
+
+ def test_retrieve_subject_token_json_file(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_JSON
+ )
+
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == JSON_FILE_SUBJECT_TOKEN
+
+ def test_retrieve_subject_token_json_file_invalid_field_name(self):
+ credential_source = {
+ "file": SUBJECT_TOKEN_JSON_FILE,
+ "format": {"type": "json", "subject_token_field_name": "not_found"},
+ }
+ credentials = self.make_credentials(credential_source=credential_source)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+ SUBJECT_TOKEN_JSON_FILE, "not_found"
+ )
+ )
+
+ def test_retrieve_subject_token_invalid_json(self, tmpdir):
+ # Provide JSON file. This should result in JSON parsing error.
+ invalid_json_file = tmpdir.join("invalid.json")
+ invalid_json_file.write("{")
+ credential_source = {
+ "file": str(invalid_json_file),
+ "format": {"type": "json", "subject_token_field_name": "access_token"},
+ }
+ credentials = self.make_credentials(credential_source=credential_source)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+ str(invalid_json_file), "access_token"
+ )
+ )
+
+ def test_retrieve_subject_token_file_not_found(self):
+ credential_source = {"file": "./not_found.txt"}
+ credentials = self.make_credentials(credential_source=credential_source)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(r"File './not_found.txt' was not found")
+
+ def test_token_info_url(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_JSON
+ )
+
+ assert credentials.token_info_url == TOKEN_INFO_URL
+
+ def test_token_info_url_custom(self):
+ for url in VALID_TOKEN_URLS:
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_JSON.copy(),
+ token_info_url=(url + "/introspect"),
+ )
+
+ assert credentials.token_info_url == url + "/introspect"
+
+ def test_token_info_url_negative(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_JSON.copy(), token_info_url=None
+ )
+
+ assert not credentials.token_info_url
+
+ def test_token_url_custom(self):
+ for url in VALID_TOKEN_URLS:
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_JSON.copy(),
+ token_url=(url + "/token"),
+ )
+
+ assert credentials._token_url == (url + "/token")
+
+ def test_service_account_impersonation_url_custom(self):
+ for url in VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS:
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_JSON.copy(),
+ service_account_impersonation_url=(
+ url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
+ ),
+ )
+
+ assert credentials._service_account_impersonation_url == (
+ url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
+ )
+
+ def test_refresh_text_file_success_without_impersonation_ignore_default_scopes(
+ self,
+ ):
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ # Test with text format type.
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ scopes=SCOPES,
+ # Default scopes should be ignored.
+ default_scopes=["ignored"],
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=AUDIENCE,
+ subject_token=TEXT_FILE_SUBJECT_TOKEN,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=None,
+ basic_auth_encoding=BASIC_AUTH_ENCODING,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ default_scopes=["ignored"],
+ )
+
+ def test_refresh_workforce_success_with_client_auth_without_impersonation(self):
+ credentials = self.make_credentials(
+ audience=WORKFORCE_AUDIENCE,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ # Test with text format type.
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ scopes=SCOPES,
+ # This will be ignored in favor of client auth.
+ workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=WORKFORCE_AUDIENCE,
+ subject_token=TEXT_FILE_SUBJECT_TOKEN,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=None,
+ basic_auth_encoding=BASIC_AUTH_ENCODING,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ workforce_pool_user_project=None,
+ )
+
+ def test_refresh_workforce_success_with_client_auth_and_no_workforce_project(self):
+ credentials = self.make_credentials(
+ audience=WORKFORCE_AUDIENCE,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ # Test with text format type.
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ scopes=SCOPES,
+ # This is not needed when client Auth is used.
+ workforce_pool_user_project=None,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=WORKFORCE_AUDIENCE,
+ subject_token=TEXT_FILE_SUBJECT_TOKEN,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=None,
+ basic_auth_encoding=BASIC_AUTH_ENCODING,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ workforce_pool_user_project=None,
+ )
+
+ def test_refresh_workforce_success_without_client_auth_without_impersonation(self):
+ credentials = self.make_credentials(
+ audience=WORKFORCE_AUDIENCE,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ client_id=None,
+ client_secret=None,
+ # Test with text format type.
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ scopes=SCOPES,
+ # This will not be ignored as client auth is not used.
+ workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=WORKFORCE_AUDIENCE,
+ subject_token=TEXT_FILE_SUBJECT_TOKEN,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=None,
+ basic_auth_encoding=None,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ def test_refresh_workforce_success_without_client_auth_with_impersonation(self):
+ credentials = self.make_credentials(
+ audience=WORKFORCE_AUDIENCE,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ client_id=None,
+ client_secret=None,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ # Test with text format type.
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ scopes=SCOPES,
+ # This will not be ignored as client auth is not used.
+ workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=WORKFORCE_AUDIENCE,
+ subject_token=TEXT_FILE_SUBJECT_TOKEN,
+ subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ basic_auth_encoding=None,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+ )
+
+ def test_refresh_text_file_success_without_impersonation_use_default_scopes(self):
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ # Test with text format type.
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ scopes=None,
+ # Default scopes should be used since user specified scopes are none.
+ default_scopes=SCOPES,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=AUDIENCE,
+ subject_token=TEXT_FILE_SUBJECT_TOKEN,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=None,
+ basic_auth_encoding=BASIC_AUTH_ENCODING,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=None,
+ default_scopes=SCOPES,
+ )
+
+ def test_refresh_text_file_success_with_impersonation_ignore_default_scopes(self):
+ # Initialize credentials with service account impersonation and basic auth.
+ credentials = self.make_credentials(
+ # Test with text format type.
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=SCOPES,
+ # Default scopes should be ignored.
+ default_scopes=["ignored"],
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=AUDIENCE,
+ subject_token=TEXT_FILE_SUBJECT_TOKEN,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ basic_auth_encoding=None,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ default_scopes=["ignored"],
+ )
+
+ def test_refresh_text_file_success_with_impersonation_use_default_scopes(self):
+ # Initialize credentials with service account impersonation, basic auth
+ # and default scopes (no user scopes).
+ credentials = self.make_credentials(
+ # Test with text format type.
+ credential_source=self.CREDENTIAL_SOURCE_TEXT,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=None,
+ # Default scopes should be used since user specified scopes are none.
+ default_scopes=SCOPES,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=AUDIENCE,
+ subject_token=TEXT_FILE_SUBJECT_TOKEN,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ basic_auth_encoding=None,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=None,
+ default_scopes=SCOPES,
+ )
+
+ def test_refresh_json_file_success_without_impersonation(self):
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ # Test with JSON format type.
+ credential_source=self.CREDENTIAL_SOURCE_JSON,
+ scopes=SCOPES,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=AUDIENCE,
+ subject_token=JSON_FILE_SUBJECT_TOKEN,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=None,
+ basic_auth_encoding=BASIC_AUTH_ENCODING,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ default_scopes=None,
+ )
+
+ def test_refresh_json_file_success_with_impersonation(self):
+ # Initialize credentials with service account impersonation and basic auth.
+ credentials = self.make_credentials(
+ # Test with JSON format type.
+ credential_source=self.CREDENTIAL_SOURCE_JSON,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=SCOPES,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=AUDIENCE,
+ subject_token=JSON_FILE_SUBJECT_TOKEN,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ basic_auth_encoding=None,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ default_scopes=None,
+ )
+
+ def test_refresh_with_retrieve_subject_token_error(self):
+ credential_source = {
+ "file": SUBJECT_TOKEN_JSON_FILE,
+ "format": {"type": "json", "subject_token_field_name": "not_found"},
+ }
+ credentials = self.make_credentials(credential_source=credential_source)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.refresh(None)
+
+ assert excinfo.match(
+ "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+ SUBJECT_TOKEN_JSON_FILE, "not_found"
+ )
+ )
+
+ def test_retrieve_subject_token_from_url(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_TEXT_URL
+ )
+ request = self.make_mock_request(token_data=TEXT_FILE_SUBJECT_TOKEN)
+ subject_token = credentials.retrieve_subject_token(request)
+
+ assert subject_token == TEXT_FILE_SUBJECT_TOKEN
+ self.assert_credential_request_kwargs(request.call_args_list[0][1], None)
+
+ def test_retrieve_subject_token_from_url_with_headers(self):
+ credentials = self.make_credentials(
+ credential_source={"url": self.CREDENTIAL_URL, "headers": {"foo": "bar"}}
+ )
+ request = self.make_mock_request(token_data=TEXT_FILE_SUBJECT_TOKEN)
+ subject_token = credentials.retrieve_subject_token(request)
+
+ assert subject_token == TEXT_FILE_SUBJECT_TOKEN
+ self.assert_credential_request_kwargs(
+ request.call_args_list[0][1], {"foo": "bar"}
+ )
+
+ def test_retrieve_subject_token_from_url_json(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_JSON_URL
+ )
+ request = self.make_mock_request(token_data=JSON_FILE_CONTENT)
+ subject_token = credentials.retrieve_subject_token(request)
+
+ assert subject_token == JSON_FILE_SUBJECT_TOKEN
+ self.assert_credential_request_kwargs(request.call_args_list[0][1], None)
+
+ def test_retrieve_subject_token_from_url_json_with_headers(self):
+ credentials = self.make_credentials(
+ credential_source={
+ "url": self.CREDENTIAL_URL,
+ "format": {"type": "json", "subject_token_field_name": "access_token"},
+ "headers": {"foo": "bar"},
+ }
+ )
+ request = self.make_mock_request(token_data=JSON_FILE_CONTENT)
+ subject_token = credentials.retrieve_subject_token(request)
+
+ assert subject_token == JSON_FILE_SUBJECT_TOKEN
+ self.assert_credential_request_kwargs(
+ request.call_args_list[0][1], {"foo": "bar"}
+ )
+
+ def test_retrieve_subject_token_from_url_not_found(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_TEXT_URL
+ )
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(
+ self.make_mock_request(token_status=404, token_data=JSON_FILE_CONTENT)
+ )
+
+ assert excinfo.match("Unable to retrieve Identity Pool subject token")
+
+ def test_retrieve_subject_token_from_url_json_invalid_field(self):
+ credential_source = {
+ "url": self.CREDENTIAL_URL,
+ "format": {"type": "json", "subject_token_field_name": "not_found"},
+ }
+ credentials = self.make_credentials(credential_source=credential_source)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(
+ self.make_mock_request(token_data=JSON_FILE_CONTENT)
+ )
+
+ assert excinfo.match(
+ "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+ self.CREDENTIAL_URL, "not_found"
+ )
+ )
+
+ def test_retrieve_subject_token_from_url_json_invalid_format(self):
+ credentials = self.make_credentials(
+ credential_source=self.CREDENTIAL_SOURCE_JSON_URL
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.retrieve_subject_token(self.make_mock_request(token_data="{"))
+
+ assert excinfo.match(
+ "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+ self.CREDENTIAL_URL, "access_token"
+ )
+ )
+
+ def test_refresh_text_file_success_without_impersonation_url(self):
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ # Test with text format type.
+ credential_source=self.CREDENTIAL_SOURCE_TEXT_URL,
+ scopes=SCOPES,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=AUDIENCE,
+ subject_token=TEXT_FILE_SUBJECT_TOKEN,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=None,
+ basic_auth_encoding=BASIC_AUTH_ENCODING,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ default_scopes=None,
+ credential_data=TEXT_FILE_SUBJECT_TOKEN,
+ )
+
+ def test_refresh_text_file_success_with_impersonation_url(self):
+ # Initialize credentials with service account impersonation and basic auth.
+ credentials = self.make_credentials(
+ # Test with text format type.
+ credential_source=self.CREDENTIAL_SOURCE_TEXT_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=SCOPES,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=AUDIENCE,
+ subject_token=TEXT_FILE_SUBJECT_TOKEN,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ basic_auth_encoding=None,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ default_scopes=None,
+ credential_data=TEXT_FILE_SUBJECT_TOKEN,
+ )
+
+ def test_refresh_json_file_success_without_impersonation_url(self):
+ credentials = self.make_credentials(
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ # Test with JSON format type.
+ credential_source=self.CREDENTIAL_SOURCE_JSON_URL,
+ scopes=SCOPES,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=AUDIENCE,
+ subject_token=JSON_FILE_SUBJECT_TOKEN,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=None,
+ basic_auth_encoding=BASIC_AUTH_ENCODING,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ default_scopes=None,
+ credential_data=JSON_FILE_CONTENT,
+ )
+
+ def test_refresh_json_file_success_with_impersonation_url(self):
+ # Initialize credentials with service account impersonation and basic auth.
+ credentials = self.make_credentials(
+ # Test with JSON format type.
+ credential_source=self.CREDENTIAL_SOURCE_JSON_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ scopes=SCOPES,
+ )
+
+ self.assert_underlying_credentials_refresh(
+ credentials=credentials,
+ audience=AUDIENCE,
+ subject_token=JSON_FILE_SUBJECT_TOKEN,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ basic_auth_encoding=None,
+ quota_project_id=None,
+ used_scopes=SCOPES,
+ scopes=SCOPES,
+ default_scopes=None,
+ credential_data=JSON_FILE_CONTENT,
+ )
+
+ def test_refresh_with_retrieve_subject_token_error_url(self):
+ credential_source = {
+ "url": self.CREDENTIAL_URL,
+ "format": {"type": "json", "subject_token_field_name": "not_found"},
+ }
+ credentials = self.make_credentials(credential_source=credential_source)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.refresh(self.make_mock_request(token_data=JSON_FILE_CONTENT))
+
+ assert excinfo.match(
+ "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+ self.CREDENTIAL_URL, "not_found"
+ )
+ )
diff --git a/contrib/python/google-auth/py3/tests/test_impersonated_credentials.py b/contrib/python/google-auth/py3/tests/test_impersonated_credentials.py
new file mode 100644
index 0000000000..d63d2d5d3b
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_impersonated_credentials.py
@@ -0,0 +1,660 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import datetime
+import http.client as http_client
+import json
+import os
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import exceptions
+from google.auth import impersonated_credentials
+from google.auth import transport
+from google.auth.impersonated_credentials import Credentials
+from google.oauth2 import credentials
+from google.oauth2 import service_account
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+ PRIVATE_KEY_BYTES = fh.read()
+
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+ID_TOKEN_DATA = (
+ "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyOTNhZDk3N2Ew"
+ "Yjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwc"
+ "zovL2Zvby5iYXIiLCJhenAiOiIxMDIxMDE1NTA4MzQyMDA3MDg1NjgiLCJle"
+ "HAiOjE1NjQ0NzUwNTEsImlhdCI6MTU2NDQ3MTQ1MSwiaXNzIjoiaHR0cHM6L"
+ "y9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwN"
+ "zA4NTY4In0.redacted"
+)
+ID_TOKEN_EXPIRY = 1564475051
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh:
+ SERVICE_ACCOUNT_INFO = json.load(fh)
+
+SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1")
+TOKEN_URI = "https://example.com/oauth2/token"
+
+ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
+ "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
+)
+ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
+ "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/imp"
+)
+
+
+@pytest.fixture
+def mock_donor_credentials():
+ with mock.patch("google.oauth2._client.jwt_grant", autospec=True) as grant:
+ grant.return_value = (
+ "source token",
+ _helpers.utcnow() + datetime.timedelta(seconds=500),
+ {},
+ )
+ yield grant
+
+
+class MockResponse:
+ def __init__(self, json_data, status_code):
+ self.json_data = json_data
+ self.status_code = status_code
+
+ def json(self):
+ return self.json_data
+
+
+@pytest.fixture
+def mock_authorizedsession_sign():
+ with mock.patch(
+ "google.auth.transport.requests.AuthorizedSession.request", autospec=True
+ ) as auth_session:
+ data = {"keyId": "1", "signedBlob": "c2lnbmF0dXJl"}
+ auth_session.return_value = MockResponse(data, http_client.OK)
+ yield auth_session
+
+
+@pytest.fixture
+def mock_authorizedsession_idtoken():
+ with mock.patch(
+ "google.auth.transport.requests.AuthorizedSession.request", autospec=True
+ ) as auth_session:
+ data = {"token": ID_TOKEN_DATA}
+ auth_session.return_value = MockResponse(data, http_client.OK)
+ yield auth_session
+
+
+class TestImpersonatedCredentials(object):
+
+ SERVICE_ACCOUNT_EMAIL = "service-account@example.com"
+ TARGET_PRINCIPAL = "impersonated@project.iam.gserviceaccount.com"
+ TARGET_SCOPES = ["https://www.googleapis.com/auth/devstorage.read_only"]
+ # DELEGATES: List[str] = []
+ # Because Python 2.7:
+ DELEGATES = [] # type: ignore
+ LIFETIME = 3600
+ SOURCE_CREDENTIALS = service_account.Credentials(
+ SIGNER, SERVICE_ACCOUNT_EMAIL, TOKEN_URI
+ )
+ USER_SOURCE_CREDENTIALS = credentials.Credentials(token="ABCDE")
+ IAM_ENDPOINT_OVERRIDE = (
+ "https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
+ + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
+ )
+
+ def make_credentials(
+ self,
+ source_credentials=SOURCE_CREDENTIALS,
+ lifetime=LIFETIME,
+ target_principal=TARGET_PRINCIPAL,
+ iam_endpoint_override=None,
+ ):
+
+ return Credentials(
+ source_credentials=source_credentials,
+ target_principal=target_principal,
+ target_scopes=self.TARGET_SCOPES,
+ delegates=self.DELEGATES,
+ lifetime=lifetime,
+ iam_endpoint_override=iam_endpoint_override,
+ )
+
+ def test_make_from_user_credentials(self):
+ credentials = self.make_credentials(
+ source_credentials=self.USER_SOURCE_CREDENTIALS
+ )
+ assert not credentials.valid
+ assert credentials.expired
+
+ def test_default_state(self):
+ credentials = self.make_credentials()
+ assert not credentials.valid
+ assert credentials.expired
+
+ def test_make_from_service_account_self_signed_jwt(self):
+ source_credentials = service_account.Credentials(
+ SIGNER, self.SERVICE_ACCOUNT_EMAIL, TOKEN_URI, always_use_jwt_access=True
+ )
+ credentials = self.make_credentials(source_credentials=source_credentials)
+ # test the source credential don't lose self signed jwt setting
+ assert credentials._source_credentials._always_use_jwt_access
+ assert credentials._source_credentials._jwt_credentials
+
+ def make_request(
+ self,
+ data,
+ status=http_client.OK,
+ headers=None,
+ side_effect=None,
+ use_data_bytes=True,
+ ):
+ response = mock.create_autospec(transport.Response, instance=False)
+ response.status = status
+ response.data = _helpers.to_bytes(data) if use_data_bytes else data
+ response.headers = headers or {}
+
+ request = mock.create_autospec(transport.Request, instance=False)
+ request.side_effect = side_effect
+ request.return_value = response
+
+ return request
+
+ def test_token_usage_metrics(self):
+ credentials = self.make_credentials()
+ credentials.token = "token"
+ credentials.expiry = None
+
+ headers = {}
+ credentials.before_request(mock.Mock(), None, None, headers)
+ assert headers["authorization"] == "Bearer token"
+ assert headers["x-goog-api-client"] == "cred-type/imp"
+
+ @pytest.mark.parametrize("use_data_bytes", [True, False])
+ def test_refresh_success(self, use_data_bytes, mock_donor_credentials):
+ credentials = self.make_credentials(lifetime=None)
+ token = "token"
+
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ response_body = {"accessToken": token, "expireTime": expire_time}
+
+ request = self.make_request(
+ data=json.dumps(response_body),
+ status=http_client.OK,
+ use_data_bytes=use_data_bytes,
+ )
+
+ with mock.patch(
+ "google.auth.metrics.token_request_access_token_impersonate",
+ return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ ):
+ credentials.refresh(request)
+
+ assert credentials.valid
+ assert not credentials.expired
+ assert (
+ request.call_args.kwargs["headers"]["x-goog-api-client"]
+ == ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE
+ )
+
+ @pytest.mark.parametrize("use_data_bytes", [True, False])
+ def test_refresh_success_iam_endpoint_override(
+ self, use_data_bytes, mock_donor_credentials
+ ):
+ credentials = self.make_credentials(
+ lifetime=None, iam_endpoint_override=self.IAM_ENDPOINT_OVERRIDE
+ )
+ token = "token"
+
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ response_body = {"accessToken": token, "expireTime": expire_time}
+
+ request = self.make_request(
+ data=json.dumps(response_body),
+ status=http_client.OK,
+ use_data_bytes=use_data_bytes,
+ )
+
+ credentials.refresh(request)
+
+ assert credentials.valid
+ assert not credentials.expired
+ # Confirm override endpoint used.
+ request_kwargs = request.call_args[1]
+ assert request_kwargs["url"] == self.IAM_ENDPOINT_OVERRIDE
+
+ @pytest.mark.parametrize("time_skew", [100, -100])
+ def test_refresh_source_credentials(self, time_skew):
+ credentials = self.make_credentials(lifetime=None)
+
+ # Source credentials is refreshed only if it is expired within
+ # _helpers.REFRESH_THRESHOLD from now. We add a time_skew to the expiry, so
+ # source credentials is refreshed only if time_skew <= 0.
+ credentials._source_credentials.expiry = (
+ _helpers.utcnow()
+ + _helpers.REFRESH_THRESHOLD
+ + datetime.timedelta(seconds=time_skew)
+ )
+ credentials._source_credentials.token = "Token"
+
+ with mock.patch(
+ "google.oauth2.service_account.Credentials.refresh", autospec=True
+ ) as source_cred_refresh:
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0)
+ + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ response_body = {"accessToken": "token", "expireTime": expire_time}
+ request = self.make_request(
+ data=json.dumps(response_body), status=http_client.OK
+ )
+
+ credentials.refresh(request)
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ # Source credentials is refreshed only if it is expired within
+ # _helpers.REFRESH_THRESHOLD
+ if time_skew > 0:
+ source_cred_refresh.assert_not_called()
+ else:
+ source_cred_refresh.assert_called_once()
+
+ def test_refresh_failure_malformed_expire_time(self, mock_donor_credentials):
+ credentials = self.make_credentials(lifetime=None)
+ token = "token"
+
+ expire_time = (_helpers.utcnow() + datetime.timedelta(seconds=500)).isoformat(
+ "T"
+ )
+ response_body = {"accessToken": token, "expireTime": expire_time}
+
+ request = self.make_request(
+ data=json.dumps(response_body), status=http_client.OK
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.refresh(request)
+
+ assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
+
+ assert not credentials.valid
+ assert credentials.expired
+
+ def test_refresh_failure_unauthorzed(self, mock_donor_credentials):
+ credentials = self.make_credentials(lifetime=None)
+
+ response_body = {
+ "error": {
+ "code": 403,
+ "message": "The caller does not have permission",
+ "status": "PERMISSION_DENIED",
+ }
+ }
+
+ request = self.make_request(
+ data=json.dumps(response_body), status=http_client.UNAUTHORIZED
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.refresh(request)
+
+ assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
+
+ assert not credentials.valid
+ assert credentials.expired
+
+ def test_refresh_failure(self):
+ credentials = self.make_credentials(lifetime=None)
+ credentials.expiry = None
+ credentials.token = "token"
+ id_creds = impersonated_credentials.IDTokenCredentials(
+ credentials, target_audience="audience"
+ )
+
+ response = mock.create_autospec(transport.Response, instance=False)
+ response.status_code = http_client.UNAUTHORIZED
+ response.json = mock.Mock(return_value="failed to get ID token")
+
+ with mock.patch(
+ "google.auth.transport.requests.AuthorizedSession.post",
+ return_value=response,
+ ):
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ id_creds.refresh(None)
+
+ assert excinfo.match("Error getting ID token")
+
+ def test_refresh_failure_http_error(self, mock_donor_credentials):
+ credentials = self.make_credentials(lifetime=None)
+
+ response_body = {}
+
+ request = self.make_request(
+ data=json.dumps(response_body), status=http_client.HTTPException
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ credentials.refresh(request)
+
+ assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
+
+ assert not credentials.valid
+ assert credentials.expired
+
+ def test_expired(self):
+ credentials = self.make_credentials(lifetime=None)
+ assert credentials.expired
+
+ def test_signer(self):
+ credentials = self.make_credentials()
+ assert isinstance(credentials.signer, impersonated_credentials.Credentials)
+
+ def test_signer_email(self):
+ credentials = self.make_credentials(target_principal=self.TARGET_PRINCIPAL)
+ assert credentials.signer_email == self.TARGET_PRINCIPAL
+
+ def test_service_account_email(self):
+ credentials = self.make_credentials(target_principal=self.TARGET_PRINCIPAL)
+ assert credentials.service_account_email == self.TARGET_PRINCIPAL
+
+ def test_sign_bytes(self, mock_donor_credentials, mock_authorizedsession_sign):
+ credentials = self.make_credentials(lifetime=None)
+ token = "token"
+
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ token_response_body = {"accessToken": token, "expireTime": expire_time}
+
+ response = mock.create_autospec(transport.Response, instance=False)
+ response.status = http_client.OK
+ response.data = _helpers.to_bytes(json.dumps(token_response_body))
+
+ request = mock.create_autospec(transport.Request, instance=False)
+ request.return_value = response
+
+ credentials.refresh(request)
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ signature = credentials.sign_bytes(b"signed bytes")
+ assert signature == b"signature"
+
+ def test_sign_bytes_failure(self):
+ credentials = self.make_credentials(lifetime=None)
+
+ with mock.patch(
+ "google.auth.transport.requests.AuthorizedSession.request", autospec=True
+ ) as auth_session:
+ data = {"error": {"code": 403, "message": "unauthorized"}}
+ auth_session.return_value = MockResponse(data, http_client.FORBIDDEN)
+
+ with pytest.raises(exceptions.TransportError) as excinfo:
+ credentials.sign_bytes(b"foo")
+ assert excinfo.match("'code': 403")
+
+ def test_with_quota_project(self):
+ credentials = self.make_credentials()
+
+ quota_project_creds = credentials.with_quota_project("project-foo")
+ assert quota_project_creds._quota_project_id == "project-foo"
+
+ @pytest.mark.parametrize("use_data_bytes", [True, False])
+ def test_with_quota_project_iam_endpoint_override(
+ self, use_data_bytes, mock_donor_credentials
+ ):
+ credentials = self.make_credentials(
+ lifetime=None, iam_endpoint_override=self.IAM_ENDPOINT_OVERRIDE
+ )
+ token = "token"
+ # iam_endpoint_override should be copied to created credentials.
+ quota_project_creds = credentials.with_quota_project("project-foo")
+
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ response_body = {"accessToken": token, "expireTime": expire_time}
+
+ request = self.make_request(
+ data=json.dumps(response_body),
+ status=http_client.OK,
+ use_data_bytes=use_data_bytes,
+ )
+
+ quota_project_creds.refresh(request)
+
+ assert quota_project_creds.valid
+ assert not quota_project_creds.expired
+ # Confirm override endpoint used.
+ request_kwargs = request.call_args[1]
+ assert request_kwargs["url"] == self.IAM_ENDPOINT_OVERRIDE
+
+ def test_with_scopes(self):
+ credentials = self.make_credentials()
+ credentials._target_scopes = []
+ assert credentials.requires_scopes is True
+ credentials = credentials.with_scopes(["fake_scope1", "fake_scope2"])
+ assert credentials.requires_scopes is False
+ assert credentials._target_scopes == ["fake_scope1", "fake_scope2"]
+
+ def test_with_scopes_provide_default_scopes(self):
+ credentials = self.make_credentials()
+ credentials._target_scopes = []
+ credentials = credentials.with_scopes(
+ ["fake_scope1"], default_scopes=["fake_scope2"]
+ )
+ assert credentials._target_scopes == ["fake_scope1"]
+
+ def test_id_token_success(
+ self, mock_donor_credentials, mock_authorizedsession_idtoken
+ ):
+ credentials = self.make_credentials(lifetime=None)
+ token = "token"
+ target_audience = "https://foo.bar"
+
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ response_body = {"accessToken": token, "expireTime": expire_time}
+
+ request = self.make_request(
+ data=json.dumps(response_body), status=http_client.OK
+ )
+
+ credentials.refresh(request)
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ id_creds = impersonated_credentials.IDTokenCredentials(
+ credentials, target_audience=target_audience
+ )
+ id_creds.refresh(request)
+
+ assert id_creds.token == ID_TOKEN_DATA
+ assert id_creds.expiry == datetime.datetime.utcfromtimestamp(ID_TOKEN_EXPIRY)
+
+ def test_id_token_metrics(self, mock_donor_credentials):
+ credentials = self.make_credentials(lifetime=None)
+ credentials.token = "token"
+ credentials.expiry = None
+ target_audience = "https://foo.bar"
+
+ id_creds = impersonated_credentials.IDTokenCredentials(
+ credentials, target_audience=target_audience
+ )
+
+ with mock.patch(
+ "google.auth.metrics.token_request_id_token_impersonate",
+ return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+ ):
+ with mock.patch(
+ "google.auth.transport.requests.AuthorizedSession.post", autospec=True
+ ) as mock_post:
+ data = {"token": ID_TOKEN_DATA}
+ mock_post.return_value = MockResponse(data, http_client.OK)
+ id_creds.refresh(None)
+
+ assert id_creds.token == ID_TOKEN_DATA
+ assert id_creds.expiry == datetime.datetime.utcfromtimestamp(
+ ID_TOKEN_EXPIRY
+ )
+ assert (
+ mock_post.call_args.kwargs["headers"]["x-goog-api-client"]
+ == ID_TOKEN_REQUEST_METRICS_HEADER_VALUE
+ )
+
+ def test_id_token_from_credential(
+ self, mock_donor_credentials, mock_authorizedsession_idtoken
+ ):
+ credentials = self.make_credentials(lifetime=None)
+ token = "token"
+ target_audience = "https://foo.bar"
+
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ response_body = {"accessToken": token, "expireTime": expire_time}
+
+ request = self.make_request(
+ data=json.dumps(response_body), status=http_client.OK
+ )
+
+ credentials.refresh(request)
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ new_credentials = self.make_credentials(lifetime=None)
+
+ id_creds = impersonated_credentials.IDTokenCredentials(
+ credentials, target_audience=target_audience, include_email=True
+ )
+ id_creds = id_creds.from_credentials(target_credentials=new_credentials)
+ id_creds.refresh(request)
+
+ assert id_creds.token == ID_TOKEN_DATA
+ assert id_creds._include_email is True
+ assert id_creds._target_credentials is new_credentials
+
+ def test_id_token_with_target_audience(
+ self, mock_donor_credentials, mock_authorizedsession_idtoken
+ ):
+ credentials = self.make_credentials(lifetime=None)
+ token = "token"
+ target_audience = "https://foo.bar"
+
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ response_body = {"accessToken": token, "expireTime": expire_time}
+
+ request = self.make_request(
+ data=json.dumps(response_body), status=http_client.OK
+ )
+
+ credentials.refresh(request)
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ id_creds = impersonated_credentials.IDTokenCredentials(
+ credentials, include_email=True
+ )
+ id_creds = id_creds.with_target_audience(target_audience=target_audience)
+ id_creds.refresh(request)
+
+ assert id_creds.token == ID_TOKEN_DATA
+ assert id_creds.expiry == datetime.datetime.utcfromtimestamp(ID_TOKEN_EXPIRY)
+ assert id_creds._include_email is True
+
+ def test_id_token_invalid_cred(
+ self, mock_donor_credentials, mock_authorizedsession_idtoken
+ ):
+ credentials = None
+
+ with pytest.raises(exceptions.GoogleAuthError) as excinfo:
+ impersonated_credentials.IDTokenCredentials(credentials)
+
+ assert excinfo.match("Provided Credential must be" " impersonated_credentials")
+
+ def test_id_token_with_include_email(
+ self, mock_donor_credentials, mock_authorizedsession_idtoken
+ ):
+ credentials = self.make_credentials(lifetime=None)
+ token = "token"
+ target_audience = "https://foo.bar"
+
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ response_body = {"accessToken": token, "expireTime": expire_time}
+
+ request = self.make_request(
+ data=json.dumps(response_body), status=http_client.OK
+ )
+
+ credentials.refresh(request)
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ id_creds = impersonated_credentials.IDTokenCredentials(
+ credentials, target_audience=target_audience
+ )
+ id_creds = id_creds.with_include_email(True)
+ id_creds.refresh(request)
+
+ assert id_creds.token == ID_TOKEN_DATA
+
+ def test_id_token_with_quota_project(
+ self, mock_donor_credentials, mock_authorizedsession_idtoken
+ ):
+ credentials = self.make_credentials(lifetime=None)
+ token = "token"
+ target_audience = "https://foo.bar"
+
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ response_body = {"accessToken": token, "expireTime": expire_time}
+
+ request = self.make_request(
+ data=json.dumps(response_body), status=http_client.OK
+ )
+
+ credentials.refresh(request)
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ id_creds = impersonated_credentials.IDTokenCredentials(
+ credentials, target_audience=target_audience
+ )
+ id_creds = id_creds.with_quota_project("project-foo")
+ id_creds.refresh(request)
+
+ assert id_creds.quota_project_id == "project-foo"
diff --git a/contrib/python/google-auth/py3/tests/test_jwt.py b/contrib/python/google-auth/py3/tests/test_jwt.py
new file mode 100644
index 0000000000..62f310606d
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_jwt.py
@@ -0,0 +1,671 @@
+# Copyright 2014 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import datetime
+import json
+import os
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import exceptions
+from google.auth import jwt
+
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+ PRIVATE_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+ PUBLIC_CERT_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "other_cert.pem"), "rb") as fh:
+ OTHER_CERT_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "es256_privatekey.pem"), "rb") as fh:
+ EC_PRIVATE_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "es256_public_cert.pem"), "rb") as fh:
+ EC_PUBLIC_CERT_BYTES = fh.read()
+
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh:
+ SERVICE_ACCOUNT_INFO = json.load(fh)
+
+
+@pytest.fixture
+def signer():
+ return crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1")
+
+
+def test_encode_basic(signer):
+ test_payload = {"test": "value"}
+ encoded = jwt.encode(signer, test_payload)
+ header, payload, _, _ = jwt._unverified_decode(encoded)
+ assert payload == test_payload
+ assert header == {"typ": "JWT", "alg": "RS256", "kid": signer.key_id}
+
+
+def test_encode_extra_headers(signer):
+ encoded = jwt.encode(signer, {}, header={"extra": "value"})
+ header = jwt.decode_header(encoded)
+ assert header == {
+ "typ": "JWT",
+ "alg": "RS256",
+ "kid": signer.key_id,
+ "extra": "value",
+ }
+
+
+def test_encode_custom_alg_in_headers(signer):
+ encoded = jwt.encode(signer, {}, header={"alg": "foo"})
+ header = jwt.decode_header(encoded)
+ assert header == {"typ": "JWT", "alg": "foo", "kid": signer.key_id}
+
+
+@pytest.fixture
+def es256_signer():
+ return crypt.ES256Signer.from_string(EC_PRIVATE_KEY_BYTES, "1")
+
+
+def test_encode_basic_es256(es256_signer):
+ test_payload = {"test": "value"}
+ encoded = jwt.encode(es256_signer, test_payload)
+ header, payload, _, _ = jwt._unverified_decode(encoded)
+ assert payload == test_payload
+ assert header == {"typ": "JWT", "alg": "ES256", "kid": es256_signer.key_id}
+
+
+@pytest.fixture
+def token_factory(signer, es256_signer):
+ def factory(claims=None, key_id=None, use_es256_signer=False):
+ now = _helpers.datetime_to_secs(_helpers.utcnow())
+ payload = {
+ "aud": "audience@example.com",
+ "iat": now,
+ "exp": now + 300,
+ "user": "billy bob",
+ "metadata": {"meta": "data"},
+ }
+ payload.update(claims or {})
+
+ # False is specified to remove the signer's key id for testing
+ # headers without key ids.
+ if key_id is False:
+ signer._key_id = None
+ key_id = None
+
+ if use_es256_signer:
+ return jwt.encode(es256_signer, payload, key_id=key_id)
+ else:
+ return jwt.encode(signer, payload, key_id=key_id)
+
+ return factory
+
+
+def test_decode_valid(token_factory):
+ payload = jwt.decode(token_factory(), certs=PUBLIC_CERT_BYTES)
+ assert payload["aud"] == "audience@example.com"
+ assert payload["user"] == "billy bob"
+ assert payload["metadata"]["meta"] == "data"
+
+
+def test_decode_header_object(token_factory):
+ payload = token_factory()
+ # Create a malformed JWT token with a number as a header instead of a
+ # dictionary (3 == base64d(M7==))
+ payload = b"M7." + b".".join(payload.split(b".")[1:])
+
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(payload, certs=PUBLIC_CERT_BYTES)
+ assert excinfo.match(r"Header segment should be a JSON object: " + str(b"M7"))
+
+
+def test_decode_payload_object(signer):
+ # Create a malformed JWT token with a payload containing both "iat" and
+ # "exp" strings, although not as fields of a dictionary
+ payload = jwt.encode(signer, "iatexp")
+
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(payload, certs=PUBLIC_CERT_BYTES)
+ assert excinfo.match(
+ r"Payload segment should be a JSON object: " + str(b"ImlhdGV4cCI")
+ )
+
+
+def test_decode_valid_es256(token_factory):
+ payload = jwt.decode(
+ token_factory(use_es256_signer=True), certs=EC_PUBLIC_CERT_BYTES
+ )
+ assert payload["aud"] == "audience@example.com"
+ assert payload["user"] == "billy bob"
+ assert payload["metadata"]["meta"] == "data"
+
+
+def test_decode_valid_with_audience(token_factory):
+ payload = jwt.decode(
+ token_factory(), certs=PUBLIC_CERT_BYTES, audience="audience@example.com"
+ )
+ assert payload["aud"] == "audience@example.com"
+ assert payload["user"] == "billy bob"
+ assert payload["metadata"]["meta"] == "data"
+
+
+def test_decode_valid_with_audience_list(token_factory):
+ payload = jwt.decode(
+ token_factory(),
+ certs=PUBLIC_CERT_BYTES,
+ audience=["audience@example.com", "another_audience@example.com"],
+ )
+ assert payload["aud"] == "audience@example.com"
+ assert payload["user"] == "billy bob"
+ assert payload["metadata"]["meta"] == "data"
+
+
+def test_decode_valid_unverified(token_factory):
+ payload = jwt.decode(token_factory(), certs=OTHER_CERT_BYTES, verify=False)
+ assert payload["aud"] == "audience@example.com"
+ assert payload["user"] == "billy bob"
+ assert payload["metadata"]["meta"] == "data"
+
+
+def test_decode_bad_token_wrong_number_of_segments():
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode("1.2", PUBLIC_CERT_BYTES)
+ assert excinfo.match(r"Wrong number of segments")
+
+
+def test_decode_bad_token_not_base64():
+ with pytest.raises((ValueError, TypeError)) as excinfo:
+ jwt.decode("1.2.3", PUBLIC_CERT_BYTES)
+ assert excinfo.match(r"Incorrect padding|more than a multiple of 4")
+
+
+def test_decode_bad_token_not_json():
+ token = b".".join([base64.urlsafe_b64encode(b"123!")] * 3)
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token, PUBLIC_CERT_BYTES)
+ assert excinfo.match(r"Can\'t parse segment")
+
+
+def test_decode_bad_token_no_iat_or_exp(signer):
+ token = jwt.encode(signer, {"test": "value"})
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token, PUBLIC_CERT_BYTES)
+ assert excinfo.match(r"Token does not contain required claim")
+
+
+def test_decode_bad_token_too_early(token_factory):
+ token = token_factory(
+ claims={
+ "iat": _helpers.datetime_to_secs(
+ _helpers.utcnow() + datetime.timedelta(hours=1)
+ )
+ }
+ )
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=59)
+ assert excinfo.match(r"Token used too early")
+
+
+def test_decode_bad_token_expired(token_factory):
+ token = token_factory(
+ claims={
+ "exp": _helpers.datetime_to_secs(
+ _helpers.utcnow() - datetime.timedelta(hours=1)
+ )
+ }
+ )
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=59)
+ assert excinfo.match(r"Token expired")
+
+
+def test_decode_success_with_no_clock_skew(token_factory):
+ token = token_factory(
+ claims={
+ "exp": _helpers.datetime_to_secs(
+ _helpers.utcnow() + datetime.timedelta(seconds=1)
+ ),
+ "iat": _helpers.datetime_to_secs(
+ _helpers.utcnow() - datetime.timedelta(seconds=1)
+ ),
+ }
+ )
+
+ jwt.decode(token, PUBLIC_CERT_BYTES)
+
+
+def test_decode_success_with_custom_clock_skew(token_factory):
+ token = token_factory(
+ claims={
+ "exp": _helpers.datetime_to_secs(
+ _helpers.utcnow() + datetime.timedelta(seconds=2)
+ ),
+ "iat": _helpers.datetime_to_secs(
+ _helpers.utcnow() - datetime.timedelta(seconds=2)
+ ),
+ }
+ )
+
+ jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=1)
+
+
+def test_decode_bad_token_wrong_audience(token_factory):
+ token = token_factory()
+ audience = "audience2@example.com"
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token, PUBLIC_CERT_BYTES, audience=audience)
+ assert excinfo.match(r"Token has wrong audience")
+
+
+def test_decode_bad_token_wrong_audience_list(token_factory):
+ token = token_factory()
+ audience = ["audience2@example.com", "audience3@example.com"]
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token, PUBLIC_CERT_BYTES, audience=audience)
+ assert excinfo.match(r"Token has wrong audience")
+
+
+def test_decode_wrong_cert(token_factory):
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token_factory(), OTHER_CERT_BYTES)
+ assert excinfo.match(r"Could not verify token signature")
+
+
+def test_decode_multicert_bad_cert(token_factory):
+ certs = {"1": OTHER_CERT_BYTES, "2": PUBLIC_CERT_BYTES}
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token_factory(), certs)
+ assert excinfo.match(r"Could not verify token signature")
+
+
+def test_decode_no_cert(token_factory):
+ certs = {"2": PUBLIC_CERT_BYTES}
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token_factory(), certs)
+ assert excinfo.match(r"Certificate for key id 1 not found")
+
+
+def test_decode_no_key_id(token_factory):
+ token = token_factory(key_id=False)
+ certs = {"2": PUBLIC_CERT_BYTES}
+ payload = jwt.decode(token, certs)
+ assert payload["user"] == "billy bob"
+
+
+def test_decode_unknown_alg():
+ headers = json.dumps({u"kid": u"1", u"alg": u"fakealg"})
+ token = b".".join(
+ map(lambda seg: base64.b64encode(seg.encode("utf-8")), [headers, u"{}", u"sig"])
+ )
+
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token)
+ assert excinfo.match(r"fakealg")
+
+
+def test_decode_missing_crytography_alg(monkeypatch):
+ monkeypatch.delitem(jwt._ALGORITHM_TO_VERIFIER_CLASS, "ES256")
+ headers = json.dumps({u"kid": u"1", u"alg": u"ES256"})
+ token = b".".join(
+ map(lambda seg: base64.b64encode(seg.encode("utf-8")), [headers, u"{}", u"sig"])
+ )
+
+ with pytest.raises(ValueError) as excinfo:
+ jwt.decode(token)
+ assert excinfo.match(r"cryptography")
+
+
+def test_roundtrip_explicit_key_id(token_factory):
+ token = token_factory(key_id="3")
+ certs = {"2": OTHER_CERT_BYTES, "3": PUBLIC_CERT_BYTES}
+ payload = jwt.decode(token, certs)
+ assert payload["user"] == "billy bob"
+
+
+class TestCredentials(object):
+ SERVICE_ACCOUNT_EMAIL = "service-account@example.com"
+ SUBJECT = "subject"
+ AUDIENCE = "audience"
+ ADDITIONAL_CLAIMS = {"meta": "data"}
+ credentials = None
+
+ @pytest.fixture(autouse=True)
+ def credentials_fixture(self, signer):
+ self.credentials = jwt.Credentials(
+ signer,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.AUDIENCE,
+ )
+
+ def test_from_service_account_info(self):
+ with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+ info = json.load(fh)
+
+ credentials = jwt.Credentials.from_service_account_info(
+ info, audience=self.AUDIENCE
+ )
+
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._issuer == info["client_email"]
+ assert credentials._subject == info["client_email"]
+ assert credentials._audience == self.AUDIENCE
+
+ def test_from_service_account_info_args(self):
+ info = SERVICE_ACCOUNT_INFO.copy()
+
+ credentials = jwt.Credentials.from_service_account_info(
+ info,
+ subject=self.SUBJECT,
+ audience=self.AUDIENCE,
+ additional_claims=self.ADDITIONAL_CLAIMS,
+ )
+
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._issuer == info["client_email"]
+ assert credentials._subject == self.SUBJECT
+ assert credentials._audience == self.AUDIENCE
+ assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+ def test_from_service_account_file(self):
+ info = SERVICE_ACCOUNT_INFO.copy()
+
+ credentials = jwt.Credentials.from_service_account_file(
+ SERVICE_ACCOUNT_JSON_FILE, audience=self.AUDIENCE
+ )
+
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._issuer == info["client_email"]
+ assert credentials._subject == info["client_email"]
+ assert credentials._audience == self.AUDIENCE
+
+ def test_from_service_account_file_args(self):
+ info = SERVICE_ACCOUNT_INFO.copy()
+
+ credentials = jwt.Credentials.from_service_account_file(
+ SERVICE_ACCOUNT_JSON_FILE,
+ subject=self.SUBJECT,
+ audience=self.AUDIENCE,
+ additional_claims=self.ADDITIONAL_CLAIMS,
+ )
+
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._issuer == info["client_email"]
+ assert credentials._subject == self.SUBJECT
+ assert credentials._audience == self.AUDIENCE
+ assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+ def test_from_signing_credentials(self):
+ jwt_from_signing = self.credentials.from_signing_credentials(
+ self.credentials, audience=mock.sentinel.new_audience
+ )
+ jwt_from_info = jwt.Credentials.from_service_account_info(
+ SERVICE_ACCOUNT_INFO, audience=mock.sentinel.new_audience
+ )
+
+ assert isinstance(jwt_from_signing, jwt.Credentials)
+ assert jwt_from_signing._signer.key_id == jwt_from_info._signer.key_id
+ assert jwt_from_signing._issuer == jwt_from_info._issuer
+ assert jwt_from_signing._subject == jwt_from_info._subject
+ assert jwt_from_signing._audience == jwt_from_info._audience
+
+ def test_default_state(self):
+ assert not self.credentials.valid
+ # Expiration hasn't been set yet
+ assert not self.credentials.expired
+
+ def test_with_claims(self):
+ new_audience = "new_audience"
+ new_credentials = self.credentials.with_claims(audience=new_audience)
+
+ assert new_credentials._signer == self.credentials._signer
+ assert new_credentials._issuer == self.credentials._issuer
+ assert new_credentials._subject == self.credentials._subject
+ assert new_credentials._audience == new_audience
+ assert new_credentials._additional_claims == self.credentials._additional_claims
+ assert new_credentials._quota_project_id == self.credentials._quota_project_id
+
+ def test__make_jwt_without_audience(self):
+ cred = jwt.Credentials.from_service_account_info(
+ SERVICE_ACCOUNT_INFO.copy(),
+ subject=self.SUBJECT,
+ audience=None,
+ additional_claims={"scope": "foo bar"},
+ )
+ token, _ = cred._make_jwt()
+ payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+ assert payload["scope"] == "foo bar"
+ assert "aud" not in payload
+
+ def test_with_quota_project(self):
+ quota_project_id = "project-foo"
+
+ new_credentials = self.credentials.with_quota_project(quota_project_id)
+ assert new_credentials._signer == self.credentials._signer
+ assert new_credentials._issuer == self.credentials._issuer
+ assert new_credentials._subject == self.credentials._subject
+ assert new_credentials._audience == self.credentials._audience
+ assert new_credentials._additional_claims == self.credentials._additional_claims
+ assert new_credentials.additional_claims == self.credentials._additional_claims
+ assert new_credentials._quota_project_id == quota_project_id
+
+ def test_sign_bytes(self):
+ to_sign = b"123"
+ signature = self.credentials.sign_bytes(to_sign)
+ assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
+
+ def test_signer(self):
+ assert isinstance(self.credentials.signer, crypt.RSASigner)
+
+ def test_signer_email(self):
+ assert self.credentials.signer_email == SERVICE_ACCOUNT_INFO["client_email"]
+
+ def _verify_token(self, token):
+ payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+ assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+ return payload
+
+ def test_refresh(self):
+ self.credentials.refresh(None)
+ assert self.credentials.valid
+ assert not self.credentials.expired
+
+ def test_expired(self):
+ assert not self.credentials.expired
+
+ self.credentials.refresh(None)
+ assert not self.credentials.expired
+
+ with mock.patch("google.auth._helpers.utcnow") as now:
+ one_day = datetime.timedelta(days=1)
+ now.return_value = self.credentials.expiry + one_day
+ assert self.credentials.expired
+
+ def test_before_request(self):
+ headers = {}
+
+ self.credentials.refresh(None)
+ self.credentials.before_request(
+ None, "GET", "http://example.com?a=1#3", headers
+ )
+
+ header_value = headers["authorization"]
+ _, token = header_value.split(" ")
+
+ # Since the audience is set, it should use the existing token.
+ assert token.encode("utf-8") == self.credentials.token
+
+ payload = self._verify_token(token)
+ assert payload["aud"] == self.AUDIENCE
+
+ def test_before_request_refreshes(self):
+ assert not self.credentials.valid
+ self.credentials.before_request(None, "GET", "http://example.com?a=1#3", {})
+ assert self.credentials.valid
+
+
+class TestOnDemandCredentials(object):
+ SERVICE_ACCOUNT_EMAIL = "service-account@example.com"
+ SUBJECT = "subject"
+ ADDITIONAL_CLAIMS = {"meta": "data"}
+ credentials = None
+
+ @pytest.fixture(autouse=True)
+ def credentials_fixture(self, signer):
+ self.credentials = jwt.OnDemandCredentials(
+ signer,
+ self.SERVICE_ACCOUNT_EMAIL,
+ self.SERVICE_ACCOUNT_EMAIL,
+ max_cache_size=2,
+ )
+
+ def test_from_service_account_info(self):
+ with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+ info = json.load(fh)
+
+ credentials = jwt.OnDemandCredentials.from_service_account_info(info)
+
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._issuer == info["client_email"]
+ assert credentials._subject == info["client_email"]
+
+ def test_from_service_account_info_args(self):
+ info = SERVICE_ACCOUNT_INFO.copy()
+
+ credentials = jwt.OnDemandCredentials.from_service_account_info(
+ info, subject=self.SUBJECT, additional_claims=self.ADDITIONAL_CLAIMS
+ )
+
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._issuer == info["client_email"]
+ assert credentials._subject == self.SUBJECT
+ assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+ def test_from_service_account_file(self):
+ info = SERVICE_ACCOUNT_INFO.copy()
+
+ credentials = jwt.OnDemandCredentials.from_service_account_file(
+ SERVICE_ACCOUNT_JSON_FILE
+ )
+
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._issuer == info["client_email"]
+ assert credentials._subject == info["client_email"]
+
+ def test_from_service_account_file_args(self):
+ info = SERVICE_ACCOUNT_INFO.copy()
+
+ credentials = jwt.OnDemandCredentials.from_service_account_file(
+ SERVICE_ACCOUNT_JSON_FILE,
+ subject=self.SUBJECT,
+ additional_claims=self.ADDITIONAL_CLAIMS,
+ )
+
+ assert credentials._signer.key_id == info["private_key_id"]
+ assert credentials._issuer == info["client_email"]
+ assert credentials._subject == self.SUBJECT
+ assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+ def test_from_signing_credentials(self):
+ jwt_from_signing = self.credentials.from_signing_credentials(self.credentials)
+ jwt_from_info = jwt.OnDemandCredentials.from_service_account_info(
+ SERVICE_ACCOUNT_INFO
+ )
+
+ assert isinstance(jwt_from_signing, jwt.OnDemandCredentials)
+ assert jwt_from_signing._signer.key_id == jwt_from_info._signer.key_id
+ assert jwt_from_signing._issuer == jwt_from_info._issuer
+ assert jwt_from_signing._subject == jwt_from_info._subject
+
+ def test_default_state(self):
+ # Credentials are *always* valid.
+ assert self.credentials.valid
+ # Credentials *never* expire.
+ assert not self.credentials.expired
+
+ def test_with_claims(self):
+ new_claims = {"meep": "moop"}
+ new_credentials = self.credentials.with_claims(additional_claims=new_claims)
+
+ assert new_credentials._signer == self.credentials._signer
+ assert new_credentials._issuer == self.credentials._issuer
+ assert new_credentials._subject == self.credentials._subject
+ assert new_credentials._additional_claims == new_claims
+
+ def test_with_quota_project(self):
+ quota_project_id = "project-foo"
+ new_credentials = self.credentials.with_quota_project(quota_project_id)
+
+ assert new_credentials._signer == self.credentials._signer
+ assert new_credentials._issuer == self.credentials._issuer
+ assert new_credentials._subject == self.credentials._subject
+ assert new_credentials._additional_claims == self.credentials._additional_claims
+ assert new_credentials._quota_project_id == quota_project_id
+
+ def test_sign_bytes(self):
+ to_sign = b"123"
+ signature = self.credentials.sign_bytes(to_sign)
+ assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
+
+ def test_signer(self):
+ assert isinstance(self.credentials.signer, crypt.RSASigner)
+
+ def test_signer_email(self):
+ assert self.credentials.signer_email == SERVICE_ACCOUNT_INFO["client_email"]
+
+ def _verify_token(self, token):
+ payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+ assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+ return payload
+
+ def test_refresh(self):
+ with pytest.raises(exceptions.RefreshError):
+ self.credentials.refresh(None)
+
+ def test_before_request(self):
+ headers = {}
+
+ self.credentials.before_request(
+ None, "GET", "http://example.com?a=1#3", headers
+ )
+
+ _, token = headers["authorization"].split(" ")
+ payload = self._verify_token(token)
+
+ assert payload["aud"] == "http://example.com"
+
+ # Making another request should re-use the same token.
+ self.credentials.before_request(None, "GET", "http://example.com?b=2", headers)
+
+ _, new_token = headers["authorization"].split(" ")
+
+ assert new_token == token
+
+ def test_expired_token(self):
+ self.credentials._cache["audience"] = (
+ mock.sentinel.token,
+ datetime.datetime.min,
+ )
+
+ token = self.credentials._get_jwt_for_audience("audience")
+
+ assert token != mock.sentinel.token
diff --git a/contrib/python/google-auth/py3/tests/test_metrics.py b/contrib/python/google-auth/py3/tests/test_metrics.py
new file mode 100644
index 0000000000..ba93892674
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_metrics.py
@@ -0,0 +1,96 @@
+# Copyright 2014 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import platform
+
+import mock
+
+from google.auth import metrics
+from google.auth import version
+
+
+def test_add_metric_header():
+ headers = {}
+ metrics.add_metric_header(headers, None)
+ assert headers == {}
+
+ headers = {"x-goog-api-client": "foo"}
+ metrics.add_metric_header(headers, "bar")
+ assert headers == {"x-goog-api-client": "foo bar"}
+
+ headers = {}
+ metrics.add_metric_header(headers, "bar")
+ assert headers == {"x-goog-api-client": "bar"}
+
+
+@mock.patch.object(platform, "python_version", return_value="3.7")
+def test_versions(mock_python_version):
+ version_save = version.__version__
+ version.__version__ = "1.1"
+ assert metrics.python_and_auth_lib_version() == "gl-python/3.7 auth/1.1"
+ version.__version__ = version_save
+
+
+@mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value="gl-python/3.7 auth/1.1",
+)
+def test_metric_values(mock_python_and_auth_lib_version):
+ assert (
+ metrics.token_request_access_token_mds()
+ == "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/mds"
+ )
+ assert (
+ metrics.token_request_id_token_mds()
+ == "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/mds"
+ )
+ assert (
+ metrics.token_request_access_token_impersonate()
+ == "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
+ )
+ assert (
+ metrics.token_request_id_token_impersonate()
+ == "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/imp"
+ )
+ assert (
+ metrics.token_request_access_token_sa_assertion()
+ == "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/sa"
+ )
+ assert (
+ metrics.token_request_id_token_sa_assertion()
+ == "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/sa"
+ )
+ assert metrics.token_request_user() == "gl-python/3.7 auth/1.1 cred-type/u"
+ assert metrics.mds_ping() == "gl-python/3.7 auth/1.1 auth-request-type/mds"
+ assert metrics.reauth_start() == "gl-python/3.7 auth/1.1 auth-request-type/re-start"
+ assert (
+ metrics.reauth_continue() == "gl-python/3.7 auth/1.1 auth-request-type/re-cont"
+ )
+
+
+@mock.patch(
+ "google.auth.metrics.python_and_auth_lib_version",
+ return_value="gl-python/3.7 auth/1.1",
+)
+def test_byoid_metric_header(mock_python_and_auth_lib_version):
+ metrics_options = {}
+ assert (
+ metrics.byoid_metrics_header(metrics_options)
+ == "gl-python/3.7 auth/1.1 google-byoid-sdk"
+ )
+ metrics_options["testKey"] = "testValue"
+ assert (
+ metrics.byoid_metrics_header(metrics_options)
+ == "gl-python/3.7 auth/1.1 google-byoid-sdk testKey/testValue"
+ )
diff --git a/contrib/python/google-auth/py3/tests/test_packaging.py b/contrib/python/google-auth/py3/tests/test_packaging.py
new file mode 100644
index 0000000000..e87b3a21b9
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_packaging.py
@@ -0,0 +1,30 @@
+# Copyright 2022 Google LLC
+#
+# 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 os
+import subprocess
+import sys
+
+
+def test_namespace_package_compat(tmp_path):
+ """
+ The ``google`` namespace package should not be masked
+ by the presence of ``google-auth``.
+ """
+ google = tmp_path / "google"
+ google.mkdir()
+ google.joinpath("othermod.py").write_text("")
+ env = dict(os.environ, PYTHONPATH=str(tmp_path))
+ cmd = [sys.executable, "-m", "google.othermod"]
+ subprocess.check_call(cmd, env=env)
diff --git a/contrib/python/google-auth/py3/tests/test_pluggable.py b/contrib/python/google-auth/py3/tests/test_pluggable.py
new file mode 100644
index 0000000000..783bbcaec0
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/test_pluggable.py
@@ -0,0 +1,1250 @@
+# Copyright 2022 Google LLC
+#
+# 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 json
+import os
+import subprocess
+
+import mock
+import pytest # type: ignore
+
+from google.auth import exceptions
+from google.auth import pluggable
+from .test__default import WORKFORCE_AUDIENCE
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password".
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com"
+SERVICE_ACCOUNT_IMPERSONATION_URL_BASE = (
+ "https://us-east1-iamcredentials.googleapis.com"
+)
+SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE = "/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
+ SERVICE_ACCOUNT_EMAIL
+)
+SERVICE_ACCOUNT_IMPERSONATION_URL = (
+ SERVICE_ACCOUNT_IMPERSONATION_URL_BASE + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
+)
+QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID"
+SCOPES = ["scope1", "scope2"]
+SUBJECT_TOKEN_FIELD_NAME = "access_token"
+
+TOKEN_URL = "https://sts.googleapis.com/v1/token"
+TOKEN_INFO_URL = "https://sts.googleapis.com/v1/introspect"
+SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
+AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
+DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
+
+VALID_TOKEN_URLS = [
+ "https://sts.googleapis.com",
+ "https://us-east-1.sts.googleapis.com",
+ "https://US-EAST-1.sts.googleapis.com",
+ "https://sts.us-east-1.googleapis.com",
+ "https://sts.US-WEST-1.googleapis.com",
+ "https://us-east-1-sts.googleapis.com",
+ "https://US-WEST-1-sts.googleapis.com",
+ "https://us-west-1-sts.googleapis.com/path?query",
+ "https://sts-us-east-1.p.googleapis.com",
+]
+INVALID_TOKEN_URLS = [
+ "https://iamcredentials.googleapis.com",
+ "sts.googleapis.com",
+ "https://",
+ "http://sts.googleapis.com",
+ "https://st.s.googleapis.com",
+ "https://us-eas\t-1.sts.googleapis.com",
+ "https:/us-east-1.sts.googleapis.com",
+ "https://US-WE/ST-1-sts.googleapis.com",
+ "https://sts-us-east-1.googleapis.com",
+ "https://sts-US-WEST-1.googleapis.com",
+ "testhttps://us-east-1.sts.googleapis.com",
+ "https://us-east-1.sts.googleapis.comevil.com",
+ "https://us-east-1.us-east-1.sts.googleapis.com",
+ "https://us-ea.s.t.sts.googleapis.com",
+ "https://sts.googleapis.comevil.com",
+ "hhttps://us-east-1.sts.googleapis.com",
+ "https://us- -1.sts.googleapis.com",
+ "https://-sts.googleapis.com",
+ "https://us-east-1.sts.googleapis.com.evil.com",
+ "https://sts.pgoogleapis.com",
+ "https://p.googleapis.com",
+ "https://sts.p.com",
+ "http://sts.p.googleapis.com",
+ "https://xyz-sts.p.googleapis.com",
+ "https://sts-xyz.123.p.googleapis.com",
+ "https://sts-xyz.p1.googleapis.com",
+ "https://sts-xyz.p.foo.com",
+ "https://sts-xyz.p.foo.googleapis.com",
+]
+VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [
+ "https://iamcredentials.googleapis.com",
+ "https://us-east-1.iamcredentials.googleapis.com",
+ "https://US-EAST-1.iamcredentials.googleapis.com",
+ "https://iamcredentials.us-east-1.googleapis.com",
+ "https://iamcredentials.US-WEST-1.googleapis.com",
+ "https://us-east-1-iamcredentials.googleapis.com",
+ "https://US-WEST-1-iamcredentials.googleapis.com",
+ "https://us-west-1-iamcredentials.googleapis.com/path?query",
+ "https://iamcredentials-us-east-1.p.googleapis.com",
+]
+INVALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [
+ "https://sts.googleapis.com",
+ "iamcredentials.googleapis.com",
+ "https://",
+ "http://iamcredentials.googleapis.com",
+ "https://iamcre.dentials.googleapis.com",
+ "https://us-eas\t-1.iamcredentials.googleapis.com",
+ "https:/us-east-1.iamcredentials.googleapis.com",
+ "https://US-WE/ST-1-iamcredentials.googleapis.com",
+ "https://iamcredentials-us-east-1.googleapis.com",
+ "https://iamcredentials-US-WEST-1.googleapis.com",
+ "testhttps://us-east-1.iamcredentials.googleapis.com",
+ "https://us-east-1.iamcredentials.googleapis.comevil.com",
+ "https://us-east-1.us-east-1.iamcredentials.googleapis.com",
+ "https://us-ea.s.t.iamcredentials.googleapis.com",
+ "https://iamcredentials.googleapis.comevil.com",
+ "hhttps://us-east-1.iamcredentials.googleapis.com",
+ "https://us- -1.iamcredentials.googleapis.com",
+ "https://-iamcredentials.googleapis.com",
+ "https://us-east-1.iamcredentials.googleapis.com.evil.com",
+ "https://iamcredentials.pgoogleapis.com",
+ "https://p.googleapis.com",
+ "https://iamcredentials.p.com",
+ "http://iamcredentials.p.googleapis.com",
+ "https://xyz-iamcredentials.p.googleapis.com",
+ "https://iamcredentials-xyz.123.p.googleapis.com",
+ "https://iamcredentials-xyz.p1.googleapis.com",
+ "https://iamcredentials-xyz.p.foo.com",
+ "https://iamcredentials-xyz.p.foo.googleapis.com",
+]
+
+
+class TestCredentials(object):
+ CREDENTIAL_SOURCE_EXECUTABLE_COMMAND = (
+ "/fake/external/excutable --arg1=value1 --arg2=value2"
+ )
+ CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = "fake_output_file"
+ CREDENTIAL_SOURCE_EXECUTABLE = {
+ "command": CREDENTIAL_SOURCE_EXECUTABLE_COMMAND,
+ "timeout_millis": 30000,
+ "interactive_timeout_millis": 300000,
+ "output_file": CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ CREDENTIAL_SOURCE = {"executable": CREDENTIAL_SOURCE_EXECUTABLE}
+ EXECUTABLE_OIDC_TOKEN = "FAKE_ID_TOKEN"
+ EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN = {
+ "version": 1,
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "id_token": EXECUTABLE_OIDC_TOKEN,
+ "expiration_time": 9999999999,
+ }
+ EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_ID_TOKEN = {
+ "version": 1,
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "id_token": EXECUTABLE_OIDC_TOKEN,
+ }
+ EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_JWT = {
+ "version": 1,
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:jwt",
+ "id_token": EXECUTABLE_OIDC_TOKEN,
+ "expiration_time": 9999999999,
+ }
+ EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_JWT = {
+ "version": 1,
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:jwt",
+ "id_token": EXECUTABLE_OIDC_TOKEN,
+ }
+ EXECUTABLE_SAML_TOKEN = "FAKE_SAML_RESPONSE"
+ EXECUTABLE_SUCCESSFUL_SAML_RESPONSE = {
+ "version": 1,
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:saml2",
+ "saml_response": EXECUTABLE_SAML_TOKEN,
+ "expiration_time": 9999999999,
+ }
+ EXECUTABLE_SUCCESSFUL_SAML_NO_EXPIRATION_TIME_RESPONSE = {
+ "version": 1,
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:saml2",
+ "saml_response": EXECUTABLE_SAML_TOKEN,
+ }
+ EXECUTABLE_FAILED_RESPONSE = {
+ "version": 1,
+ "success": False,
+ "code": "401",
+ "message": "Permission denied. Caller not authorized",
+ }
+ CREDENTIAL_URL = "http://fakeurl.com"
+
+ @classmethod
+ def make_pluggable(
+ cls,
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ client_id=None,
+ client_secret=None,
+ quota_project_id=None,
+ scopes=None,
+ default_scopes=None,
+ service_account_impersonation_url=None,
+ credential_source=None,
+ workforce_pool_user_project=None,
+ interactive=None,
+ ):
+ return pluggable.Credentials(
+ audience=audience,
+ subject_token_type=subject_token_type,
+ token_url=token_url,
+ token_info_url=token_info_url,
+ service_account_impersonation_url=service_account_impersonation_url,
+ credential_source=credential_source,
+ client_id=client_id,
+ client_secret=client_secret,
+ quota_project_id=quota_project_id,
+ scopes=scopes,
+ default_scopes=default_scopes,
+ workforce_pool_user_project=workforce_pool_user_project,
+ interactive=interactive,
+ )
+
+ def test_from_constructor_and_injection(self):
+ credentials = pluggable.Credentials(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ interactive=True,
+ )
+ setattr(credentials, "_tokeninfo_username", "mock_external_account_id")
+
+ assert isinstance(credentials, pluggable.Credentials)
+ assert credentials.interactive
+ assert credentials.external_account_id == "mock_external_account_id"
+
+ @mock.patch.object(pluggable.Credentials, "__init__", return_value=None)
+ def test_from_info_full_options(self, mock_init):
+ credentials = pluggable.Credentials.from_info(
+ {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "service_account_impersonation": {"token_lifetime_seconds": 2800},
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ "quota_project_id": QUOTA_PROJECT_ID,
+ "credential_source": self.CREDENTIAL_SOURCE,
+ }
+ )
+
+ # Confirm pluggable.Credentials instantiated with expected attributes.
+ assert isinstance(credentials, pluggable.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ credential_source=self.CREDENTIAL_SOURCE,
+ quota_project_id=QUOTA_PROJECT_ID,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(pluggable.Credentials, "__init__", return_value=None)
+ def test_from_info_required_options_only(self, mock_init):
+ credentials = pluggable.Credentials.from_info(
+ {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE,
+ }
+ )
+
+ # Confirm pluggable.Credentials instantiated with expected attributes.
+ assert isinstance(credentials, pluggable.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=None,
+ service_account_impersonation_url=None,
+ service_account_impersonation_options={},
+ client_id=None,
+ client_secret=None,
+ credential_source=self.CREDENTIAL_SOURCE,
+ quota_project_id=None,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(pluggable.Credentials, "__init__", return_value=None)
+ def test_from_file_full_options(self, mock_init, tmpdir):
+ info = {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "service_account_impersonation": {"token_lifetime_seconds": 2800},
+ "client_id": CLIENT_ID,
+ "client_secret": CLIENT_SECRET,
+ "quota_project_id": QUOTA_PROJECT_ID,
+ "credential_source": self.CREDENTIAL_SOURCE,
+ }
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(info))
+ credentials = pluggable.Credentials.from_file(str(config_file))
+
+ # Confirm pluggable.Credentials instantiated with expected attributes.
+ assert isinstance(credentials, pluggable.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=TOKEN_INFO_URL,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ service_account_impersonation_options={"token_lifetime_seconds": 2800},
+ client_id=CLIENT_ID,
+ client_secret=CLIENT_SECRET,
+ credential_source=self.CREDENTIAL_SOURCE,
+ quota_project_id=QUOTA_PROJECT_ID,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ @mock.patch.object(pluggable.Credentials, "__init__", return_value=None)
+ def test_from_file_required_options_only(self, mock_init, tmpdir):
+ info = {
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE,
+ }
+ config_file = tmpdir.join("config.json")
+ config_file.write(json.dumps(info))
+ credentials = pluggable.Credentials.from_file(str(config_file))
+
+ # Confirm pluggable.Credentials instantiated with expected attributes.
+ assert isinstance(credentials, pluggable.Credentials)
+ mock_init.assert_called_once_with(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ token_url=TOKEN_URL,
+ token_info_url=None,
+ service_account_impersonation_url=None,
+ service_account_impersonation_options={},
+ client_id=None,
+ client_secret=None,
+ credential_source=self.CREDENTIAL_SOURCE,
+ quota_project_id=None,
+ workforce_pool_user_project=None,
+ universe_domain=DEFAULT_UNIVERSE_DOMAIN,
+ )
+
+ def test_constructor_invalid_options(self):
+ credential_source = {"unsupported": "value"}
+
+ with pytest.raises(ValueError) as excinfo:
+ self.make_pluggable(credential_source=credential_source)
+
+ assert excinfo.match(r"Missing credential_source")
+
+ def test_constructor_invalid_credential_source(self):
+ with pytest.raises(ValueError) as excinfo:
+ self.make_pluggable(credential_source="non-dict")
+
+ assert excinfo.match(r"Missing credential_source")
+
+ def test_info_with_credential_source(self):
+ credentials = self.make_pluggable(
+ credential_source=self.CREDENTIAL_SOURCE.copy()
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "token_info_url": TOKEN_INFO_URL,
+ "credential_source": self.CREDENTIAL_SOURCE,
+ "universe_domain": DEFAULT_UNIVERSE_DOMAIN,
+ }
+
+ def test_token_info_url(self):
+ credentials = self.make_pluggable(
+ credential_source=self.CREDENTIAL_SOURCE.copy()
+ )
+
+ assert credentials.token_info_url == TOKEN_INFO_URL
+
+ def test_token_info_url_custom(self):
+ for url in VALID_TOKEN_URLS:
+ credentials = self.make_pluggable(
+ credential_source=self.CREDENTIAL_SOURCE.copy(),
+ token_info_url=(url + "/introspect"),
+ )
+
+ assert credentials.token_info_url == url + "/introspect"
+
+ def test_token_info_url_negative(self):
+ credentials = self.make_pluggable(
+ credential_source=self.CREDENTIAL_SOURCE.copy(), token_info_url=None
+ )
+
+ assert not credentials.token_info_url
+
+ def test_token_url_custom(self):
+ for url in VALID_TOKEN_URLS:
+ credentials = self.make_pluggable(
+ credential_source=self.CREDENTIAL_SOURCE.copy(),
+ token_url=(url + "/token"),
+ )
+
+ assert credentials._token_url == (url + "/token")
+
+ def test_service_account_impersonation_url_custom(self):
+ for url in VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS:
+ credentials = self.make_pluggable(
+ credential_source=self.CREDENTIAL_SOURCE.copy(),
+ service_account_impersonation_url=(
+ url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
+ ),
+ )
+
+ assert credentials._service_account_impersonation_url == (
+ url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_successfully(self, tmpdir):
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join(
+ "actual_output_file"
+ )
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = {
+ "command": "command",
+ "interactive_timeout_millis": 300000,
+ "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE}
+
+ testData = {
+ "subject_token_oidc_id_token": {
+ "stdout": json.dumps(
+ self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN
+ ).encode("UTF-8"),
+ "impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "file_content": self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_ID_TOKEN,
+ "expect_token": self.EXECUTABLE_OIDC_TOKEN,
+ },
+ "subject_token_oidc_id_token_interacitve_mode": {
+ "audience": WORKFORCE_AUDIENCE,
+ "file_content": self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_ID_TOKEN,
+ "interactive": True,
+ "expect_token": self.EXECUTABLE_OIDC_TOKEN,
+ },
+ "subject_token_oidc_jwt": {
+ "stdout": json.dumps(
+ self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_JWT
+ ).encode("UTF-8"),
+ "impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "file_content": self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_JWT,
+ "expect_token": self.EXECUTABLE_OIDC_TOKEN,
+ },
+ "subject_token_oidc_jwt_interactive_mode": {
+ "audience": WORKFORCE_AUDIENCE,
+ "file_content": self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_JWT,
+ "interactive": True,
+ "expect_token": self.EXECUTABLE_OIDC_TOKEN,
+ },
+ "subject_token_saml": {
+ "stdout": json.dumps(self.EXECUTABLE_SUCCESSFUL_SAML_RESPONSE).encode(
+ "UTF-8"
+ ),
+ "impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+ "file_content": self.EXECUTABLE_SUCCESSFUL_SAML_NO_EXPIRATION_TIME_RESPONSE,
+ "expect_token": self.EXECUTABLE_SAML_TOKEN,
+ },
+ "subject_token_saml_interactive_mode": {
+ "audience": WORKFORCE_AUDIENCE,
+ "file_content": self.EXECUTABLE_SUCCESSFUL_SAML_NO_EXPIRATION_TIME_RESPONSE,
+ "interactive": True,
+ "expect_token": self.EXECUTABLE_SAML_TOKEN,
+ },
+ }
+
+ for data in testData.values():
+ with open(
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w"
+ ) as output_file:
+ json.dump(data.get("file_content"), output_file)
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[], stdout=data.get("stdout"), returncode=0
+ ),
+ ):
+ credentials = self.make_pluggable(
+ audience=data.get("audience", AUDIENCE),
+ service_account_impersonation_url=data.get("impersonation_url"),
+ credential_source=ACTUAL_CREDENTIAL_SOURCE,
+ interactive=data.get("interactive", False),
+ )
+ subject_token = credentials.retrieve_subject_token(None)
+ assert subject_token == data.get("expect_token")
+ os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE)
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_saml(self):
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(self.EXECUTABLE_SUCCESSFUL_SAML_RESPONSE).encode(
+ "UTF-8"
+ ),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == self.EXECUTABLE_SAML_TOKEN
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_saml_interactive_mode(self, tmpdir):
+
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join(
+ "actual_output_file"
+ )
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = {
+ "command": "command",
+ "interactive_timeout_millis": 300000,
+ "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE}
+ with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file:
+ json.dump(
+ self.EXECUTABLE_SUCCESSFUL_SAML_NO_EXPIRATION_TIME_RESPONSE, output_file
+ )
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(args=[], returncode=0),
+ ):
+ credentials = self.make_pluggable(
+ audience=WORKFORCE_AUDIENCE,
+ credential_source=ACTUAL_CREDENTIAL_SOURCE,
+ interactive=True,
+ )
+
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == self.EXECUTABLE_SAML_TOKEN
+ os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE)
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_failed(self):
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(self.EXECUTABLE_FAILED_RESPONSE).encode("UTF-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ r"Executable returned unsuccessful response: code: 401, message: Permission denied. Caller not authorized."
+ )
+
+ @mock.patch.dict(
+ os.environ,
+ {
+ "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1",
+ "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1",
+ },
+ )
+ def test_retrieve_subject_token_failed_interactive_mode(self, tmpdir):
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join(
+ "actual_output_file"
+ )
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = {
+ "command": "command",
+ "interactive_timeout_millis": 300000,
+ "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE}
+ with open(
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w", encoding="utf-8"
+ ) as output_file:
+ json.dump(self.EXECUTABLE_FAILED_RESPONSE, output_file)
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(args=[], returncode=0),
+ ):
+ credentials = self.make_pluggable(
+ audience=WORKFORCE_AUDIENCE,
+ credential_source=ACTUAL_CREDENTIAL_SOURCE,
+ interactive=True,
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ r"Executable returned unsuccessful response: code: 401, message: Permission denied. Caller not authorized."
+ )
+ os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE)
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "0"})
+ def test_retrieve_subject_token_not_allowd(self):
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(
+ self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN
+ ).encode("UTF-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(ValueError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(r"Executables need to be explicitly allowed")
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_invalid_version(self):
+ EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_VERSION_2 = {
+ "version": 2,
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "id_token": self.EXECUTABLE_OIDC_TOKEN,
+ "expiration_time": 9999999999,
+ }
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_VERSION_2).encode(
+ "UTF-8"
+ ),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(r"Executable returned unsupported version.")
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_expired_token(self):
+ EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_EXPIRED = {
+ "version": 1,
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "id_token": self.EXECUTABLE_OIDC_TOKEN,
+ "expiration_time": 0,
+ }
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_EXPIRED).encode(
+ "UTF-8"
+ ),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(r"The token returned by the executable is expired.")
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_file_cache(self, tmpdir):
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join(
+ "actual_output_file"
+ )
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = {
+ "command": "command",
+ "timeout_millis": 30000,
+ "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE}
+ with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file:
+ json.dump(self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN, output_file)
+
+ credentials = self.make_pluggable(credential_source=ACTUAL_CREDENTIAL_SOURCE)
+
+ subject_token = credentials.retrieve_subject_token(None)
+ assert subject_token == self.EXECUTABLE_OIDC_TOKEN
+
+ os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE)
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_no_file_cache(self):
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = {
+ "command": "command",
+ "timeout_millis": 30000,
+ }
+ ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE}
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(
+ self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN
+ ).encode("UTF-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(
+ credential_source=ACTUAL_CREDENTIAL_SOURCE
+ )
+
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == self.EXECUTABLE_OIDC_TOKEN
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_file_cache_value_error_report(self, tmpdir):
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join(
+ "actual_output_file"
+ )
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = {
+ "command": "command",
+ "timeout_millis": 30000,
+ "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE}
+ ACTUAL_EXECUTABLE_RESPONSE = {
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "id_token": self.EXECUTABLE_OIDC_TOKEN,
+ "expiration_time": 9999999999,
+ }
+ with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file:
+ json.dump(ACTUAL_EXECUTABLE_RESPONSE, output_file)
+
+ credentials = self.make_pluggable(credential_source=ACTUAL_CREDENTIAL_SOURCE)
+
+ with pytest.raises(ValueError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(r"The executable response is missing the version field.")
+
+ os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE)
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_file_cache_refresh_error_retry(self, tmpdir):
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join(
+ "actual_output_file"
+ )
+ ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = {
+ "command": "command",
+ "timeout_millis": 30000,
+ "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE}
+ ACTUAL_EXECUTABLE_RESPONSE = {
+ "version": 2,
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "id_token": self.EXECUTABLE_OIDC_TOKEN,
+ "expiration_time": 9999999999,
+ }
+ with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file:
+ json.dump(ACTUAL_EXECUTABLE_RESPONSE, output_file)
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(
+ self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN
+ ).encode("UTF-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(
+ credential_source=ACTUAL_CREDENTIAL_SOURCE
+ )
+
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == self.EXECUTABLE_OIDC_TOKEN
+
+ os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE)
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_unsupported_token_type(self):
+ EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = {
+ "version": 1,
+ "success": True,
+ "token_type": "unsupported_token_type",
+ "id_token": self.EXECUTABLE_OIDC_TOKEN,
+ "expiration_time": 9999999999,
+ }
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE).encode("UTF-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(r"Executable returned unsupported token type.")
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_missing_version(self):
+ EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = {
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "id_token": self.EXECUTABLE_OIDC_TOKEN,
+ "expiration_time": 9999999999,
+ }
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE).encode("UTF-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(ValueError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ r"The executable response is missing the version field."
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_missing_success(self):
+ EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = {
+ "version": 1,
+ "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "id_token": self.EXECUTABLE_OIDC_TOKEN,
+ "expiration_time": 9999999999,
+ }
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE).encode("UTF-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(ValueError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ r"The executable response is missing the success field."
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_missing_error_code_message(self):
+ EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = {"version": 1, "success": False}
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE).encode("UTF-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(ValueError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ r"Error code and message fields are required in the response."
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_without_expiration_time_should_pass_when_output_file_not_specified(
+ self,
+ ):
+ EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = {
+ "version": 1,
+ "success": True,
+ "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ "id_token": self.EXECUTABLE_OIDC_TOKEN,
+ }
+
+ CREDENTIAL_SOURCE = {
+ "executable": {"command": "command", "timeout_millis": 30000}
+ }
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE).encode("UTF-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=CREDENTIAL_SOURCE)
+ subject_token = credentials.retrieve_subject_token(None)
+
+ assert subject_token == self.EXECUTABLE_OIDC_TOKEN
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_missing_token_type(self):
+ EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = {
+ "version": 1,
+ "success": True,
+ "id_token": self.EXECUTABLE_OIDC_TOKEN,
+ "expiration_time": 9999999999,
+ }
+
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE).encode("UTF-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(ValueError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ r"The executable response is missing the token_type field."
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_credential_source_missing_command(self):
+ with pytest.raises(ValueError) as excinfo:
+ CREDENTIAL_SOURCE = {
+ "executable": {
+ "timeout_millis": 30000,
+ "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ }
+ _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE)
+
+ assert excinfo.match(
+ r"Missing command field. Executable command must be provided."
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_credential_source_missing_output_interactive_mode(self):
+ CREDENTIAL_SOURCE = {
+ "executable": {"command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND}
+ }
+ credentials = self.make_pluggable(
+ credential_source=CREDENTIAL_SOURCE, interactive=True
+ )
+ with pytest.raises(ValueError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ r"An output_file must be specified in the credential configuration for interactive mode."
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_credential_source_timeout_missing_will_use_default_timeout_value(self):
+ CREDENTIAL_SOURCE = {
+ "executable": {
+ "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND,
+ "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ }
+ credentials = self.make_pluggable(credential_source=CREDENTIAL_SOURCE)
+
+ assert (
+ credentials._credential_source_executable_timeout_millis
+ == pluggable.EXECUTABLE_TIMEOUT_MILLIS_DEFAULT
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_credential_source_timeout_small(self):
+ with pytest.raises(ValueError) as excinfo:
+ CREDENTIAL_SOURCE = {
+ "executable": {
+ "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND,
+ "timeout_millis": 5000 - 1,
+ "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ }
+ _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE)
+
+ assert excinfo.match(r"Timeout must be between 5 and 120 seconds.")
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_credential_source_timeout_large(self):
+ with pytest.raises(ValueError) as excinfo:
+ CREDENTIAL_SOURCE = {
+ "executable": {
+ "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND,
+ "timeout_millis": 120000 + 1,
+ "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ }
+ _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE)
+
+ assert excinfo.match(r"Timeout must be between 5 and 120 seconds.")
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_credential_source_interactive_timeout_small(self):
+ with pytest.raises(ValueError) as excinfo:
+ CREDENTIAL_SOURCE = {
+ "executable": {
+ "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND,
+ "interactive_timeout_millis": 30000 - 1,
+ "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ }
+ _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE)
+
+ assert excinfo.match(
+ r"Interactive timeout must be between 30 seconds and 30 minutes."
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_credential_source_interactive_timeout_large(self):
+ with pytest.raises(ValueError) as excinfo:
+ CREDENTIAL_SOURCE = {
+ "executable": {
+ "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND,
+ "interactive_timeout_millis": 1800000 + 1,
+ "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ }
+ _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE)
+
+ assert excinfo.match(
+ r"Interactive timeout must be between 30 seconds and 30 minutes."
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_executable_fail(self):
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[], stdout=None, returncode=1
+ ),
+ ):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ r"Executable exited with non-zero return code 1. Error: None"
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_non_workforce_fail_interactive_mode(self):
+ credentials = self.make_pluggable(
+ credential_source=self.CREDENTIAL_SOURCE, interactive=True
+ )
+ with pytest.raises(ValueError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(r"Interactive mode is only enabled for workforce pool.")
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_fail_on_validation_missing_interactive_timeout(
+ self
+ ):
+ CREDENTIAL_SOURCE_EXECUTABLE = {
+ "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND,
+ "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+ }
+ CREDENTIAL_SOURCE = {"executable": CREDENTIAL_SOURCE_EXECUTABLE}
+ credentials = self.make_pluggable(
+ credential_source=CREDENTIAL_SOURCE, interactive=True
+ )
+ with pytest.raises(ValueError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ r"Interactive mode cannot run without an interactive timeout."
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_executable_fail_interactive_mode(self):
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[], stdout=None, returncode=1
+ ),
+ ):
+ credentials = self.make_pluggable(
+ audience=WORKFORCE_AUDIENCE,
+ credential_source=self.CREDENTIAL_SOURCE,
+ interactive=True,
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(
+ r"Executable exited with non-zero return code 1. Error: None"
+ )
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "0"})
+ def test_revoke_failed_executable_not_allowed(self):
+ credentials = self.make_pluggable(
+ credential_source=self.CREDENTIAL_SOURCE, interactive=True
+ )
+ with pytest.raises(ValueError) as excinfo:
+ _ = credentials.revoke(None)
+
+ assert excinfo.match(r"Executables need to be explicitly allowed")
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_revoke_failed(self):
+ testData = {
+ "non_interactive_mode": {
+ "interactive": False,
+ "expectErrType": ValueError,
+ "expectErrPattern": r"Revoke is only enabled under interactive mode.",
+ },
+ "executable_failed": {
+ "returncode": 1,
+ "expectErrType": exceptions.RefreshError,
+ "expectErrPattern": r"Auth revoke failed on executable.",
+ },
+ "response_validation_missing_version": {
+ "response": {},
+ "expectErrType": ValueError,
+ "expectErrPattern": r"The executable response is missing the version field.",
+ },
+ "response_validation_invalid_version": {
+ "response": {"version": 2},
+ "expectErrType": exceptions.RefreshError,
+ "expectErrPattern": r"Executable returned unsupported version.",
+ },
+ "response_validation_missing_success": {
+ "response": {"version": 1},
+ "expectErrType": ValueError,
+ "expectErrPattern": r"The executable response is missing the success field.",
+ },
+ "response_validation_failed_with_success_field_is_false": {
+ "response": {"version": 1, "success": False},
+ "expectErrType": exceptions.RefreshError,
+ "expectErrPattern": r"Revoke failed with unsuccessful response.",
+ },
+ }
+ for data in testData.values():
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(data.get("response")).encode("UTF-8"),
+ returncode=data.get("returncode", 0),
+ ),
+ ):
+ credentials = self.make_pluggable(
+ audience=WORKFORCE_AUDIENCE,
+ service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+ credential_source=self.CREDENTIAL_SOURCE,
+ interactive=data.get("interactive", True),
+ )
+
+ with pytest.raises(data.get("expectErrType")) as excinfo:
+ _ = credentials.revoke(None)
+
+ assert excinfo.match(data.get("expectErrPattern"))
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_revoke_successfully(self):
+ ACTUAL_RESPONSE = {"version": 1, "success": True}
+ with mock.patch(
+ "subprocess.run",
+ return_value=subprocess.CompletedProcess(
+ args=[],
+ stdout=json.dumps(ACTUAL_RESPONSE).encode("utf-8"),
+ returncode=0,
+ ),
+ ):
+ credentials = self.make_pluggable(
+ audience=WORKFORCE_AUDIENCE,
+ credential_source=self.CREDENTIAL_SOURCE,
+ interactive=True,
+ )
+ _ = credentials.revoke(None)
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_retrieve_subject_token_python_2(self):
+ with mock.patch("sys.version_info", (2, 7)):
+ credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE)
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _ = credentials.retrieve_subject_token(None)
+
+ assert excinfo.match(r"Pluggable auth is only supported for python 3.7+")
+
+ @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"})
+ def test_revoke_subject_token_python_2(self):
+ with mock.patch("sys.version_info", (2, 7)):
+ credentials = self.make_pluggable(
+ audience=WORKFORCE_AUDIENCE,
+ credential_source=self.CREDENTIAL_SOURCE,
+ interactive=True,
+ )
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ _ = credentials.revoke(None)
+
+ assert excinfo.match(r"Pluggable auth is only supported for python 3.7+")
diff --git a/contrib/python/google-auth/py3/tests/transport/__init__.py b/contrib/python/google-auth/py3/tests/transport/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/transport/__init__.py
diff --git a/contrib/python/google-auth/py3/tests/transport/compliance.py b/contrib/python/google-auth/py3/tests/transport/compliance.py
new file mode 100644
index 0000000000..b3cd7e8234
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/transport/compliance.py
@@ -0,0 +1,108 @@
+# Copyright 2016 Google LLC
+#
+# 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 http.client as http_client
+import time
+
+import flask # type: ignore
+import pytest # type: ignore
+from pytest_localserver.http import WSGIServer # type: ignore
+
+from google.auth import exceptions
+
+# .invalid will never resolve, see https://tools.ietf.org/html/rfc2606
+NXDOMAIN = "test.invalid"
+
+
+class RequestResponseTests(object):
+ @pytest.fixture(scope="module")
+ def server(self):
+ """Provides a test HTTP server.
+
+ The test server is automatically created before
+ a test and destroyed at the end. The server is serving a test
+ application that can be used to verify requests.
+ """
+ app = flask.Flask(__name__)
+ app.debug = True
+
+ # pylint: disable=unused-variable
+ # (pylint thinks the flask routes are unusued.)
+ @app.route("/basic")
+ def index():
+ header_value = flask.request.headers.get("x-test-header", "value")
+ headers = {"X-Test-Header": header_value}
+ return "Basic Content", http_client.OK, headers
+
+ @app.route("/server_error")
+ def server_error():
+ return "Error", http_client.INTERNAL_SERVER_ERROR
+
+ @app.route("/wait")
+ def wait():
+ time.sleep(3)
+ return "Waited"
+
+ # pylint: enable=unused-variable
+
+ server = WSGIServer(application=app.wsgi_app)
+ server.start()
+ yield server
+ server.stop()
+
+ def test_request_basic(self, server):
+ request = self.make_request()
+ response = request(url=server.url + "/basic", method="GET")
+
+ assert response.status == http_client.OK
+ assert response.headers["x-test-header"] == "value"
+ assert response.data == b"Basic Content"
+
+ def test_request_with_timeout_success(self, server):
+ request = self.make_request()
+ response = request(url=server.url + "/basic", method="GET", timeout=2)
+
+ assert response.status == http_client.OK
+ assert response.headers["x-test-header"] == "value"
+ assert response.data == b"Basic Content"
+
+ def test_request_with_timeout_failure(self, server):
+ request = self.make_request()
+
+ with pytest.raises(exceptions.TransportError):
+ request(url=server.url + "/wait", method="GET", timeout=1)
+
+ def test_request_headers(self, server):
+ request = self.make_request()
+ response = request(
+ url=server.url + "/basic",
+ method="GET",
+ headers={"x-test-header": "hello world"},
+ )
+
+ assert response.status == http_client.OK
+ assert response.headers["x-test-header"] == "hello world"
+ assert response.data == b"Basic Content"
+
+ def test_request_error(self, server):
+ request = self.make_request()
+ response = request(url=server.url + "/server_error", method="GET")
+
+ assert response.status == http_client.INTERNAL_SERVER_ERROR
+ assert response.data == b"Error"
+
+ def test_connection_error(self):
+ request = self.make_request()
+ with pytest.raises(exceptions.TransportError):
+ request(url="http://{}".format(NXDOMAIN), method="GET")
diff --git a/contrib/python/google-auth/py3/tests/transport/test__custom_tls_signer.py b/contrib/python/google-auth/py3/tests/transport/test__custom_tls_signer.py
new file mode 100644
index 0000000000..5836b325ad
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/transport/test__custom_tls_signer.py
@@ -0,0 +1,234 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import ctypes
+import os
+
+import mock
+import pytest # type: ignore
+from requests.packages.urllib3.util.ssl_ import create_urllib3_context # type: ignore
+import urllib3.contrib.pyopenssl # type: ignore
+
+from google.auth import exceptions
+from google.auth.transport import _custom_tls_signer
+
+urllib3.contrib.pyopenssl.inject_into_urllib3()
+
+FAKE_ENTERPRISE_CERT_FILE_PATH = "/path/to/enterprise/cert/file"
+ENTERPRISE_CERT_FILE = os.path.join(
+ os.path.dirname(__file__), "../data/enterprise_cert_valid.json"
+)
+INVALID_ENTERPRISE_CERT_FILE = os.path.join(
+ os.path.dirname(__file__), "../data/enterprise_cert_invalid.json"
+)
+
+
+def test_load_offload_lib():
+ with mock.patch("ctypes.CDLL", return_value=mock.MagicMock()):
+ lib = _custom_tls_signer.load_offload_lib("/path/to/offload/lib")
+
+ assert lib.ConfigureSslContext.argtypes == [
+ _custom_tls_signer.SIGN_CALLBACK_CTYPE,
+ ctypes.c_char_p,
+ ctypes.c_void_p,
+ ]
+ assert lib.ConfigureSslContext.restype == ctypes.c_int
+
+
+def test_load_signer_lib():
+ with mock.patch("ctypes.CDLL", return_value=mock.MagicMock()):
+ lib = _custom_tls_signer.load_signer_lib("/path/to/signer/lib")
+
+ assert lib.SignForPython.restype == ctypes.c_int
+ assert lib.SignForPython.argtypes == [
+ ctypes.c_char_p,
+ ctypes.c_char_p,
+ ctypes.c_int,
+ ctypes.c_char_p,
+ ctypes.c_int,
+ ]
+
+ assert lib.GetCertPemForPython.restype == ctypes.c_int
+ assert lib.GetCertPemForPython.argtypes == [
+ ctypes.c_char_p,
+ ctypes.c_char_p,
+ ctypes.c_int,
+ ]
+
+
+def test__compute_sha256_digest():
+ to_be_signed = ctypes.create_string_buffer(b"foo")
+ sig = _custom_tls_signer._compute_sha256_digest(to_be_signed, 4)
+
+ assert (
+ base64.b64encode(sig).decode() == "RG5gyEH8CAAh3lxgbt2PLPAHPO8p6i9+cn5dqHfUUYM="
+ )
+
+
+def test_get_sign_callback():
+ # mock signer lib's SignForPython function
+ mock_sig_len = 10
+ mock_signer_lib = mock.MagicMock()
+ mock_signer_lib.SignForPython.return_value = mock_sig_len
+
+ # create a sign callback. The callback calls signer lib's SignForPython method
+ sign_callback = _custom_tls_signer.get_sign_callback(
+ mock_signer_lib, FAKE_ENTERPRISE_CERT_FILE_PATH
+ )
+
+ # mock the parameters used to call the sign callback
+ to_be_signed = ctypes.POINTER(ctypes.c_ubyte)()
+ to_be_signed_len = 4
+ returned_sig_array = ctypes.c_ubyte()
+ mock_sig_array = ctypes.byref(returned_sig_array)
+ returned_sign_len = ctypes.c_ulong()
+ mock_sig_len_array = ctypes.byref(returned_sign_len)
+
+ # call the callback, make sure the signature len is returned via mock_sig_len_array[0]
+ assert sign_callback(
+ mock_sig_array, mock_sig_len_array, to_be_signed, to_be_signed_len
+ )
+ assert returned_sign_len.value == mock_sig_len
+
+
+def test_get_sign_callback_failed_to_sign():
+ # mock signer lib's SignForPython function. Set the sig len to be 0 to
+ # indicate the signing failed.
+ mock_sig_len = 0
+ mock_signer_lib = mock.MagicMock()
+ mock_signer_lib.SignForPython.return_value = mock_sig_len
+
+ # create a sign callback. The callback calls signer lib's SignForPython method
+ sign_callback = _custom_tls_signer.get_sign_callback(
+ mock_signer_lib, FAKE_ENTERPRISE_CERT_FILE_PATH
+ )
+
+ # mock the parameters used to call the sign callback
+ to_be_signed = ctypes.POINTER(ctypes.c_ubyte)()
+ to_be_signed_len = 4
+ returned_sig_array = ctypes.c_ubyte()
+ mock_sig_array = ctypes.byref(returned_sig_array)
+ returned_sign_len = ctypes.c_ulong()
+ mock_sig_len_array = ctypes.byref(returned_sign_len)
+ sign_callback(mock_sig_array, mock_sig_len_array, to_be_signed, to_be_signed_len)
+
+ # sign callback should return 0
+ assert not sign_callback(
+ mock_sig_array, mock_sig_len_array, to_be_signed, to_be_signed_len
+ )
+
+
+def test_get_cert_no_cert():
+ # mock signer lib's GetCertPemForPython function to return 0 to indicts
+ # the cert doesn't exit (cert len = 0)
+ mock_signer_lib = mock.MagicMock()
+ mock_signer_lib.GetCertPemForPython.return_value = 0
+
+ # call the get cert method
+ with pytest.raises(exceptions.MutualTLSChannelError) as excinfo:
+ _custom_tls_signer.get_cert(mock_signer_lib, FAKE_ENTERPRISE_CERT_FILE_PATH)
+
+ assert excinfo.match("failed to get certificate")
+
+
+def test_get_cert():
+ # mock signer lib's GetCertPemForPython function
+ mock_cert_len = 10
+ mock_signer_lib = mock.MagicMock()
+ mock_signer_lib.GetCertPemForPython.return_value = mock_cert_len
+
+ # call the get cert method
+ mock_cert = _custom_tls_signer.get_cert(
+ mock_signer_lib, FAKE_ENTERPRISE_CERT_FILE_PATH
+ )
+
+ # make sure the signer lib's GetCertPemForPython is called twice, and the
+ # mock_cert has length mock_cert_len
+ assert mock_signer_lib.GetCertPemForPython.call_count == 2
+ assert len(mock_cert) == mock_cert_len
+
+
+def test_custom_tls_signer():
+ offload_lib = mock.MagicMock()
+ signer_lib = mock.MagicMock()
+
+ # Test load_libraries method
+ with mock.patch(
+ "google.auth.transport._custom_tls_signer.load_signer_lib"
+ ) as load_signer_lib:
+ with mock.patch(
+ "google.auth.transport._custom_tls_signer.load_offload_lib"
+ ) as load_offload_lib:
+ load_offload_lib.return_value = offload_lib
+ load_signer_lib.return_value = signer_lib
+ signer_object = _custom_tls_signer.CustomTlsSigner(ENTERPRISE_CERT_FILE)
+ signer_object.load_libraries()
+ assert signer_object._cert is None
+ assert signer_object._enterprise_cert_file_path == ENTERPRISE_CERT_FILE
+ assert signer_object._offload_lib == offload_lib
+ assert signer_object._signer_lib == signer_lib
+ load_signer_lib.assert_called_with("/path/to/signer/lib")
+ load_offload_lib.assert_called_with("/path/to/offload/lib")
+
+ # Test set_up_custom_key and set_up_ssl_context methods
+ with mock.patch("google.auth.transport._custom_tls_signer.get_cert") as get_cert:
+ with mock.patch(
+ "google.auth.transport._custom_tls_signer.get_sign_callback"
+ ) as get_sign_callback:
+ get_cert.return_value = b"mock_cert"
+ signer_object.set_up_custom_key()
+ signer_object.attach_to_ssl_context(create_urllib3_context())
+ get_cert.assert_called_once()
+ get_sign_callback.assert_called_once()
+ offload_lib.ConfigureSslContext.assert_called_once()
+
+
+def test_custom_tls_signer_failed_to_load_libraries():
+ # Test load_libraries method
+ with pytest.raises(exceptions.MutualTLSChannelError) as excinfo:
+ signer_object = _custom_tls_signer.CustomTlsSigner(INVALID_ENTERPRISE_CERT_FILE)
+ signer_object.load_libraries()
+ assert excinfo.match("enterprise cert file is invalid")
+
+
+def test_custom_tls_signer_fail_to_offload():
+ offload_lib = mock.MagicMock()
+ signer_lib = mock.MagicMock()
+
+ with mock.patch(
+ "google.auth.transport._custom_tls_signer.load_signer_lib"
+ ) as load_signer_lib:
+ with mock.patch(
+ "google.auth.transport._custom_tls_signer.load_offload_lib"
+ ) as load_offload_lib:
+ load_offload_lib.return_value = offload_lib
+ load_signer_lib.return_value = signer_lib
+ signer_object = _custom_tls_signer.CustomTlsSigner(ENTERPRISE_CERT_FILE)
+ signer_object.load_libraries()
+
+ # set the return value to be 0 which indicts offload fails
+ offload_lib.ConfigureSslContext.return_value = 0
+
+ with pytest.raises(exceptions.MutualTLSChannelError) as excinfo:
+ with mock.patch(
+ "google.auth.transport._custom_tls_signer.get_cert"
+ ) as get_cert:
+ with mock.patch(
+ "google.auth.transport._custom_tls_signer.get_sign_callback"
+ ):
+ get_cert.return_value = b"mock_cert"
+ signer_object.set_up_custom_key()
+ signer_object.attach_to_ssl_context(create_urllib3_context())
+ assert excinfo.match("failed to configure SSL context")
diff --git a/contrib/python/google-auth/py3/tests/transport/test__http_client.py b/contrib/python/google-auth/py3/tests/transport/test__http_client.py
new file mode 100644
index 0000000000..202276323c
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/transport/test__http_client.py
@@ -0,0 +1,31 @@
+# Copyright 2016 Google LLC
+#
+# 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 pytest # type: ignore
+
+from google.auth import exceptions
+import google.auth.transport._http_client
+from tests.transport import compliance
+
+
+class TestRequestResponse(compliance.RequestResponseTests):
+ def make_request(self):
+ return google.auth.transport._http_client.Request()
+
+ def test_non_http(self):
+ request = self.make_request()
+ with pytest.raises(exceptions.TransportError) as excinfo:
+ request(url="https://{}".format(compliance.NXDOMAIN), method="GET")
+
+ assert excinfo.match("https")
diff --git a/contrib/python/google-auth/py3/tests/transport/test__mtls_helper.py b/contrib/python/google-auth/py3/tests/transport/test__mtls_helper.py
new file mode 100644
index 0000000000..642283a5c5
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/transport/test__mtls_helper.py
@@ -0,0 +1,441 @@
+# Copyright 2020 Google LLC
+#
+# 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 os
+import re
+
+import mock
+from OpenSSL import crypto
+import pytest # type: ignore
+
+from google.auth import exceptions
+from google.auth.transport import _mtls_helper
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+
+CONTEXT_AWARE_METADATA = {"cert_provider_command": ["some command"]}
+
+ENCRYPTED_EC_PRIVATE_KEY = b"""-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHkME8GCSqGSIb3DQEFDTBCMCkGCSqGSIb3DQEFDDAcBAgl2/yVgs1h3QICCAAw
+DAYIKoZIhvcNAgkFADAVBgkrBgEEAZdVAQIECJk2GRrvxOaJBIGQXIBnMU4wmciT
+uA6yD8q0FxuIzjG7E2S6tc5VRgSbhRB00eBO3jWmO2pBybeQW+zVioDcn50zp2ts
+wYErWC+LCm1Zg3r+EGnT1E1GgNoODbVQ3AEHlKh1CGCYhEovxtn3G+Fjh7xOBrNB
+saVVeDb4tHD4tMkiVVUBrUcTZPndP73CtgyGHYEphasYPzEz3+AU
+-----END ENCRYPTED PRIVATE KEY-----"""
+
+EC_PUBLIC_KEY = b"""-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvCNi1NoDY1oMqPHIgXI8RBbTYGi/
+brEjbre1nSiQW11xRTJbVeETdsuP0EAu2tG3PcRhhwDfeJ8zXREgTBurNw==
+-----END PUBLIC KEY-----"""
+
+PASSPHRASE = b"""-----BEGIN PASSPHRASE-----
+password
+-----END PASSPHRASE-----"""
+PASSPHRASE_VALUE = b"password"
+
+
+def check_cert_and_key(content, expected_cert, expected_key):
+ success = True
+
+ cert_match = re.findall(_mtls_helper._CERT_REGEX, content)
+ success = success and len(cert_match) == 1 and cert_match[0] == expected_cert
+
+ key_match = re.findall(_mtls_helper._KEY_REGEX, content)
+ success = success and len(key_match) == 1 and key_match[0] == expected_key
+
+ return success
+
+
+class TestCertAndKeyRegex(object):
+ def test_cert_and_key(self):
+ # Test single cert and single key
+ check_cert_and_key(
+ pytest.public_cert_bytes + pytest.private_key_bytes,
+ pytest.public_cert_bytes,
+ pytest.private_key_bytes,
+ )
+ check_cert_and_key(
+ pytest.private_key_bytes + pytest.public_cert_bytes,
+ pytest.public_cert_bytes,
+ pytest.private_key_bytes,
+ )
+
+ # Test cert chain and single key
+ check_cert_and_key(
+ pytest.public_cert_bytes
+ + pytest.public_cert_bytes
+ + pytest.private_key_bytes,
+ pytest.public_cert_bytes + pytest.public_cert_bytes,
+ pytest.private_key_bytes,
+ )
+ check_cert_and_key(
+ pytest.private_key_bytes
+ + pytest.public_cert_bytes
+ + pytest.public_cert_bytes,
+ pytest.public_cert_bytes + pytest.public_cert_bytes,
+ pytest.private_key_bytes,
+ )
+
+ def test_key(self):
+ # Create some fake keys for regex check.
+ KEY = b"""-----BEGIN PRIVATE KEY-----
+ MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg
+ /fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB
+ -----END PRIVATE KEY-----"""
+ RSA_KEY = b"""-----BEGIN RSA PRIVATE KEY-----
+ MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg
+ /fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB
+ -----END RSA PRIVATE KEY-----"""
+ EC_KEY = b"""-----BEGIN EC PRIVATE KEY-----
+ MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg
+ /fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB
+ -----END EC PRIVATE KEY-----"""
+
+ check_cert_and_key(
+ pytest.public_cert_bytes + KEY, pytest.public_cert_bytes, KEY
+ )
+ check_cert_and_key(
+ pytest.public_cert_bytes + RSA_KEY, pytest.public_cert_bytes, RSA_KEY
+ )
+ check_cert_and_key(
+ pytest.public_cert_bytes + EC_KEY, pytest.public_cert_bytes, EC_KEY
+ )
+
+
+class TestCheckaMetadataPath(object):
+ def test_success(self):
+ metadata_path = os.path.join(DATA_DIR, "context_aware_metadata.json")
+ returned_path = _mtls_helper._check_dca_metadata_path(metadata_path)
+ assert returned_path is not None
+
+ def test_failure(self):
+ metadata_path = os.path.join(DATA_DIR, "not_exists.json")
+ returned_path = _mtls_helper._check_dca_metadata_path(metadata_path)
+ assert returned_path is None
+
+
+class TestReadMetadataFile(object):
+ def test_success(self):
+ metadata_path = os.path.join(DATA_DIR, "context_aware_metadata.json")
+ metadata = _mtls_helper._read_dca_metadata_file(metadata_path)
+
+ assert "cert_provider_command" in metadata
+
+ def test_file_not_json(self):
+ # read a file which is not json format.
+ metadata_path = os.path.join(DATA_DIR, "privatekey.pem")
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._read_dca_metadata_file(metadata_path)
+
+
+class TestRunCertProviderCommand(object):
+ def create_mock_process(self, output, error):
+ # There are two steps to execute a script with subprocess.Popen.
+ # (1) process = subprocess.Popen([comannds])
+ # (2) stdout, stderr = process.communicate()
+ # This function creates a mock process which can be returned by a mock
+ # subprocess.Popen. The mock process returns the given output and error
+ # when mock_process.communicate() is called.
+ mock_process = mock.Mock()
+ attrs = {"communicate.return_value": (output, error), "returncode": 0}
+ mock_process.configure_mock(**attrs)
+ return mock_process
+
+ @mock.patch("subprocess.Popen", autospec=True)
+ def test_success(self, mock_popen):
+ mock_popen.return_value = self.create_mock_process(
+ pytest.public_cert_bytes + pytest.private_key_bytes, b""
+ )
+ cert, key, passphrase = _mtls_helper._run_cert_provider_command(["command"])
+ assert cert == pytest.public_cert_bytes
+ assert key == pytest.private_key_bytes
+ assert passphrase is None
+
+ mock_popen.return_value = self.create_mock_process(
+ pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, b""
+ )
+ cert, key, passphrase = _mtls_helper._run_cert_provider_command(
+ ["command"], expect_encrypted_key=True
+ )
+ assert cert == pytest.public_cert_bytes
+ assert key == ENCRYPTED_EC_PRIVATE_KEY
+ assert passphrase == PASSPHRASE_VALUE
+
+ @mock.patch("subprocess.Popen", autospec=True)
+ def test_success_with_cert_chain(self, mock_popen):
+ PUBLIC_CERT_CHAIN_BYTES = pytest.public_cert_bytes + pytest.public_cert_bytes
+ mock_popen.return_value = self.create_mock_process(
+ PUBLIC_CERT_CHAIN_BYTES + pytest.private_key_bytes, b""
+ )
+ cert, key, passphrase = _mtls_helper._run_cert_provider_command(["command"])
+ assert cert == PUBLIC_CERT_CHAIN_BYTES
+ assert key == pytest.private_key_bytes
+ assert passphrase is None
+
+ mock_popen.return_value = self.create_mock_process(
+ PUBLIC_CERT_CHAIN_BYTES + ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, b""
+ )
+ cert, key, passphrase = _mtls_helper._run_cert_provider_command(
+ ["command"], expect_encrypted_key=True
+ )
+ assert cert == PUBLIC_CERT_CHAIN_BYTES
+ assert key == ENCRYPTED_EC_PRIVATE_KEY
+ assert passphrase == PASSPHRASE_VALUE
+
+ @mock.patch("subprocess.Popen", autospec=True)
+ def test_missing_cert(self, mock_popen):
+ mock_popen.return_value = self.create_mock_process(
+ pytest.private_key_bytes, b""
+ )
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._run_cert_provider_command(["command"])
+
+ mock_popen.return_value = self.create_mock_process(
+ ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, b""
+ )
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._run_cert_provider_command(
+ ["command"], expect_encrypted_key=True
+ )
+
+ @mock.patch("subprocess.Popen", autospec=True)
+ def test_missing_key(self, mock_popen):
+ mock_popen.return_value = self.create_mock_process(
+ pytest.public_cert_bytes, b""
+ )
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._run_cert_provider_command(["command"])
+
+ mock_popen.return_value = self.create_mock_process(
+ pytest.public_cert_bytes + PASSPHRASE, b""
+ )
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._run_cert_provider_command(
+ ["command"], expect_encrypted_key=True
+ )
+
+ @mock.patch("subprocess.Popen", autospec=True)
+ def test_missing_passphrase(self, mock_popen):
+ mock_popen.return_value = self.create_mock_process(
+ pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY, b""
+ )
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._run_cert_provider_command(
+ ["command"], expect_encrypted_key=True
+ )
+
+ @mock.patch("subprocess.Popen", autospec=True)
+ def test_passphrase_not_expected(self, mock_popen):
+ mock_popen.return_value = self.create_mock_process(
+ pytest.public_cert_bytes + pytest.private_key_bytes + PASSPHRASE, b""
+ )
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._run_cert_provider_command(["command"])
+
+ @mock.patch("subprocess.Popen", autospec=True)
+ def test_encrypted_key_expected(self, mock_popen):
+ mock_popen.return_value = self.create_mock_process(
+ pytest.public_cert_bytes + pytest.private_key_bytes + PASSPHRASE, b""
+ )
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._run_cert_provider_command(
+ ["command"], expect_encrypted_key=True
+ )
+
+ @mock.patch("subprocess.Popen", autospec=True)
+ def test_unencrypted_key_expected(self, mock_popen):
+ mock_popen.return_value = self.create_mock_process(
+ pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY, b""
+ )
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._run_cert_provider_command(["command"])
+
+ @mock.patch("subprocess.Popen", autospec=True)
+ def test_cert_provider_returns_error(self, mock_popen):
+ mock_popen.return_value = self.create_mock_process(b"", b"some error")
+ mock_popen.return_value.returncode = 1
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._run_cert_provider_command(["command"])
+
+ @mock.patch("subprocess.Popen", autospec=True)
+ def test_popen_raise_exception(self, mock_popen):
+ mock_popen.side_effect = OSError()
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper._run_cert_provider_command(["command"])
+
+
+class TestGetClientSslCredentials(object):
+ @mock.patch(
+ "google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
+ )
+ @mock.patch(
+ "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+ )
+ @mock.patch(
+ "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+ )
+ def test_success(
+ self,
+ mock_check_dca_metadata_path,
+ mock_read_dca_metadata_file,
+ mock_run_cert_provider_command,
+ ):
+ mock_check_dca_metadata_path.return_value = True
+ mock_read_dca_metadata_file.return_value = {
+ "cert_provider_command": ["command"]
+ }
+ mock_run_cert_provider_command.return_value = (b"cert", b"key", None)
+ has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials()
+ assert has_cert
+ assert cert == b"cert"
+ assert key == b"key"
+ assert passphrase is None
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+ )
+ def test_success_without_metadata(self, mock_check_dca_metadata_path):
+ mock_check_dca_metadata_path.return_value = False
+ has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials()
+ assert not has_cert
+ assert cert is None
+ assert key is None
+ assert passphrase is None
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
+ )
+ @mock.patch(
+ "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+ )
+ @mock.patch(
+ "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+ )
+ def test_success_with_encrypted_key(
+ self,
+ mock_check_dca_metadata_path,
+ mock_read_dca_metadata_file,
+ mock_run_cert_provider_command,
+ ):
+ mock_check_dca_metadata_path.return_value = True
+ mock_read_dca_metadata_file.return_value = {
+ "cert_provider_command": ["command"]
+ }
+ mock_run_cert_provider_command.return_value = (b"cert", b"key", b"passphrase")
+ has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials(
+ generate_encrypted_key=True
+ )
+ assert has_cert
+ assert cert == b"cert"
+ assert key == b"key"
+ assert passphrase == b"passphrase"
+ mock_run_cert_provider_command.assert_called_once_with(
+ ["command", "--with_passphrase"], expect_encrypted_key=True
+ )
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+ )
+ @mock.patch(
+ "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+ )
+ def test_missing_cert_command(
+ self, mock_check_dca_metadata_path, mock_read_dca_metadata_file
+ ):
+ mock_check_dca_metadata_path.return_value = True
+ mock_read_dca_metadata_file.return_value = {}
+ with pytest.raises(exceptions.ClientCertError):
+ _mtls_helper.get_client_ssl_credentials()
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
+ )
+ @mock.patch(
+ "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+ )
+ @mock.patch(
+ "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+ )
+ def test_customize_context_aware_metadata_path(
+ self,
+ mock_check_dca_metadata_path,
+ mock_read_dca_metadata_file,
+ mock_run_cert_provider_command,
+ ):
+ context_aware_metadata_path = "/path/to/metata/data"
+ mock_check_dca_metadata_path.return_value = context_aware_metadata_path
+ mock_read_dca_metadata_file.return_value = {
+ "cert_provider_command": ["command"]
+ }
+ mock_run_cert_provider_command.return_value = (b"cert", b"key", None)
+
+ has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials(
+ context_aware_metadata_path=context_aware_metadata_path
+ )
+
+ assert has_cert
+ assert cert == b"cert"
+ assert key == b"key"
+ assert passphrase is None
+ mock_check_dca_metadata_path.assert_called_with(context_aware_metadata_path)
+ mock_read_dca_metadata_file.assert_called_with(context_aware_metadata_path)
+
+
+class TestGetClientCertAndKey(object):
+ def test_callback_success(self):
+ callback = mock.Mock()
+ callback.return_value = (pytest.public_cert_bytes, pytest.private_key_bytes)
+
+ found_cert_key, cert, key = _mtls_helper.get_client_cert_and_key(callback)
+ assert found_cert_key
+ assert cert == pytest.public_cert_bytes
+ assert key == pytest.private_key_bytes
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True
+ )
+ def test_use_metadata(self, mock_get_client_ssl_credentials):
+ mock_get_client_ssl_credentials.return_value = (
+ True,
+ pytest.public_cert_bytes,
+ pytest.private_key_bytes,
+ None,
+ )
+
+ found_cert_key, cert, key = _mtls_helper.get_client_cert_and_key()
+ assert found_cert_key
+ assert cert == pytest.public_cert_bytes
+ assert key == pytest.private_key_bytes
+
+
+class TestDecryptPrivateKey(object):
+ def test_success(self):
+ decrypted_key = _mtls_helper.decrypt_private_key(
+ ENCRYPTED_EC_PRIVATE_KEY, PASSPHRASE_VALUE
+ )
+ private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, decrypted_key)
+ public_key = crypto.load_publickey(crypto.FILETYPE_PEM, EC_PUBLIC_KEY)
+ x509 = crypto.X509()
+ x509.set_pubkey(public_key)
+
+ # Test the decrypted key works by signing and verification.
+ signature = crypto.sign(private_key, b"data", "sha256")
+ crypto.verify(x509, signature, b"data", "sha256")
+
+ def test_crypto_error(self):
+ with pytest.raises(crypto.Error):
+ _mtls_helper.decrypt_private_key(
+ ENCRYPTED_EC_PRIVATE_KEY, b"wrong_password"
+ )
diff --git a/contrib/python/google-auth/py3/tests/transport/test_grpc.py b/contrib/python/google-auth/py3/tests/transport/test_grpc.py
new file mode 100644
index 0000000000..05dc5fad0e
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/transport/test_grpc.py
@@ -0,0 +1,503 @@
+# Copyright 2016 Google LLC
+#
+# 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 datetime
+import os
+import time
+
+import mock
+import pytest # type: ignore
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+from google.oauth2 import service_account
+
+try:
+ # pylint: disable=ungrouped-imports
+ import grpc # type: ignore
+ import google.auth.transport.grpc
+
+ HAS_GRPC = True
+except ImportError: # pragma: NO COVER
+ HAS_GRPC = False
+
+import yatest.common
+DATA_DIR = os.path.join(yatest.common.test_source_path(), "data")
+METADATA_PATH = os.path.join(DATA_DIR, "context_aware_metadata.json")
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+ PRIVATE_KEY_BYTES = fh.read()
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+ PUBLIC_CERT_BYTES = fh.read()
+
+pytestmark = pytest.mark.skipif(not HAS_GRPC, reason="gRPC is unavailable.")
+
+
+class CredentialsStub(credentials.Credentials):
+ def __init__(self, token="token"):
+ super(CredentialsStub, self).__init__()
+ self.token = token
+ self.expiry = None
+
+ def refresh(self, request):
+ self.token += "1"
+
+ def with_quota_project(self, quota_project_id):
+ raise NotImplementedError()
+
+
+class TestAuthMetadataPlugin(object):
+ def test_call_no_refresh(self):
+ credentials = CredentialsStub()
+ request = mock.create_autospec(transport.Request)
+
+ plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request)
+
+ context = mock.create_autospec(grpc.AuthMetadataContext, instance=True)
+ context.method_name = mock.sentinel.method_name
+ context.service_url = mock.sentinel.service_url
+ callback = mock.create_autospec(grpc.AuthMetadataPluginCallback)
+
+ plugin(context, callback)
+
+ time.sleep(2)
+
+ callback.assert_called_once_with(
+ [("authorization", "Bearer {}".format(credentials.token))], None
+ )
+
+ def test_call_refresh(self):
+ credentials = CredentialsStub()
+ credentials.expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD
+ request = mock.create_autospec(transport.Request)
+
+ plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request)
+
+ context = mock.create_autospec(grpc.AuthMetadataContext, instance=True)
+ context.method_name = mock.sentinel.method_name
+ context.service_url = mock.sentinel.service_url
+ callback = mock.create_autospec(grpc.AuthMetadataPluginCallback)
+
+ plugin(context, callback)
+
+ time.sleep(2)
+
+ assert credentials.token == "token1"
+ callback.assert_called_once_with(
+ [("authorization", "Bearer {}".format(credentials.token))], None
+ )
+
+ def test__get_authorization_headers_with_service_account(self):
+ credentials = mock.create_autospec(service_account.Credentials)
+ request = mock.create_autospec(transport.Request)
+
+ plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request)
+
+ context = mock.create_autospec(grpc.AuthMetadataContext, instance=True)
+ context.method_name = "methodName"
+ context.service_url = "https://pubsub.googleapis.com/methodName"
+
+ plugin._get_authorization_headers(context)
+
+ credentials._create_self_signed_jwt.assert_called_once_with(None)
+
+ def test__get_authorization_headers_with_service_account_and_default_host(self):
+ credentials = mock.create_autospec(service_account.Credentials)
+ request = mock.create_autospec(transport.Request)
+
+ default_host = "pubsub.googleapis.com"
+ plugin = google.auth.transport.grpc.AuthMetadataPlugin(
+ credentials, request, default_host=default_host
+ )
+
+ context = mock.create_autospec(grpc.AuthMetadataContext, instance=True)
+ context.method_name = "methodName"
+ context.service_url = "https://pubsub.googleapis.com/methodName"
+
+ plugin._get_authorization_headers(context)
+
+ credentials._create_self_signed_jwt.assert_called_once_with(
+ "https://{}/".format(default_host)
+ )
+
+
+@mock.patch(
+ "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True
+)
+@mock.patch("grpc.composite_channel_credentials", autospec=True)
+@mock.patch("grpc.metadata_call_credentials", autospec=True)
+@mock.patch("grpc.ssl_channel_credentials", autospec=True)
+@mock.patch("grpc.secure_channel", autospec=True)
+class TestSecureAuthorizedChannel(object):
+ @mock.patch(
+ "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+ )
+ @mock.patch(
+ "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+ )
+ def test_secure_authorized_channel_adc(
+ self,
+ check_dca_metadata_path,
+ read_dca_metadata_file,
+ secure_channel,
+ ssl_channel_credentials,
+ metadata_call_credentials,
+ composite_channel_credentials,
+ get_client_ssl_credentials,
+ ):
+ credentials = CredentialsStub()
+ request = mock.create_autospec(transport.Request)
+ target = "example.com:80"
+
+ # Mock the context aware metadata and client cert/key so mTLS SSL channel
+ # will be used.
+ check_dca_metadata_path.return_value = METADATA_PATH
+ read_dca_metadata_file.return_value = {
+ "cert_provider_command": ["some command"]
+ }
+ get_client_ssl_credentials.return_value = (
+ True,
+ PUBLIC_CERT_BYTES,
+ PRIVATE_KEY_BYTES,
+ None,
+ )
+
+ channel = None
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ channel = google.auth.transport.grpc.secure_authorized_channel(
+ credentials, request, target, options=mock.sentinel.options
+ )
+
+ # Check the auth plugin construction.
+ auth_plugin = metadata_call_credentials.call_args[0][0]
+ assert isinstance(auth_plugin, google.auth.transport.grpc.AuthMetadataPlugin)
+ assert auth_plugin._credentials == credentials
+ assert auth_plugin._request == request
+
+ # Check the ssl channel call.
+ ssl_channel_credentials.assert_called_once_with(
+ certificate_chain=PUBLIC_CERT_BYTES, private_key=PRIVATE_KEY_BYTES
+ )
+
+ # Check the composite credentials call.
+ composite_channel_credentials.assert_called_once_with(
+ ssl_channel_credentials.return_value, metadata_call_credentials.return_value
+ )
+
+ # Check the channel call.
+ secure_channel.assert_called_once_with(
+ target,
+ composite_channel_credentials.return_value,
+ options=mock.sentinel.options,
+ )
+ assert channel == secure_channel.return_value
+
+ @mock.patch("google.auth.transport.grpc.SslCredentials", autospec=True)
+ def test_secure_authorized_channel_adc_without_client_cert_env(
+ self,
+ ssl_credentials_adc_method,
+ secure_channel,
+ ssl_channel_credentials,
+ metadata_call_credentials,
+ composite_channel_credentials,
+ get_client_ssl_credentials,
+ ):
+ # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE
+ # environment variable is not set.
+ credentials = CredentialsStub()
+ request = mock.create_autospec(transport.Request)
+ target = "example.com:80"
+
+ channel = google.auth.transport.grpc.secure_authorized_channel(
+ credentials, request, target, options=mock.sentinel.options
+ )
+
+ # Check the auth plugin construction.
+ auth_plugin = metadata_call_credentials.call_args[0][0]
+ assert isinstance(auth_plugin, google.auth.transport.grpc.AuthMetadataPlugin)
+ assert auth_plugin._credentials == credentials
+ assert auth_plugin._request == request
+
+ # Check the ssl channel call.
+ ssl_channel_credentials.assert_called_once()
+ ssl_credentials_adc_method.assert_not_called()
+
+ # Check the composite credentials call.
+ composite_channel_credentials.assert_called_once_with(
+ ssl_channel_credentials.return_value, metadata_call_credentials.return_value
+ )
+
+ # Check the channel call.
+ secure_channel.assert_called_once_with(
+ target,
+ composite_channel_credentials.return_value,
+ options=mock.sentinel.options,
+ )
+ assert channel == secure_channel.return_value
+
+ def test_secure_authorized_channel_explicit_ssl(
+ self,
+ secure_channel,
+ ssl_channel_credentials,
+ metadata_call_credentials,
+ composite_channel_credentials,
+ get_client_ssl_credentials,
+ ):
+ credentials = mock.Mock()
+ request = mock.Mock()
+ target = "example.com:80"
+ ssl_credentials = mock.Mock()
+
+ google.auth.transport.grpc.secure_authorized_channel(
+ credentials, request, target, ssl_credentials=ssl_credentials
+ )
+
+ # Since explicit SSL credentials are provided, get_client_ssl_credentials
+ # shouldn't be called.
+ assert not get_client_ssl_credentials.called
+
+ # Check the ssl channel call.
+ assert not ssl_channel_credentials.called
+
+ # Check the composite credentials call.
+ composite_channel_credentials.assert_called_once_with(
+ ssl_credentials, metadata_call_credentials.return_value
+ )
+
+ def test_secure_authorized_channel_mutual_exclusive(
+ self,
+ secure_channel,
+ ssl_channel_credentials,
+ metadata_call_credentials,
+ composite_channel_credentials,
+ get_client_ssl_credentials,
+ ):
+ credentials = mock.Mock()
+ request = mock.Mock()
+ target = "example.com:80"
+ ssl_credentials = mock.Mock()
+ client_cert_callback = mock.Mock()
+
+ with pytest.raises(ValueError):
+ google.auth.transport.grpc.secure_authorized_channel(
+ credentials,
+ request,
+ target,
+ ssl_credentials=ssl_credentials,
+ client_cert_callback=client_cert_callback,
+ )
+
+ def test_secure_authorized_channel_with_client_cert_callback_success(
+ self,
+ secure_channel,
+ ssl_channel_credentials,
+ metadata_call_credentials,
+ composite_channel_credentials,
+ get_client_ssl_credentials,
+ ):
+ credentials = mock.Mock()
+ request = mock.Mock()
+ target = "example.com:80"
+ client_cert_callback = mock.Mock()
+ client_cert_callback.return_value = (PUBLIC_CERT_BYTES, PRIVATE_KEY_BYTES)
+
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ google.auth.transport.grpc.secure_authorized_channel(
+ credentials, request, target, client_cert_callback=client_cert_callback
+ )
+
+ client_cert_callback.assert_called_once()
+
+ # Check we are using the cert and key provided by client_cert_callback.
+ ssl_channel_credentials.assert_called_once_with(
+ certificate_chain=PUBLIC_CERT_BYTES, private_key=PRIVATE_KEY_BYTES
+ )
+
+ # Check the composite credentials call.
+ composite_channel_credentials.assert_called_once_with(
+ ssl_channel_credentials.return_value, metadata_call_credentials.return_value
+ )
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+ )
+ @mock.patch(
+ "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+ )
+ def test_secure_authorized_channel_with_client_cert_callback_failure(
+ self,
+ check_dca_metadata_path,
+ read_dca_metadata_file,
+ secure_channel,
+ ssl_channel_credentials,
+ metadata_call_credentials,
+ composite_channel_credentials,
+ get_client_ssl_credentials,
+ ):
+ credentials = mock.Mock()
+ request = mock.Mock()
+ target = "example.com:80"
+
+ client_cert_callback = mock.Mock()
+ client_cert_callback.side_effect = Exception("callback exception")
+
+ with pytest.raises(Exception) as excinfo:
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ google.auth.transport.grpc.secure_authorized_channel(
+ credentials,
+ request,
+ target,
+ client_cert_callback=client_cert_callback,
+ )
+
+ assert str(excinfo.value) == "callback exception"
+
+ def test_secure_authorized_channel_cert_callback_without_client_cert_env(
+ self,
+ secure_channel,
+ ssl_channel_credentials,
+ metadata_call_credentials,
+ composite_channel_credentials,
+ get_client_ssl_credentials,
+ ):
+ # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE
+ # environment variable is not set.
+ credentials = mock.Mock()
+ request = mock.Mock()
+ target = "example.com:80"
+ client_cert_callback = mock.Mock()
+
+ google.auth.transport.grpc.secure_authorized_channel(
+ credentials, request, target, client_cert_callback=client_cert_callback
+ )
+
+ # Check client_cert_callback is not called because GOOGLE_API_USE_CLIENT_CERTIFICATE
+ # is not set.
+ client_cert_callback.assert_not_called()
+
+ ssl_channel_credentials.assert_called_once()
+
+ # Check the composite credentials call.
+ composite_channel_credentials.assert_called_once_with(
+ ssl_channel_credentials.return_value, metadata_call_credentials.return_value
+ )
+
+
+@mock.patch("grpc.ssl_channel_credentials", autospec=True)
+@mock.patch(
+ "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True
+)
+@mock.patch("google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True)
+@mock.patch(
+ "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+)
+class TestSslCredentials(object):
+ def test_no_context_aware_metadata(
+ self,
+ mock_check_dca_metadata_path,
+ mock_read_dca_metadata_file,
+ mock_get_client_ssl_credentials,
+ mock_ssl_channel_credentials,
+ ):
+ # Mock that the metadata file doesn't exist.
+ mock_check_dca_metadata_path.return_value = None
+
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ ssl_credentials = google.auth.transport.grpc.SslCredentials()
+
+ # Since no context aware metadata is found, we wouldn't call
+ # get_client_ssl_credentials, and the SSL channel credentials created is
+ # non mTLS.
+ assert ssl_credentials.ssl_credentials is not None
+ assert not ssl_credentials.is_mtls
+ mock_get_client_ssl_credentials.assert_not_called()
+ mock_ssl_channel_credentials.assert_called_once_with()
+
+ def test_get_client_ssl_credentials_failure(
+ self,
+ mock_check_dca_metadata_path,
+ mock_read_dca_metadata_file,
+ mock_get_client_ssl_credentials,
+ mock_ssl_channel_credentials,
+ ):
+ mock_check_dca_metadata_path.return_value = METADATA_PATH
+ mock_read_dca_metadata_file.return_value = {
+ "cert_provider_command": ["some command"]
+ }
+
+ # Mock that client cert and key are not loaded and exception is raised.
+ mock_get_client_ssl_credentials.side_effect = exceptions.ClientCertError()
+
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ assert google.auth.transport.grpc.SslCredentials().ssl_credentials
+
+ def test_get_client_ssl_credentials_success(
+ self,
+ mock_check_dca_metadata_path,
+ mock_read_dca_metadata_file,
+ mock_get_client_ssl_credentials,
+ mock_ssl_channel_credentials,
+ ):
+ mock_check_dca_metadata_path.return_value = METADATA_PATH
+ mock_read_dca_metadata_file.return_value = {
+ "cert_provider_command": ["some command"]
+ }
+ mock_get_client_ssl_credentials.return_value = (
+ True,
+ PUBLIC_CERT_BYTES,
+ PRIVATE_KEY_BYTES,
+ None,
+ )
+
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ ssl_credentials = google.auth.transport.grpc.SslCredentials()
+
+ assert ssl_credentials.ssl_credentials is not None
+ assert ssl_credentials.is_mtls
+ mock_get_client_ssl_credentials.assert_called_once()
+ mock_ssl_channel_credentials.assert_called_once_with(
+ certificate_chain=PUBLIC_CERT_BYTES, private_key=PRIVATE_KEY_BYTES
+ )
+
+ def test_get_client_ssl_credentials_without_client_cert_env(
+ self,
+ mock_check_dca_metadata_path,
+ mock_read_dca_metadata_file,
+ mock_get_client_ssl_credentials,
+ mock_ssl_channel_credentials,
+ ):
+ # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set.
+ ssl_credentials = google.auth.transport.grpc.SslCredentials()
+
+ assert ssl_credentials.ssl_credentials is not None
+ assert not ssl_credentials.is_mtls
+ mock_check_dca_metadata_path.assert_not_called()
+ mock_read_dca_metadata_file.assert_not_called()
+ mock_get_client_ssl_credentials.assert_not_called()
+ mock_ssl_channel_credentials.assert_called_once()
diff --git a/contrib/python/google-auth/py3/tests/transport/test_mtls.py b/contrib/python/google-auth/py3/tests/transport/test_mtls.py
new file mode 100644
index 0000000000..b62063e479
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/transport/test_mtls.py
@@ -0,0 +1,83 @@
+# Copyright 2020 Google LLC
+#
+# 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 mock
+import pytest # type: ignore
+
+from google.auth import exceptions
+from google.auth.transport import mtls
+
+
+@mock.patch(
+ "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+)
+def test_has_default_client_cert_source(check_dca_metadata_path):
+ check_dca_metadata_path.return_value = mock.Mock()
+ assert mtls.has_default_client_cert_source()
+
+ check_dca_metadata_path.return_value = None
+ assert not mtls.has_default_client_cert_source()
+
+
+@mock.patch("google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True)
+@mock.patch("google.auth.transport.mtls.has_default_client_cert_source", autospec=True)
+def test_default_client_cert_source(
+ has_default_client_cert_source, get_client_cert_and_key
+):
+ # Test default client cert source doesn't exist.
+ has_default_client_cert_source.return_value = False
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ mtls.default_client_cert_source()
+
+ # The following tests will assume default client cert source exists.
+ has_default_client_cert_source.return_value = True
+
+ # Test good callback.
+ get_client_cert_and_key.return_value = (True, b"cert", b"key")
+ callback = mtls.default_client_cert_source()
+ assert callback() == (b"cert", b"key")
+
+ # Test bad callback which throws exception.
+ get_client_cert_and_key.side_effect = ValueError()
+ callback = mtls.default_client_cert_source()
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ callback()
+
+
+@mock.patch(
+ "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True
+)
+@mock.patch("google.auth.transport.mtls.has_default_client_cert_source", autospec=True)
+def test_default_client_encrypted_cert_source(
+ has_default_client_cert_source, get_client_ssl_credentials
+):
+ # Test default client cert source doesn't exist.
+ has_default_client_cert_source.return_value = False
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ mtls.default_client_encrypted_cert_source("cert_path", "key_path")
+
+ # The following tests will assume default client cert source exists.
+ has_default_client_cert_source.return_value = True
+
+ # Test good callback.
+ get_client_ssl_credentials.return_value = (True, b"cert", b"key", b"passphrase")
+ callback = mtls.default_client_encrypted_cert_source("cert_path", "key_path")
+ with mock.patch("{}.open".format(__name__), return_value=mock.MagicMock()):
+ assert callback() == ("cert_path", "key_path", b"passphrase")
+
+ # Test bad callback which throws exception.
+ get_client_ssl_credentials.side_effect = exceptions.ClientCertError()
+ callback = mtls.default_client_encrypted_cert_source("cert_path", "key_path")
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ callback()
diff --git a/contrib/python/google-auth/py3/tests/transport/test_requests.py b/contrib/python/google-auth/py3/tests/transport/test_requests.py
new file mode 100644
index 0000000000..d962814346
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/transport/test_requests.py
@@ -0,0 +1,575 @@
+# Copyright 2016 Google LLC
+#
+# 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 datetime
+import functools
+import http.client as http_client
+import os
+import sys
+
+import freezegun
+import mock
+import OpenSSL
+import pytest # type: ignore
+import requests
+import requests.adapters
+
+from google.auth import environment_vars
+from google.auth import exceptions
+import google.auth.credentials
+import google.auth.transport._custom_tls_signer
+import google.auth.transport._mtls_helper
+import google.auth.transport.requests
+from google.oauth2 import service_account
+from tests.transport import compliance
+
+
+@pytest.fixture
+def frozen_time():
+ with freezegun.freeze_time("1970-01-01 00:00:00", tick=False) as frozen:
+ yield frozen
+
+
+class TestRequestResponse(compliance.RequestResponseTests):
+ def make_request(self):
+ return google.auth.transport.requests.Request()
+
+ def test_timeout(self):
+ http = mock.create_autospec(requests.Session, instance=True)
+ request = google.auth.transport.requests.Request(http)
+ request(url="http://example.com", method="GET", timeout=5)
+
+ assert http.request.call_args[1]["timeout"] == 5
+
+ def test_session_closed_on_del(self):
+ http = mock.create_autospec(requests.Session, instance=True)
+ request = google.auth.transport.requests.Request(http)
+ request.__del__()
+ http.close.assert_called_with()
+
+ http = mock.create_autospec(requests.Session, instance=True)
+ http.close.side_effect = TypeError("test injected TypeError")
+ request = google.auth.transport.requests.Request(http)
+ request.__del__()
+ http.close.assert_called_with()
+
+
+class TestTimeoutGuard(object):
+ def make_guard(self, *args, **kwargs):
+ return google.auth.transport.requests.TimeoutGuard(*args, **kwargs)
+
+ def test_tracks_elapsed_time_w_numeric_timeout(self, frozen_time):
+ with self.make_guard(timeout=10) as guard:
+ frozen_time.tick(delta=datetime.timedelta(seconds=3.8))
+ assert guard.remaining_timeout == 6.2
+
+ def test_tracks_elapsed_time_w_tuple_timeout(self, frozen_time):
+ with self.make_guard(timeout=(16, 19)) as guard:
+ frozen_time.tick(delta=datetime.timedelta(seconds=3.8))
+ assert guard.remaining_timeout == (12.2, 15.2)
+
+ def test_noop_if_no_timeout(self, frozen_time):
+ with self.make_guard(timeout=None) as guard:
+ frozen_time.tick(delta=datetime.timedelta(days=3650))
+ # NOTE: no timeout error raised, despite years have passed
+ assert guard.remaining_timeout is None
+
+ def test_timeout_error_w_numeric_timeout(self, frozen_time):
+ with pytest.raises(requests.exceptions.Timeout):
+ with self.make_guard(timeout=10) as guard:
+ frozen_time.tick(delta=datetime.timedelta(seconds=10.001))
+ assert guard.remaining_timeout == pytest.approx(-0.001)
+
+ def test_timeout_error_w_tuple_timeout(self, frozen_time):
+ with pytest.raises(requests.exceptions.Timeout):
+ with self.make_guard(timeout=(11, 10)) as guard:
+ frozen_time.tick(delta=datetime.timedelta(seconds=10.001))
+ assert guard.remaining_timeout == pytest.approx((0.999, -0.001))
+
+ def test_custom_timeout_error_type(self, frozen_time):
+ class FooError(Exception):
+ pass
+
+ with pytest.raises(FooError):
+ with self.make_guard(timeout=1, timeout_error_type=FooError):
+ frozen_time.tick(delta=datetime.timedelta(seconds=2))
+
+ def test_lets_suite_errors_bubble_up(self, frozen_time):
+ with pytest.raises(IndexError):
+ with self.make_guard(timeout=1):
+ [1, 2, 3][3]
+
+
+class CredentialsStub(google.auth.credentials.Credentials):
+ def __init__(self, token="token"):
+ super(CredentialsStub, self).__init__()
+ self.token = token
+
+ def apply(self, headers, token=None):
+ headers["authorization"] = self.token
+
+ def before_request(self, request, method, url, headers):
+ self.apply(headers)
+
+ def refresh(self, request):
+ self.token += "1"
+
+ def with_quota_project(self, quota_project_id):
+ raise NotImplementedError()
+
+
+class TimeTickCredentialsStub(CredentialsStub):
+ """Credentials that spend some (mocked) time when refreshing a token."""
+
+ def __init__(self, time_tick, token="token"):
+ self._time_tick = time_tick
+ super(TimeTickCredentialsStub, self).__init__(token=token)
+
+ def refresh(self, request):
+ self._time_tick()
+ super(TimeTickCredentialsStub, self).refresh(requests)
+
+
+class AdapterStub(requests.adapters.BaseAdapter):
+ def __init__(self, responses, headers=None):
+ super(AdapterStub, self).__init__()
+ self.responses = responses
+ self.requests = []
+ self.headers = headers or {}
+
+ def send(self, request, **kwargs):
+ # pylint: disable=arguments-differ
+ # request is the only required argument here and the only argument
+ # we care about.
+ self.requests.append(request)
+ return self.responses.pop(0)
+
+ def close(self): # pragma: NO COVER
+ # pylint wants this to be here because it's abstract in the base
+ # class, but requests never actually calls it.
+ return
+
+
+class TimeTickAdapterStub(AdapterStub):
+ """Adapter that spends some (mocked) time when making a request."""
+
+ def __init__(self, time_tick, responses, headers=None):
+ self._time_tick = time_tick
+ super(TimeTickAdapterStub, self).__init__(responses, headers=headers)
+
+ def send(self, request, **kwargs):
+ self._time_tick()
+ return super(TimeTickAdapterStub, self).send(request, **kwargs)
+
+
+class TestMutualTlsAdapter(object):
+ @mock.patch.object(requests.adapters.HTTPAdapter, "init_poolmanager")
+ @mock.patch.object(requests.adapters.HTTPAdapter, "proxy_manager_for")
+ def test_success(self, mock_proxy_manager_for, mock_init_poolmanager):
+ adapter = google.auth.transport.requests._MutualTlsAdapter(
+ pytest.public_cert_bytes, pytest.private_key_bytes
+ )
+
+ adapter.init_poolmanager()
+ mock_init_poolmanager.assert_called_with(ssl_context=adapter._ctx_poolmanager)
+
+ adapter.proxy_manager_for()
+ mock_proxy_manager_for.assert_called_with(ssl_context=adapter._ctx_proxymanager)
+
+ def test_invalid_cert_or_key(self):
+ with pytest.raises(OpenSSL.crypto.Error):
+ google.auth.transport.requests._MutualTlsAdapter(
+ b"invalid cert", b"invalid key"
+ )
+
+ @mock.patch.dict("sys.modules", {"OpenSSL.crypto": None})
+ def test_import_error(self):
+ with pytest.raises(ImportError):
+ google.auth.transport.requests._MutualTlsAdapter(
+ pytest.public_cert_bytes, pytest.private_key_bytes
+ )
+
+
+def make_response(status=http_client.OK, data=None):
+ response = requests.Response()
+ response.status_code = status
+ response._content = data
+ return response
+
+
+class TestAuthorizedSession(object):
+ TEST_URL = "http://example.com/"
+
+ def test_constructor(self):
+ authed_session = google.auth.transport.requests.AuthorizedSession(
+ mock.sentinel.credentials
+ )
+
+ assert authed_session.credentials == mock.sentinel.credentials
+
+ def test_constructor_with_auth_request(self):
+ http = mock.create_autospec(requests.Session)
+ auth_request = google.auth.transport.requests.Request(http)
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(
+ mock.sentinel.credentials, auth_request=auth_request
+ )
+
+ assert authed_session._auth_request is auth_request
+
+ def test_request_default_timeout(self):
+ credentials = mock.Mock(wraps=CredentialsStub())
+ response = make_response()
+ adapter = AdapterStub([response])
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+ authed_session.mount(self.TEST_URL, adapter)
+
+ patcher = mock.patch("google.auth.transport.requests.requests.Session.request")
+ with patcher as patched_request:
+ authed_session.request("GET", self.TEST_URL)
+
+ expected_timeout = google.auth.transport.requests._DEFAULT_TIMEOUT
+ assert patched_request.call_args[1]["timeout"] == expected_timeout
+
+ def test_request_no_refresh(self):
+ credentials = mock.Mock(wraps=CredentialsStub())
+ response = make_response()
+ adapter = AdapterStub([response])
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+ authed_session.mount(self.TEST_URL, adapter)
+
+ result = authed_session.request("GET", self.TEST_URL)
+
+ assert response == result
+ assert credentials.before_request.called
+ assert not credentials.refresh.called
+ assert len(adapter.requests) == 1
+ assert adapter.requests[0].url == self.TEST_URL
+ assert adapter.requests[0].headers["authorization"] == "token"
+
+ def test_request_refresh(self):
+ credentials = mock.Mock(wraps=CredentialsStub())
+ final_response = make_response(status=http_client.OK)
+ # First request will 401, second request will succeed.
+ adapter = AdapterStub(
+ [make_response(status=http_client.UNAUTHORIZED), final_response]
+ )
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(
+ credentials, refresh_timeout=60
+ )
+ authed_session.mount(self.TEST_URL, adapter)
+
+ result = authed_session.request("GET", self.TEST_URL)
+
+ assert result == final_response
+ assert credentials.before_request.call_count == 2
+ assert credentials.refresh.called
+ assert len(adapter.requests) == 2
+
+ assert adapter.requests[0].url == self.TEST_URL
+ assert adapter.requests[0].headers["authorization"] == "token"
+
+ assert adapter.requests[1].url == self.TEST_URL
+ assert adapter.requests[1].headers["authorization"] == "token1"
+
+ def test_request_max_allowed_time_timeout_error(self, frozen_time):
+ tick_one_second = functools.partial(
+ frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+ )
+
+ credentials = mock.Mock(
+ wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
+ )
+ adapter = TimeTickAdapterStub(
+ time_tick=tick_one_second, responses=[make_response(status=http_client.OK)]
+ )
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+ authed_session.mount(self.TEST_URL, adapter)
+
+ # Because a request takes a full mocked second, max_allowed_time shorter
+ # than that will cause a timeout error.
+ with pytest.raises(requests.exceptions.Timeout):
+ authed_session.request("GET", self.TEST_URL, max_allowed_time=0.9)
+
+ def test_request_max_allowed_time_w_transport_timeout_no_error(self, frozen_time):
+ tick_one_second = functools.partial(
+ frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+ )
+
+ credentials = mock.Mock(
+ wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
+ )
+ adapter = TimeTickAdapterStub(
+ time_tick=tick_one_second,
+ responses=[
+ make_response(status=http_client.UNAUTHORIZED),
+ make_response(status=http_client.OK),
+ ],
+ )
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+ authed_session.mount(self.TEST_URL, adapter)
+
+ # A short configured transport timeout does not affect max_allowed_time.
+ # The latter is not adjusted to it and is only concerned with the actual
+ # execution time. The call below should thus not raise a timeout error.
+ authed_session.request("GET", self.TEST_URL, timeout=0.5, max_allowed_time=3.1)
+
+ def test_request_max_allowed_time_w_refresh_timeout_no_error(self, frozen_time):
+ tick_one_second = functools.partial(
+ frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+ )
+
+ credentials = mock.Mock(
+ wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
+ )
+ adapter = TimeTickAdapterStub(
+ time_tick=tick_one_second,
+ responses=[
+ make_response(status=http_client.UNAUTHORIZED),
+ make_response(status=http_client.OK),
+ ],
+ )
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(
+ credentials, refresh_timeout=1.1
+ )
+ authed_session.mount(self.TEST_URL, adapter)
+
+ # A short configured refresh timeout does not affect max_allowed_time.
+ # The latter is not adjusted to it and is only concerned with the actual
+ # execution time. The call below should thus not raise a timeout error
+ # (and `timeout` does not come into play either, as it's very long).
+ authed_session.request("GET", self.TEST_URL, timeout=60, max_allowed_time=3.1)
+
+ def test_request_timeout_w_refresh_timeout_timeout_error(self, frozen_time):
+ tick_one_second = functools.partial(
+ frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+ )
+
+ credentials = mock.Mock(
+ wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
+ )
+ adapter = TimeTickAdapterStub(
+ time_tick=tick_one_second,
+ responses=[
+ make_response(status=http_client.UNAUTHORIZED),
+ make_response(status=http_client.OK),
+ ],
+ )
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(
+ credentials, refresh_timeout=100
+ )
+ authed_session.mount(self.TEST_URL, adapter)
+
+ # An UNAUTHORIZED response triggers a refresh (an extra request), thus
+ # the final request that otherwise succeeds results in a timeout error
+ # (all three requests together last 3 mocked seconds).
+ with pytest.raises(requests.exceptions.Timeout):
+ authed_session.request(
+ "GET", self.TEST_URL, timeout=60, max_allowed_time=2.9
+ )
+
+ def test_authorized_session_without_default_host(self):
+ credentials = mock.create_autospec(service_account.Credentials)
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+
+ authed_session.credentials._create_self_signed_jwt.assert_called_once_with(None)
+
+ def test_authorized_session_with_default_host(self):
+ default_host = "pubsub.googleapis.com"
+ credentials = mock.create_autospec(service_account.Credentials)
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(
+ credentials, default_host=default_host
+ )
+
+ authed_session.credentials._create_self_signed_jwt.assert_called_once_with(
+ "https://{}/".format(default_host)
+ )
+
+ def test_configure_mtls_channel_with_callback(self):
+ mock_callback = mock.Mock()
+ mock_callback.return_value = (
+ pytest.public_cert_bytes,
+ pytest.private_key_bytes,
+ )
+
+ auth_session = google.auth.transport.requests.AuthorizedSession(
+ credentials=mock.Mock()
+ )
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ auth_session.configure_mtls_channel(mock_callback)
+
+ assert auth_session.is_mtls
+ assert isinstance(
+ auth_session.adapters["https://"],
+ google.auth.transport.requests._MutualTlsAdapter,
+ )
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+ )
+ def test_configure_mtls_channel_with_metadata(self, mock_get_client_cert_and_key):
+ mock_get_client_cert_and_key.return_value = (
+ True,
+ pytest.public_cert_bytes,
+ pytest.private_key_bytes,
+ )
+
+ auth_session = google.auth.transport.requests.AuthorizedSession(
+ credentials=mock.Mock()
+ )
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ auth_session.configure_mtls_channel()
+
+ assert auth_session.is_mtls
+ assert isinstance(
+ auth_session.adapters["https://"],
+ google.auth.transport.requests._MutualTlsAdapter,
+ )
+
+ @mock.patch.object(google.auth.transport.requests._MutualTlsAdapter, "__init__")
+ @mock.patch(
+ "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+ )
+ def test_configure_mtls_channel_non_mtls(
+ self, mock_get_client_cert_and_key, mock_adapter_ctor
+ ):
+ mock_get_client_cert_and_key.return_value = (False, None, None)
+
+ auth_session = google.auth.transport.requests.AuthorizedSession(
+ credentials=mock.Mock()
+ )
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ auth_session.configure_mtls_channel()
+
+ assert not auth_session.is_mtls
+
+ # Assert _MutualTlsAdapter constructor is not called.
+ mock_adapter_ctor.assert_not_called()
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+ )
+ def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key):
+ mock_get_client_cert_and_key.side_effect = exceptions.ClientCertError()
+
+ auth_session = google.auth.transport.requests.AuthorizedSession(
+ credentials=mock.Mock()
+ )
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ auth_session.configure_mtls_channel()
+
+ mock_get_client_cert_and_key.return_value = (False, None, None)
+ with mock.patch.dict("sys.modules"):
+ sys.modules["OpenSSL"] = None
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ with mock.patch.dict(
+ os.environ,
+ {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"},
+ ):
+ auth_session.configure_mtls_channel()
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+ )
+ def test_configure_mtls_channel_without_client_cert_env(
+ self, get_client_cert_and_key
+ ):
+ # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE
+ # environment variable is not set.
+ auth_session = google.auth.transport.requests.AuthorizedSession(
+ credentials=mock.Mock()
+ )
+
+ auth_session.configure_mtls_channel()
+ assert not auth_session.is_mtls
+ get_client_cert_and_key.assert_not_called()
+
+ mock_callback = mock.Mock()
+ auth_session.configure_mtls_channel(mock_callback)
+ assert not auth_session.is_mtls
+ mock_callback.assert_not_called()
+
+ def test_close_wo_passed_in_auth_request(self):
+ authed_session = google.auth.transport.requests.AuthorizedSession(
+ mock.sentinel.credentials
+ )
+ authed_session._auth_request_session = mock.Mock(spec=["close"])
+
+ authed_session.close()
+
+ authed_session._auth_request_session.close.assert_called_once_with()
+
+ def test_close_w_passed_in_auth_request(self):
+ http = mock.create_autospec(requests.Session)
+ auth_request = google.auth.transport.requests.Request(http)
+ authed_session = google.auth.transport.requests.AuthorizedSession(
+ mock.sentinel.credentials, auth_request=auth_request
+ )
+
+ authed_session.close() # no raise
+
+
+class TestMutualTlsOffloadAdapter(object):
+ @mock.patch.object(requests.adapters.HTTPAdapter, "init_poolmanager")
+ @mock.patch.object(requests.adapters.HTTPAdapter, "proxy_manager_for")
+ @mock.patch.object(
+ google.auth.transport._custom_tls_signer.CustomTlsSigner, "load_libraries"
+ )
+ @mock.patch.object(
+ google.auth.transport._custom_tls_signer.CustomTlsSigner, "set_up_custom_key"
+ )
+ @mock.patch.object(
+ google.auth.transport._custom_tls_signer.CustomTlsSigner,
+ "attach_to_ssl_context",
+ )
+ def test_success(
+ self,
+ mock_attach_to_ssl_context,
+ mock_set_up_custom_key,
+ mock_load_libraries,
+ mock_proxy_manager_for,
+ mock_init_poolmanager,
+ ):
+ enterprise_cert_file_path = "/path/to/enterprise/cert/json"
+ adapter = google.auth.transport.requests._MutualTlsOffloadAdapter(
+ enterprise_cert_file_path
+ )
+
+ mock_load_libraries.assert_called_once()
+ mock_set_up_custom_key.assert_called_once()
+ assert mock_attach_to_ssl_context.call_count == 2
+
+ adapter.init_poolmanager()
+ mock_init_poolmanager.assert_called_with(ssl_context=adapter._ctx_poolmanager)
+
+ adapter.proxy_manager_for()
+ mock_proxy_manager_for.assert_called_with(ssl_context=adapter._ctx_proxymanager)
diff --git a/contrib/python/google-auth/py3/tests/transport/test_urllib3.py b/contrib/python/google-auth/py3/tests/transport/test_urllib3.py
new file mode 100644
index 0000000000..e832300321
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/transport/test_urllib3.py
@@ -0,0 +1,322 @@
+# Copyright 2016 Google LLC
+#
+# 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 http.client as http_client
+import os
+import sys
+
+import mock
+import OpenSSL
+import pytest # type: ignore
+import urllib3 # type: ignore
+
+from google.auth import environment_vars
+from google.auth import exceptions
+import google.auth.credentials
+import google.auth.transport._mtls_helper
+import google.auth.transport.urllib3
+from google.oauth2 import service_account
+from tests.transport import compliance
+
+
+class TestRequestResponse(compliance.RequestResponseTests):
+ def make_request(self):
+ http = urllib3.PoolManager()
+ return google.auth.transport.urllib3.Request(http)
+
+ def test_timeout(self):
+ http = mock.create_autospec(urllib3.PoolManager)
+ request = google.auth.transport.urllib3.Request(http)
+ request(url="http://example.com", method="GET", timeout=5)
+
+ assert http.request.call_args[1]["timeout"] == 5
+
+
+def test__make_default_http_with_certifi():
+ http = google.auth.transport.urllib3._make_default_http()
+ assert "cert_reqs" in http.connection_pool_kw
+
+
+@mock.patch.object(google.auth.transport.urllib3, "certifi", new=None)
+def test__make_default_http_without_certifi():
+ http = google.auth.transport.urllib3._make_default_http()
+ assert "cert_reqs" not in http.connection_pool_kw
+
+
+class CredentialsStub(google.auth.credentials.Credentials):
+ def __init__(self, token="token"):
+ super(CredentialsStub, self).__init__()
+ self.token = token
+
+ def apply(self, headers, token=None):
+ headers["authorization"] = self.token
+
+ def before_request(self, request, method, url, headers):
+ self.apply(headers)
+
+ def refresh(self, request):
+ self.token += "1"
+
+ def with_quota_project(self, quota_project_id):
+ raise NotImplementedError()
+
+
+class HttpStub(object):
+ def __init__(self, responses, headers=None):
+ self.responses = responses
+ self.requests = []
+ self.headers = headers or {}
+
+ def urlopen(self, method, url, body=None, headers=None, **kwargs):
+ self.requests.append((method, url, body, headers, kwargs))
+ return self.responses.pop(0)
+
+ def clear(self):
+ pass
+
+
+class ResponseStub(object):
+ def __init__(self, status=http_client.OK, data=None):
+ self.status = status
+ self.data = data
+
+
+class TestMakeMutualTlsHttp(object):
+ def test_success(self):
+ http = google.auth.transport.urllib3._make_mutual_tls_http(
+ pytest.public_cert_bytes, pytest.private_key_bytes
+ )
+ assert isinstance(http, urllib3.PoolManager)
+
+ def test_crypto_error(self):
+ with pytest.raises(OpenSSL.crypto.Error):
+ google.auth.transport.urllib3._make_mutual_tls_http(
+ b"invalid cert", b"invalid key"
+ )
+
+ @mock.patch.dict("sys.modules", {"OpenSSL.crypto": None})
+ def test_import_error(self):
+ with pytest.raises(ImportError):
+ google.auth.transport.urllib3._make_mutual_tls_http(
+ pytest.public_cert_bytes, pytest.private_key_bytes
+ )
+
+
+class TestAuthorizedHttp(object):
+ TEST_URL = "http://example.com"
+
+ def test_authed_http_defaults(self):
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+ mock.sentinel.credentials
+ )
+
+ assert authed_http.credentials == mock.sentinel.credentials
+ assert isinstance(authed_http.http, urllib3.PoolManager)
+
+ def test_urlopen_no_refresh(self):
+ credentials = mock.Mock(wraps=CredentialsStub())
+ response = ResponseStub()
+ http = HttpStub([response])
+
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+ credentials, http=http
+ )
+
+ result = authed_http.urlopen("GET", self.TEST_URL)
+
+ assert result == response
+ assert credentials.before_request.called
+ assert not credentials.refresh.called
+ assert http.requests == [
+ ("GET", self.TEST_URL, None, {"authorization": "token"}, {})
+ ]
+
+ def test_urlopen_refresh(self):
+ credentials = mock.Mock(wraps=CredentialsStub())
+ final_response = ResponseStub(status=http_client.OK)
+ # First request will 401, second request will succeed.
+ http = HttpStub([ResponseStub(status=http_client.UNAUTHORIZED), final_response])
+
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+ credentials, http=http
+ )
+
+ authed_http = authed_http.urlopen("GET", "http://example.com")
+
+ assert authed_http == final_response
+ assert credentials.before_request.call_count == 2
+ assert credentials.refresh.called
+ assert http.requests == [
+ ("GET", self.TEST_URL, None, {"authorization": "token"}, {}),
+ ("GET", self.TEST_URL, None, {"authorization": "token1"}, {}),
+ ]
+
+ def test_urlopen_no_default_host(self):
+ credentials = mock.create_autospec(service_account.Credentials)
+
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
+
+ authed_http.credentials._create_self_signed_jwt.assert_called_once_with(None)
+
+ def test_urlopen_with_default_host(self):
+ default_host = "pubsub.googleapis.com"
+ credentials = mock.create_autospec(service_account.Credentials)
+
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+ credentials, default_host=default_host
+ )
+
+ authed_http.credentials._create_self_signed_jwt.assert_called_once_with(
+ "https://{}/".format(default_host)
+ )
+
+ def test_proxies(self):
+ http = mock.create_autospec(urllib3.PoolManager)
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(None, http=http)
+
+ with authed_http:
+ pass
+
+ assert http.__enter__.called
+ assert http.__exit__.called
+
+ authed_http.headers = mock.sentinel.headers
+ assert authed_http.headers == http.headers
+
+ @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True)
+ def test_configure_mtls_channel_with_callback(self, mock_make_mutual_tls_http):
+ callback = mock.Mock()
+ callback.return_value = (pytest.public_cert_bytes, pytest.private_key_bytes)
+
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+ credentials=mock.Mock(), http=mock.Mock()
+ )
+
+ with pytest.warns(UserWarning):
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ is_mtls = authed_http.configure_mtls_channel(callback)
+
+ assert is_mtls
+ mock_make_mutual_tls_http.assert_called_once_with(
+ cert=pytest.public_cert_bytes, key=pytest.private_key_bytes
+ )
+
+ @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True)
+ @mock.patch(
+ "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+ )
+ def test_configure_mtls_channel_with_metadata(
+ self, mock_get_client_cert_and_key, mock_make_mutual_tls_http
+ ):
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+ credentials=mock.Mock()
+ )
+
+ mock_get_client_cert_and_key.return_value = (
+ True,
+ pytest.public_cert_bytes,
+ pytest.private_key_bytes,
+ )
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ is_mtls = authed_http.configure_mtls_channel()
+
+ assert is_mtls
+ mock_get_client_cert_and_key.assert_called_once()
+ mock_make_mutual_tls_http.assert_called_once_with(
+ cert=pytest.public_cert_bytes, key=pytest.private_key_bytes
+ )
+
+ @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True)
+ @mock.patch(
+ "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+ )
+ def test_configure_mtls_channel_non_mtls(
+ self, mock_get_client_cert_and_key, mock_make_mutual_tls_http
+ ):
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+ credentials=mock.Mock()
+ )
+
+ mock_get_client_cert_and_key.return_value = (False, None, None)
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ is_mtls = authed_http.configure_mtls_channel()
+
+ assert not is_mtls
+ mock_get_client_cert_and_key.assert_called_once()
+ mock_make_mutual_tls_http.assert_not_called()
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+ )
+ def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key):
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+ credentials=mock.Mock()
+ )
+
+ mock_get_client_cert_and_key.side_effect = exceptions.ClientCertError()
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ with mock.patch.dict(
+ os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+ ):
+ authed_http.configure_mtls_channel()
+
+ mock_get_client_cert_and_key.return_value = (False, None, None)
+ with mock.patch.dict("sys.modules"):
+ sys.modules["OpenSSL"] = None
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ with mock.patch.dict(
+ os.environ,
+ {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"},
+ ):
+ authed_http.configure_mtls_channel()
+
+ @mock.patch(
+ "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+ )
+ def test_configure_mtls_channel_without_client_cert_env(
+ self, get_client_cert_and_key
+ ):
+ callback = mock.Mock()
+
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+ credentials=mock.Mock(), http=mock.Mock()
+ )
+
+ # Test the callback is not called if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set.
+ is_mtls = authed_http.configure_mtls_channel(callback)
+ assert not is_mtls
+ callback.assert_not_called()
+
+ # Test ADC client cert is not used if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set.
+ is_mtls = authed_http.configure_mtls_channel(callback)
+ assert not is_mtls
+ get_client_cert_and_key.assert_not_called()
+
+ def test_clear_pool_on_del(self):
+ http = mock.create_autospec(urllib3.PoolManager)
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+ mock.sentinel.credentials, http=http
+ )
+ authed_http.__del__()
+ http.clear.assert_called_with()
+
+ authed_http.http = None
+ authed_http.__del__()
+ # Expect it to not crash
diff --git a/contrib/python/google-auth/py3/tests/ya.make b/contrib/python/google-auth/py3/tests/ya.make
new file mode 100644
index 0000000000..e7a1b3b272
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/ya.make
@@ -0,0 +1,77 @@
+PY3TEST()
+
+PEERDIR(
+ contrib/python/Flask
+ contrib/python/google-auth
+ contrib/python/mock
+ contrib/python/responses
+ contrib/python/pyOpenSSL
+ contrib/python/pytest-localserver
+ contrib/python/oauth2client
+ contrib/python/freezegun
+)
+
+DATA(
+ arcadia/contrib/python/google-auth/py3/tests/data
+)
+
+PY_SRCS(
+ NAMESPACE tests
+ transport/__init__.py
+ transport/compliance.py
+)
+
+TEST_SRCS(
+ __init__.py
+ compute_engine/__init__.py
+ compute_engine/test__metadata.py
+ compute_engine/test_credentials.py
+ conftest.py
+ crypt/__init__.py
+ crypt/test__cryptography_rsa.py
+ crypt/test__python_rsa.py
+ crypt/test_crypt.py
+ crypt/test_es256.py
+ oauth2/__init__.py
+ oauth2/test__client.py
+ # oauth2/test_challenges.py - need pyu2f
+ oauth2/test_credentials.py
+ oauth2/test_gdch_credentials.py
+ oauth2/test_id_token.py
+ oauth2/test_reauth.py
+ oauth2/test_service_account.py
+ oauth2/test_sts.py
+ oauth2/test_utils.py
+ test__cloud_sdk.py
+ test__default.py
+ test__helpers.py
+ test__oauth2client.py
+ test__service_account_info.py
+ test_app_engine.py
+ test_aws.py
+ test_credentials.py
+ test_downscoped.py
+ test_external_account.py
+ test_external_account_authorized_user.py
+ test_iam.py
+ test_identity_pool.py
+ test_impersonated_credentials.py
+ test_jwt.py
+ test_pluggable.py
+ # transport/test__custom_tls_signer.py
+ transport/test__http_client.py
+ transport/test__mtls_helper.py
+ transport/test_grpc.py
+ transport/test_mtls.py
+ # transport/test_requests.py
+ # transport/test_urllib3.py
+)
+
+RESOURCE(
+ data/privatekey.pem data/privatekey.pem
+ data/public_cert.pem data/public_cert.pem
+)
+
+NO_LINT()
+
+END()
diff --git a/contrib/python/google-auth/py3/ya.make b/contrib/python/google-auth/py3/ya.make
new file mode 100644
index 0000000000..77b6e5f741
--- /dev/null
+++ b/contrib/python/google-auth/py3/ya.make
@@ -0,0 +1,100 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+VERSION(2.23.0)
+
+LICENSE(Apache-2.0)
+
+PEERDIR(
+ contrib/python/cachetools
+ contrib/python/cryptography
+ contrib/python/grpcio
+ contrib/python/pyasn1-modules
+ contrib/python/requests
+ contrib/python/rsa
+ contrib/python/urllib3
+)
+
+NO_LINT()
+
+NO_CHECK_IMPORTS(
+ google.auth._oauth2client
+ google.auth.transport._aiohttp_requests
+)
+
+PY_SRCS(
+ TOP_LEVEL
+ google/auth/__init__.py
+ google/auth/_cloud_sdk.py
+ google/auth/_credentials_async.py
+ google/auth/_default.py
+ google/auth/_default_async.py
+ google/auth/_exponential_backoff.py
+ google/auth/_helpers.py
+ google/auth/_jwt_async.py
+ google/auth/_oauth2client.py
+ google/auth/_service_account_info.py
+ google/auth/api_key.py
+ google/auth/app_engine.py
+ google/auth/aws.py
+ google/auth/compute_engine/__init__.py
+ google/auth/compute_engine/_metadata.py
+ google/auth/compute_engine/credentials.py
+ google/auth/credentials.py
+ google/auth/crypt/__init__.py
+ google/auth/crypt/_cryptography_rsa.py
+ google/auth/crypt/_helpers.py
+ google/auth/crypt/_python_rsa.py
+ google/auth/crypt/base.py
+ google/auth/crypt/es256.py
+ google/auth/crypt/rsa.py
+ google/auth/downscoped.py
+ google/auth/environment_vars.py
+ google/auth/exceptions.py
+ google/auth/external_account.py
+ google/auth/external_account_authorized_user.py
+ google/auth/iam.py
+ google/auth/identity_pool.py
+ google/auth/impersonated_credentials.py
+ google/auth/jwt.py
+ google/auth/metrics.py
+ google/auth/pluggable.py
+ google/auth/transport/__init__.py
+ google/auth/transport/_aiohttp_requests.py
+ google/auth/transport/_custom_tls_signer.py
+ google/auth/transport/_http_client.py
+ google/auth/transport/_mtls_helper.py
+ google/auth/transport/grpc.py
+ google/auth/transport/mtls.py
+ google/auth/transport/requests.py
+ google/auth/transport/urllib3.py
+ google/auth/version.py
+ google/oauth2/__init__.py
+ google/oauth2/_client.py
+ google/oauth2/_client_async.py
+ google/oauth2/_credentials_async.py
+ google/oauth2/_id_token_async.py
+ google/oauth2/_reauth_async.py
+ google/oauth2/_service_account_async.py
+ google/oauth2/challenges.py
+ google/oauth2/credentials.py
+ google/oauth2/gdch_credentials.py
+ google/oauth2/id_token.py
+ google/oauth2/reauth.py
+ google/oauth2/service_account.py
+ google/oauth2/sts.py
+ google/oauth2/utils.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/google-auth/py3/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ tests
+)