From 92b42a183ee51a1a86b2c72c9296a26cfbcd20d4 Mon Sep 17 00:00:00 2001 From: amtk3 Date: Thu, 15 Jan 2026 15:07:02 +1100 Subject: [PATCH 1/8] Support the mTLS IAM domain for Certificate based Access --- google/auth/iam.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/google/auth/iam.py b/google/auth/iam.py index 1e4cdffec..e828af33d 100644 --- a/google/auth/iam.py +++ b/google/auth/iam.py @@ -28,6 +28,7 @@ from google.auth import credentials from google.auth import crypt from google.auth import exceptions +from google.auth.transport import _mtls_helper IAM_RETRY_CODES = { http_client.INTERNAL_SERVER_ERROR, @@ -38,26 +39,20 @@ _IAM_SCOPE = ["https://www.googleapis.com/auth/iam"] -_IAM_ENDPOINT = ( - "https://iamcredentials.googleapis.com/v1/projects/-" - + "/serviceAccounts/{}:generateAccessToken" -) +# 1. Determine if the IAM mTLS domain should be used +if hasattr(_mtls_helper, "check_use_client_cert") and _mtls_helper.check_use_client_cert(): + domain = "iamcredentials.mtls.googleapis.com" +else: + domain = "iamcredentials.googleapis.com" -_IAM_SIGN_ENDPOINT = ( - "https://iamcredentials.googleapis.com/v1/projects/-" - + "/serviceAccounts/{}:signBlob" -) - -_IAM_SIGNJWT_ENDPOINT = ( - "https://iamcredentials.googleapis.com/v1/projects/-" - + "/serviceAccounts/{}:signJwt" -) - -_IAM_IDTOKEN_ENDPOINT = ( - "https://iamcredentials.googleapis.com/v1/" - + "projects/-/serviceAccounts/{}:generateIdToken" -) +# 2. Create the common base URL +base_url = f"https://{domain}/v1/projects/-/serviceAccounts/{{}}" +# 3. Define the endpoints +_IAM_ENDPOINT = base_url + ":generateAccessToken" +_IAM_SIGN_ENDPOINT = base_url + ":signBlob" +_IAM_SIGNJWT_ENDPOINT = base_url + ":signJwt" +_IAM_IDTOKEN_ENDPOINT = base_url + ":generateIdToken" class Signer(crypt.Signer): """Signs messages using the IAM `signBlob API`_. From af469a3bca7f7f6a8c4d2d3b46b778f8ba07a16c Mon Sep 17 00:00:00 2001 From: amtk3 Date: Thu, 15 Jan 2026 15:50:09 +1100 Subject: [PATCH 2/8] Update google/auth/iam.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- google/auth/iam.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/google/auth/iam.py b/google/auth/iam.py index e828af33d..fdd5c8381 100644 --- a/google/auth/iam.py +++ b/google/auth/iam.py @@ -49,8 +49,8 @@ base_url = f"https://{domain}/v1/projects/-/serviceAccounts/{{}}" # 3. Define the endpoints -_IAM_ENDPOINT = base_url + ":generateAccessToken" -_IAM_SIGN_ENDPOINT = base_url + ":signBlob" +_IAM_ENDPOINT = base_url + ":generateAccessToken" +_IAM_SIGN_ENDPOINT = base_url + ":signBlob" _IAM_SIGNJWT_ENDPOINT = base_url + ":signJwt" _IAM_IDTOKEN_ENDPOINT = base_url + ":generateIdToken" From b52742ba25d0fc6c57908c3d923236af2c8c443d Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 16 Jan 2026 11:41:23 -0500 Subject: [PATCH 3/8] updates handling of mtls and universe domain --- google/auth/iam.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/google/auth/iam.py b/google/auth/iam.py index fdd5c8381..fec4b0219 100644 --- a/google/auth/iam.py +++ b/google/auth/iam.py @@ -28,7 +28,7 @@ from google.auth import credentials from google.auth import crypt from google.auth import exceptions -from google.auth.transport import _mtls_helper +from google.auth.transport import mtls IAM_RETRY_CODES = { http_client.INTERNAL_SERVER_ERROR, @@ -39,20 +39,31 @@ _IAM_SCOPE = ["https://www.googleapis.com/auth/iam"] -# 1. Determine if the IAM mTLS domain should be used -if hasattr(_mtls_helper, "check_use_client_cert") and _mtls_helper.check_use_client_cert(): - domain = "iamcredentials.mtls.googleapis.com" +# 1. Determine if we should use mTLS. +# Note: We only support automatic mTLS on the default googleapis.com universe. +if hasattr(mtls, "should_use_client_cert"): + use_client_cert = mtls.should_use_client_cert() +else: # pragma: NO COVER + # if unsupported, fallback to reading from env var + use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower() == "true" + +# 2. Construct the template domain using the library's DEFAULT_UNIVERSE_DOMAIN constant. +# This ensures that the .replace() calls in the classes will work correctly. +if use_client_cert: + # We use the .mtls. prefix only for the default universe template + _IAM_DOMAIN = f"iamcredentials.mtls.{credentials.DEFAULT_UNIVERSE_DOMAIN}" else: - domain = "iamcredentials.googleapis.com" + _IAM_DOMAIN = f"iamcredentials.{credentials.DEFAULT_UNIVERSE_DOMAIN}" -# 2. Create the common base URL -base_url = f"https://{domain}/v1/projects/-/serviceAccounts/{{}}" +# 3. Create the common base URL template +# We use double brackets {{}} so .format() can be called later for the email. +_IAM_BASE_URL = f"https://{_IAM_DOMAIN}/v1/projects/-/serviceAccounts/{{}}" -# 3. Define the endpoints -_IAM_ENDPOINT = base_url + ":generateAccessToken" -_IAM_SIGN_ENDPOINT = base_url + ":signBlob" -_IAM_SIGNJWT_ENDPOINT = base_url + ":signJwt" -_IAM_IDTOKEN_ENDPOINT = base_url + ":generateIdToken" +# 4. Define the endpoints as templates +_IAM_ENDPOINT = _IAM_BASE_URL + ":generateAccessToken" +_IAM_SIGN_ENDPOINT = _IAM_BASE_URL + ":signBlob" +_IAM_SIGNJWT_ENDPOINT = _IAM_BASE_URL + ":signJwt" +_IAM_IDTOKEN_ENDPOINT = _IAM_BASE_URL + ":generateIdToken" class Signer(crypt.Signer): """Signs messages using the IAM `signBlob API`_. From 64cd681a65f97161fbf48619aa0c99bd6f1324d6 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 16 Jan 2026 12:00:59 -0500 Subject: [PATCH 4/8] updates linting --- google/auth/iam.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/google/auth/iam.py b/google/auth/iam.py index fec4b0219..22684a9d1 100644 --- a/google/auth/iam.py +++ b/google/auth/iam.py @@ -22,6 +22,7 @@ import base64 import http.client as http_client import json +import os from google.auth import _exponential_backoff from google.auth import _helpers @@ -39,13 +40,15 @@ _IAM_SCOPE = ["https://www.googleapis.com/auth/iam"] -# 1. Determine if we should use mTLS. +# 1. Determine if we should use mTLS. # Note: We only support automatic mTLS on the default googleapis.com universe. if hasattr(mtls, "should_use_client_cert"): use_client_cert = mtls.should_use_client_cert() else: # pragma: NO COVER # if unsupported, fallback to reading from env var - use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower() == "true" + use_client_cert = ( + os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower() == "true" + ) # 2. Construct the template domain using the library's DEFAULT_UNIVERSE_DOMAIN constant. # This ensures that the .replace() calls in the classes will work correctly. @@ -65,6 +68,7 @@ _IAM_SIGNJWT_ENDPOINT = _IAM_BASE_URL + ":signJwt" _IAM_IDTOKEN_ENDPOINT = _IAM_BASE_URL + ":generateIdToken" + class Signer(crypt.Signer): """Signs messages using the IAM `signBlob API`_. From e2e2b9bf06ad0e18486b3f8c9bc264da66c35cee Mon Sep 17 00:00:00 2001 From: amtk3 Date: Mon, 19 Jan 2026 13:59:48 +1100 Subject: [PATCH 5/8] Support an alternative env to decide if mtls should be enabled --- google/auth/iam.py | 1 + 1 file changed, 1 insertion(+) diff --git a/google/auth/iam.py b/google/auth/iam.py index 22684a9d1..2696efe09 100644 --- a/google/auth/iam.py +++ b/google/auth/iam.py @@ -48,6 +48,7 @@ # if unsupported, fallback to reading from env var use_client_cert = ( os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower() == "true" + or os.getenv("CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE", "false").lower() == "true" ) # 2. Construct the template domain using the library's DEFAULT_UNIVERSE_DOMAIN constant. From f540519b6eacef4e33940fc60f54a8b48d39f731 Mon Sep 17 00:00:00 2001 From: amtk3 Date: Mon, 19 Jan 2026 14:09:01 +1100 Subject: [PATCH 6/8] feat(iam): support an alternative env to decide if mtls should be enabled --- google/auth/iam.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/google/auth/iam.py b/google/auth/iam.py index 2696efe09..2bc73718c 100644 --- a/google/auth/iam.py +++ b/google/auth/iam.py @@ -46,9 +46,12 @@ use_client_cert = mtls.should_use_client_cert() else: # pragma: NO COVER # if unsupported, fallback to reading from env var - use_client_cert = ( - os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower() == "true" - or os.getenv("CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE", "false").lower() == "true" + use_client_cert = any( + os.getenv(var, "false").lower() == "true" + for var in ( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", + "CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE", + ) ) # 2. Construct the template domain using the library's DEFAULT_UNIVERSE_DOMAIN constant. From 6a3bf2617942ad2c1ce566b47aad24f0e9a5be3e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 08:19:19 +0000 Subject: [PATCH 7/8] feat: add fallback env vars for mTLS config Added `CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE` and `CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH` as fallback environment variables for mTLS configuration. Updated `google/auth/transport/_mtls_helper.py` to check these variables if the primary `GOOGLE_API_...` variables are not set. Added tests to verify precedence and fallback logic. --- google/auth/environment_vars.py | 12 ++++ google/auth/transport/_mtls_helper.py | 23 ++++++- tests/transport/test_mtls_env_vars.py | 97 +++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tests/transport/test_mtls_env_vars.py diff --git a/google/auth/environment_vars.py b/google/auth/environment_vars.py index ca041ca16..c7d706467 100644 --- a/google/auth/environment_vars.py +++ b/google/auth/environment_vars.py @@ -113,6 +113,18 @@ """Environment variable defining the location of Google API certificate config file.""" +CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE = ( + "CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE" +) +"""Environment variable controlling whether to use client certificate or not. +This variable is the fallback of GOOGLE_API_USE_CLIENT_CERTIFICATE.""" + +CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH = ( + "CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH" +) +"""Environment variable defining the location of Google API certificate config +file. This variable is the fallback of GOOGLE_API_CERTIFICATE_CONFIG.""" + GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES = ( "GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES" ) diff --git a/google/auth/transport/_mtls_helper.py b/google/auth/transport/_mtls_helper.py index 623b435ee..d5ccbdc4e 100644 --- a/google/auth/transport/_mtls_helper.py +++ b/google/auth/transport/_mtls_helper.py @@ -151,7 +151,14 @@ def _get_cert_config_path(certificate_config_path=None): if env_path is not None and env_path != "": certificate_config_path = env_path else: - certificate_config_path = CERTIFICATE_CONFIGURATION_DEFAULT_PATH + env_path = environ.get( + environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH, + None, + ) + if env_path is not None and env_path != "": + certificate_config_path = env_path + else: + certificate_config_path = CERTIFICATE_CONFIGURATION_DEFAULT_PATH certificate_config_path = path.expanduser(certificate_config_path) if not path.exists(certificate_config_path): @@ -452,13 +459,23 @@ def check_use_client_cert(): Returns: bool: Whether the client certificate should be used for mTLS connection. """ - use_client_cert = getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE") + use_client_cert = getenv(environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE) + if use_client_cert is None: + use_client_cert = getenv( + environment_vars.CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE + ) + # Check if the value of GOOGLE_API_USE_CLIENT_CERTIFICATE is set. if use_client_cert: return use_client_cert.lower() == "true" else: # Check if the value of GOOGLE_API_CERTIFICATE_CONFIG is set. - cert_path = getenv("GOOGLE_API_CERTIFICATE_CONFIG") + cert_path = getenv(environment_vars.GOOGLE_API_CERTIFICATE_CONFIG) + if cert_path is None: + cert_path = getenv( + environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH + ) + if cert_path: try: with open(cert_path, "r") as f: diff --git a/tests/transport/test_mtls_env_vars.py b/tests/transport/test_mtls_env_vars.py new file mode 100644 index 000000000..3f62a3a51 --- /dev/null +++ b/tests/transport/test_mtls_env_vars.py @@ -0,0 +1,97 @@ +# 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 +from unittest import mock +import pytest +from google.auth.transport import _mtls_helper +from google.auth import environment_vars + +class TestEnvVarsPrecedence: + def test_use_client_cert_precedence(self): + # GOOGLE_API_USE_CLIENT_CERTIFICATE takes precedence + with mock.patch.dict(os.environ, { + environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true", + environment_vars.CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE: "false" + }): + assert _mtls_helper.check_use_client_cert() is True + + with mock.patch.dict(os.environ, { + environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "false", + environment_vars.CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE: "true" + }): + assert _mtls_helper.check_use_client_cert() is False + + def test_use_client_cert_fallback(self): + # Fallback to CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE if GOOGLE_API_USE_CLIENT_CERTIFICATE is unset + with mock.patch.dict(os.environ, { + environment_vars.CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE: "true" + }): + # Ensure GOOGLE_API_USE_CLIENT_CERTIFICATE is not set + if environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE in os.environ: + del os.environ[environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE] + assert _mtls_helper.check_use_client_cert() is True + + with mock.patch.dict(os.environ, { + environment_vars.CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE: "false" + }): + if environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE in os.environ: + del os.environ[environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE] + assert _mtls_helper.check_use_client_cert() is False + + def test_cert_config_path_precedence(self): + # GOOGLE_API_CERTIFICATE_CONFIG takes precedence + google_path = "/path/to/google/config" + cloudsdk_path = "/path/to/cloudsdk/config" + + with mock.patch.dict(os.environ, { + environment_vars.GOOGLE_API_CERTIFICATE_CONFIG: google_path, + environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH: cloudsdk_path + }): + with mock.patch("os.path.exists", return_value=True): + assert _mtls_helper._get_cert_config_path() == google_path + + def test_cert_config_path_fallback(self): + # Fallback to CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH if GOOGLE_API_CERTIFICATE_CONFIG is unset + cloudsdk_path = "/path/to/cloudsdk/config" + + with mock.patch.dict(os.environ, { + environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH: cloudsdk_path + }): + if environment_vars.GOOGLE_API_CERTIFICATE_CONFIG in os.environ: + del os.environ[environment_vars.GOOGLE_API_CERTIFICATE_CONFIG] + + with mock.patch("os.path.exists", return_value=True): + assert _mtls_helper._get_cert_config_path() == cloudsdk_path + + @mock.patch("builtins.open", autospec=True) + def test_check_use_client_cert_config_fallback(self, mock_file): + # Test fallback for config file when determining if client cert should be used + cloudsdk_path = "/path/to/cloudsdk/config" + + mock_file.side_effect = mock.mock_open( + read_data='{"cert_configs": {"workload": "exists"}}' + ) + + with mock.patch.dict(os.environ, { + environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH: cloudsdk_path + }): + if environment_vars.GOOGLE_API_CERTIFICATE_CONFIG in os.environ: + del os.environ[environment_vars.GOOGLE_API_CERTIFICATE_CONFIG] + if environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE in os.environ: + del os.environ[environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE] + if environment_vars.CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE in os.environ: + del os.environ[environment_vars.CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE] + + assert _mtls_helper.check_use_client_cert() is True From 90793f19763bf87dbe537b55ffffb018914cf683 Mon Sep 17 00:00:00 2001 From: amtk3 Date: Thu, 22 Jan 2026 19:33:43 +1100 Subject: [PATCH 8/8] Use "_mtls_helper" class --- google/auth/iam.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/google/auth/iam.py b/google/auth/iam.py index 22684a9d1..b9ab0bacc 100644 --- a/google/auth/iam.py +++ b/google/auth/iam.py @@ -22,14 +22,13 @@ import base64 import http.client as http_client import json -import os from google.auth import _exponential_backoff from google.auth import _helpers from google.auth import credentials from google.auth import crypt from google.auth import exceptions -from google.auth.transport import mtls +from google.auth.transport import _mtls_helper IAM_RETRY_CODES = { http_client.INTERNAL_SERVER_ERROR, @@ -40,20 +39,9 @@ _IAM_SCOPE = ["https://www.googleapis.com/auth/iam"] -# 1. Determine if we should use mTLS. -# Note: We only support automatic mTLS on the default googleapis.com universe. -if hasattr(mtls, "should_use_client_cert"): - use_client_cert = mtls.should_use_client_cert() -else: # pragma: NO COVER - # if unsupported, fallback to reading from env var - use_client_cert = ( - os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower() == "true" - ) - -# 2. Construct the template domain using the library's DEFAULT_UNIVERSE_DOMAIN constant. -# This ensures that the .replace() calls in the classes will work correctly. -if use_client_cert: - # We use the .mtls. prefix only for the default universe template +# Determine if we should use mTLS. +if hasattr(_mtls_helper, "check_use_client_cert") and _mtls_helper.check_use_client_cert(): + # Construct the template domain using the library's DEFAULT_UNIVERSE_DOMAIN constant. _IAM_DOMAIN = f"iamcredentials.mtls.{credentials.DEFAULT_UNIVERSE_DOMAIN}" else: _IAM_DOMAIN = f"iamcredentials.{credentials.DEFAULT_UNIVERSE_DOMAIN}"