aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/google-auth/py3/tests
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/tests
parent84f2d3d4cc985e63217cff149bd2e6d67ae6fe22 (diff)
downloadydb-0e578a4c44d4abd539d9838347b9ebafaca41dfb.tar.gz
Change "ya.make"
Diffstat (limited to 'contrib/python/google-auth/py3/tests')
-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
81 files changed, 20716 insertions, 0 deletions
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()