From 47590a56d9ef4ca4cc023a3fb6db31cb0751f9b2 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 12 Mar 2026 13:47:16 -0700 Subject: [PATCH 1/5] remove requirement on requests library --- packages/google-auth/google/auth/_default.py | 13 +- .../google/auth/compute_engine/_metadata.py | 9 +- .../google/auth/compute_engine/_mtls.py | 112 ++++++++++-------- 3 files changed, 78 insertions(+), 56 deletions(-) diff --git a/packages/google-auth/google/auth/_default.py b/packages/google-auth/google/auth/_default.py index d3beab4b6cd7..70f641ae56a9 100644 --- a/packages/google-auth/google/auth/_default.py +++ b/packages/google-auth/google/auth/_default.py @@ -391,9 +391,10 @@ def _get_gae_credentials(): def _get_gce_credentials(request=None, quota_project_id=None): """Gets credentials and project ID from the GCE Metadata Service.""" # Ping requires a transport, but we want application default credentials - # to require no arguments. So, we'll use the _http_client transport which - # uses http.client. This is only acceptable because the metadata server - # doesn't do SSL and never requires proxies. + # to require no arguments. + # We'll use the requests transport if present, otherwise we'll fall back to + #_http_client transport. If TLS is enabled, http_client will fail to communicate + # with the metadata server. That case will be handled by compute_engine._mtls. # While this library is normally bundled with compute_engine, there are # some cases where it's not available, so we tolerate ImportError. @@ -405,7 +406,11 @@ def _get_gce_credentials(request=None, quota_project_id=None): return None, None if request is None: - request = google.auth.transport._http_client.Request() + try: + # use requests transport if available for TLS support + request = google.auth.transport.requests.Request() + except ImportError: + request = google.auth.transport._http_client.Request() if _metadata.is_on_gce(request=request): # Get the project ID. diff --git a/packages/google-auth/google/auth/compute_engine/_metadata.py b/packages/google-auth/google/auth/compute_engine/_metadata.py index aae724ab18ee..59dff8b00d2f 100644 --- a/packages/google-auth/google/auth/compute_engine/_metadata.py +++ b/packages/google-auth/google/auth/compute_engine/_metadata.py @@ -24,7 +24,10 @@ import os from urllib.parse import urljoin -import requests +try: + import requests as requests_lib +except ImportError: # pragma: NO COVER + requests_lib = None from google.auth import _helpers from google.auth import environment_vars @@ -174,10 +177,10 @@ def _prepare_request_for_mds(request, use_mtls=False) -> None: """ # Only modify the request if mTLS is enabled, and request supports sessions. - if use_mtls and hasattr(request, "session"): + if use_mtls and request_lib and hasattr(request, "session"): # Ensure the request has a session to mount the adapter to. if not request.session: - request.session = requests.Session() + request.session = requests_lib.Session() adapter = _mtls.MdsMtlsAdapter() # Mount the adapter for all default GCE metadata hosts. diff --git a/packages/google-auth/google/auth/compute_engine/_mtls.py b/packages/google-auth/google/auth/compute_engine/_mtls.py index 6525dd03e1bd..62ab5d7e3be0 100644 --- a/packages/google-auth/google/auth/compute_engine/_mtls.py +++ b/packages/google-auth/google/auth/compute_engine/_mtls.py @@ -24,8 +24,13 @@ import ssl from urllib.parse import urlparse, urlunparse -import requests -from requests.adapters import HTTPAdapter +try: + import requests + from requests.adapters import HTTPAdapter + + _HAS_REQUESTS = True +except ImportError: # pragma: NO COVER + _HAS_REQUESTS = False from google.auth import environment_vars, exceptions @@ -102,6 +107,10 @@ def should_use_mds_mtls(mds_mtls_config: MdsMtlsConfig = MdsMtlsConfig()): """Determines if mTLS should be used for the metadata server.""" mode = _parse_mds_mode() if mode == MdsMtlsMode.STRICT: + if not _HAS_REQUESTS: + raise exceptions.MutualTLSChannelError( + "The requests library is required for mTLS. Install it with `google-auth[requests]`" + ) if not _certs_exist(mds_mtls_config): raise exceptions.MutualTLSChannelError( "mTLS certificates not found in strict mode." @@ -110,55 +119,60 @@ def should_use_mds_mtls(mds_mtls_config: MdsMtlsConfig = MdsMtlsConfig()): elif mode == MdsMtlsMode.NONE: return False else: # Default mode + if not _HAS_REQUESTS: + return False return _certs_exist(mds_mtls_config) -class MdsMtlsAdapter(HTTPAdapter): - """An HTTP adapter that uses mTLS for the metadata server.""" +if _HAS_REQUESTS: - def __init__( - self, mds_mtls_config: MdsMtlsConfig = MdsMtlsConfig(), *args, **kwargs - ): - self.ssl_context = ssl.create_default_context() - self.ssl_context.load_verify_locations(cafile=mds_mtls_config.ca_cert_path) - self.ssl_context.load_cert_chain( - certfile=mds_mtls_config.client_combined_cert_path - ) - super(MdsMtlsAdapter, self).__init__(*args, **kwargs) - - def init_poolmanager(self, *args, **kwargs): - kwargs["ssl_context"] = self.ssl_context - return super(MdsMtlsAdapter, self).init_poolmanager(*args, **kwargs) - - def proxy_manager_for(self, *args, **kwargs): - kwargs["ssl_context"] = self.ssl_context - return super(MdsMtlsAdapter, self).proxy_manager_for(*args, **kwargs) - - def send(self, request, **kwargs): - # If we are in strict mode, always use mTLS (no HTTP fallback) - if _parse_mds_mode() == MdsMtlsMode.STRICT: - return super(MdsMtlsAdapter, self).send(request, **kwargs) - - # In default mode, attempt mTLS first, then fallback to HTTP on failure - try: - response = super(MdsMtlsAdapter, self).send(request, **kwargs) - response.raise_for_status() - return response - except ( - ssl.SSLError, - requests.exceptions.SSLError, - requests.exceptions.HTTPError, - ) as e: - _LOGGER.warning( - "mTLS connection to Compute Engine Metadata server failed. " - "Falling back to standard HTTP. Reason: %s", - e, + class MdsMtlsAdapter(HTTPAdapter): + """An HTTP adapter that uses mTLS for the metadata server.""" + + def __init__( + self, mds_mtls_config: MdsMtlsConfig = MdsMtlsConfig(), *args, **kwargs + ): + self.ssl_context = ssl.create_default_context() + self.ssl_context.load_verify_locations(cafile=mds_mtls_config.ca_cert_path) + self.ssl_context.load_cert_chain( + certfile=mds_mtls_config.client_combined_cert_path ) - # Fallback to standard HTTP - parsed_original_url = urlparse(request.url) - http_fallback_url = urlunparse(parsed_original_url._replace(scheme="http")) - request.url = http_fallback_url - - # Use a standard HTTPAdapter for the fallback - http_adapter = HTTPAdapter() - return http_adapter.send(request, **kwargs) + super(MdsMtlsAdapter, self).__init__(*args, **kwargs) + + def init_poolmanager(self, *args, **kwargs): + kwargs["ssl_context"] = self.ssl_context + return super(MdsMtlsAdapter, self).init_poolmanager(*args, **kwargs) + + def proxy_manager_for(self, *args, **kwargs): + kwargs["ssl_context"] = self.ssl_context + return super(MdsMtlsAdapter, self).proxy_manager_for(*args, **kwargs) + + def send(self, request, **kwargs): + # If we are in strict mode, always use mTLS (no HTTP fallback) + if _parse_mds_mode() == MdsMtlsMode.STRICT: + return super(MdsMtlsAdapter, self).send(request, **kwargs) + + # In default mode, attempt mTLS first, then fallback to HTTP on failure + try: + response = super(MdsMtlsAdapter, self).send(request, **kwargs) + response.raise_for_status() + return response + except ( + ssl.SSLError, + requests.exceptions.SSLError, + requests.exceptions.HTTPError, + ) as e: + _LOGGER.warning( + "mTLS connection to Compute Engine Metadata server failed. " + "Falling back to standard HTTP. Reason: %s", + e, + ) + # Fallback to standard HTTP + parsed_original_url = urlparse(request.url) + http_fallback_url = urlunparse(parsed_original_url._replace(scheme="http")) + request.url = http_fallback_url + + # Use a standard HTTPAdapter for the fallback + http_adapter = HTTPAdapter() + return http_adapter.send(request, **kwargs) + From 82a4250a0c721447f2695a5d180a660d39d8a73f Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 12 Mar 2026 14:25:07 -0700 Subject: [PATCH 2/5] added tests --- packages/google-auth/noxfile.py | 19 +++++++++++----- packages/google-auth/setup.py | 14 ++++++++---- .../tests/compute_engine/test__metadata.py | 22 +++++++++++++++++++ .../tests/compute_engine/test__mtls.py | 21 ++++++++++++++++++ 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/packages/google-auth/noxfile.py b/packages/google-auth/noxfile.py index ca829fa96030..ccd2db604fe2 100644 --- a/packages/google-auth/noxfile.py +++ b/packages/google-auth/noxfile.py @@ -115,14 +115,14 @@ def mypy(session): @nox.session(python=ALL_PYTHON) -@nox.parametrize(["install_deprecated_extras"], (True, False)) -def unit(session, install_deprecated_extras): +@nox.parametrize(["install_extras"], (True, False)) +def unit(session, install_extras): # Install all test dependencies, then install this package in-place. if session.python in ("3.7",): session.skip("Python 3.7 is no longer supported") min_py, max_py = UNIT_TEST_PYTHON_VERSIONS[0], UNIT_TEST_PYTHON_VERSIONS[-1] - if not install_deprecated_extras and session.python not in (min_py, max_py): + if not install_extras and session.python not in (min_py, max_py): # only run double tests on first and last supported versions session.skip( f"Extended tests only run on boundary Python versions ({min_py}, {max_py}) to reduce CI load." @@ -131,13 +131,22 @@ def unit(session, install_deprecated_extras): constraints_path = str( CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" ) - extras_str = "testing" - if install_deprecated_extras: + + if install_extras: + extras_str = "testing" # rsa and oauth2client were both archived and support dropped, # but we still test old code paths session.install("oauth2client") extras_str += ",rsa" + else: + # Install testing dependencies explicitly without requests + extras_str = "testing_minimal" + session.install("-e", f".[{extras_str}]", "-c", constraints_path) + + if not install_extras: + # Verify requests is actually not installed + session.run("python", "-c", "import requests", success_codes=[1]) session.run( "pytest", f"--junitxml=unit_{session.python}_sponge_log.xml", diff --git a/packages/google-auth/setup.py b/packages/google-auth/setup.py index 8a66b7c39d0e..7b803fb99173 100644 --- a/packages/google-auth/setup.py +++ b/packages/google-auth/setup.py @@ -46,7 +46,7 @@ rsa_extra_require = ["rsa>=3.1.4,<5"] # Unit test requirements. -testing_extra_require = [ +testing_minimal_require = [ # TODO(https://github.com/googleapis/google-auth-library-python/issues/1735): Remove `grpcio` from testing requirements once an extra is added for `grpcio` dependency. "grpcio", "flask", @@ -59,14 +59,19 @@ *reauth_extra_require, "responses", *urllib3_extra_require, - # Async Dependencies - *aiohttp_extra_require, - "aioresponses", "pytest-asyncio", # TODO(https://github.com/googleapis/google-auth-library-python/issues/1665): Remove the pinned version of pyopenssl # once `TestDecryptPrivateKey::test_success` is updated to remove the deprecated `OpenSSL.crypto.sign` and # `OpenSSL.crypto.verify` methods. See: https://www.pyopenssl.org/en/latest/changelog.html#id3. "pyopenssl < 24.3.0", +] + +# Testing with requests/aiohttp +testing_extra_require = [ + *testing_minimal_require, + *requests_extra_require, + *aiohttp_extra_require, + "aioresponses", # TODO(https://github.com/googleapis/google-auth-library-python/issues/1722): `test_aiohttp_requests` depend on # aiohttp < 3.10.0 which is a bug. Investigate and remove the pinned aiohttp version. "aiohttp < 3.10.0", @@ -82,6 +87,7 @@ "reauth": reauth_extra_require, "requests": requests_extra_require, "testing": testing_extra_require, + "testing_minimal": testing_minimal_require, "urllib3": urllib3_extra_require, "rsa": rsa_extra_require, # TODO(https://github.com/googleapis/google-auth-library-python/issues/1735): Add an extra for `grpcio` dependency. diff --git a/packages/google-auth/tests/compute_engine/test__metadata.py b/packages/google-auth/tests/compute_engine/test__metadata.py index 35996ab24b92..c8eb9e128ea5 100644 --- a/packages/google-auth/tests/compute_engine/test__metadata.py +++ b/packages/google-auth/tests/compute_engine/test__metadata.py @@ -969,3 +969,25 @@ def test__prepare_request_for_mds_mtls_http_request(mock_mds_mtls_adapter): _metadata._prepare_request_for_mds(request, use_mtls=True) assert mock_mds_mtls_adapter.call_count == 0 + +@mock.patch("google.auth.compute_engine._mtls._HAS_REQUESTS", False) +@mock.patch("google.auth.compute_engine._mtls._parse_mds_mode") +def test_get_without_requests_default(mock_parse_mds_mode): + mock_parse_mds_mode.return_value = _metadata._mtls.MdsMtlsMode.DEFAULT + request = make_request("foobar", headers={"content-type": "text/plain"}) + + result = _metadata.get(request, PATH) + assert result == "foobar" + # Ensure no session is created since requests is missing + assert not hasattr(request, "session") + + +@mock.patch("google.auth.compute_engine._mtls._HAS_REQUESTS", False) +@mock.patch("google.auth.compute_engine._mtls._parse_mds_mode") +def test_get_without_requests_strict(mock_parse_mds_mode): + mock_parse_mds_mode.return_value = _metadata._mtls.MdsMtlsMode.STRICT + request = make_request("foobar", headers={"content-type": "text/plain"}) + + with pytest.raises(exceptions.MutualTLSChannelError, match="requests library"): + _metadata.get(request, PATH) + diff --git a/packages/google-auth/tests/compute_engine/test__mtls.py b/packages/google-auth/tests/compute_engine/test__mtls.py index 6b40b6682869..f09052a5d18c 100644 --- a/packages/google-auth/tests/compute_engine/test__mtls.py +++ b/packages/google-auth/tests/compute_engine/test__mtls.py @@ -286,3 +286,24 @@ def test_mds_mtls_adapter_send_no_fallback_strict_mode( request = requests.Request(method="GET", url="https://fake-mds.com").prepare() with pytest.raises(requests.exceptions.SSLError): adapter.send(request) + +@mock.patch("google.auth.compute_engine._mtls._HAS_REQUESTS", False) +@mock.patch("google.auth.compute_engine._mtls._parse_mds_mode") +def test_should_use_mds_mtls_without_requests_strict(mock_parse_mds_mode): + mock_parse_mds_mode.return_value = _mtls.MdsMtlsMode.STRICT + with pytest.raises(exceptions.MutualTLSChannelError, match="requests library"): + _mtls.should_use_mds_mtls() + + +@mock.patch("google.auth.compute_engine._mtls._HAS_REQUESTS", False) +@mock.patch("google.auth.compute_engine._mtls._parse_mds_mode") +def test_should_use_mds_mtls_without_requests_default(mock_parse_mds_mode): + mock_parse_mds_mode.return_value = _mtls.MdsMtlsMode.DEFAULT + assert _mtls.should_use_mds_mtls() is False + + +@mock.patch("google.auth.compute_engine._mtls._HAS_REQUESTS", False) +@mock.patch("google.auth.compute_engine._mtls._parse_mds_mode") +def test_should_use_mds_mtls_without_requests_none(mock_parse_mds_mode): + mock_parse_mds_mode.return_value = _mtls.MdsMtlsMode.NONE + assert _mtls.should_use_mds_mtls() is False From e249699adb44ff3a19a4ee57831a5cb67c13c0dc Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 12 Mar 2026 14:37:10 -0700 Subject: [PATCH 3/5] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/google-auth/google/auth/compute_engine/_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-auth/google/auth/compute_engine/_metadata.py b/packages/google-auth/google/auth/compute_engine/_metadata.py index 59dff8b00d2f..517c7997ed9d 100644 --- a/packages/google-auth/google/auth/compute_engine/_metadata.py +++ b/packages/google-auth/google/auth/compute_engine/_metadata.py @@ -27,7 +27,7 @@ try: import requests as requests_lib except ImportError: # pragma: NO COVER - requests_lib = None + requests_lib = None # type: ignore[assignment] from google.auth import _helpers from google.auth import environment_vars From 7174f2c77d9a853218bc02be1e892db2d658f546 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 12 Mar 2026 15:28:30 -0700 Subject: [PATCH 4/5] fixed variable name --- packages/google-auth/google/auth/compute_engine/_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-auth/google/auth/compute_engine/_metadata.py b/packages/google-auth/google/auth/compute_engine/_metadata.py index 517c7997ed9d..4dffa467afbe 100644 --- a/packages/google-auth/google/auth/compute_engine/_metadata.py +++ b/packages/google-auth/google/auth/compute_engine/_metadata.py @@ -177,7 +177,7 @@ def _prepare_request_for_mds(request, use_mtls=False) -> None: """ # Only modify the request if mTLS is enabled, and request supports sessions. - if use_mtls and request_lib and hasattr(request, "session"): + if use_mtls and requests_lib and hasattr(request, "session"): # Ensure the request has a session to mount the adapter to. if not request.session: request.session = requests_lib.Session() From 438f5583d8cbf4a497fbfd31d182e3ef65b2136d Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 12 Mar 2026 15:30:49 -0700 Subject: [PATCH 5/5] ran blacken --- packages/google-auth/google/auth/_default.py | 4 ++-- packages/google-auth/google/auth/compute_engine/_mtls.py | 5 +++-- packages/google-auth/noxfile.py | 2 +- packages/google-auth/tests/compute_engine/test__metadata.py | 6 +++--- packages/google-auth/tests/compute_engine/test__mtls.py | 1 + 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/google-auth/google/auth/_default.py b/packages/google-auth/google/auth/_default.py index 70f641ae56a9..84125d80cbcd 100644 --- a/packages/google-auth/google/auth/_default.py +++ b/packages/google-auth/google/auth/_default.py @@ -391,9 +391,9 @@ def _get_gae_credentials(): def _get_gce_credentials(request=None, quota_project_id=None): """Gets credentials and project ID from the GCE Metadata Service.""" # Ping requires a transport, but we want application default credentials - # to require no arguments. + # to require no arguments. # We'll use the requests transport if present, otherwise we'll fall back to - #_http_client transport. If TLS is enabled, http_client will fail to communicate + # _http_client transport. If TLS is enabled, http_client will fail to communicate # with the metadata server. That case will be handled by compute_engine._mtls. # While this library is normally bundled with compute_engine, there are diff --git a/packages/google-auth/google/auth/compute_engine/_mtls.py b/packages/google-auth/google/auth/compute_engine/_mtls.py index 62ab5d7e3be0..55144b0881d9 100644 --- a/packages/google-auth/google/auth/compute_engine/_mtls.py +++ b/packages/google-auth/google/auth/compute_engine/_mtls.py @@ -169,10 +169,11 @@ def send(self, request, **kwargs): ) # Fallback to standard HTTP parsed_original_url = urlparse(request.url) - http_fallback_url = urlunparse(parsed_original_url._replace(scheme="http")) + http_fallback_url = urlunparse( + parsed_original_url._replace(scheme="http") + ) request.url = http_fallback_url # Use a standard HTTPAdapter for the fallback http_adapter = HTTPAdapter() return http_adapter.send(request, **kwargs) - diff --git a/packages/google-auth/noxfile.py b/packages/google-auth/noxfile.py index ccd2db604fe2..2a1a840ac27b 100644 --- a/packages/google-auth/noxfile.py +++ b/packages/google-auth/noxfile.py @@ -143,7 +143,7 @@ def unit(session, install_extras): extras_str = "testing_minimal" session.install("-e", f".[{extras_str}]", "-c", constraints_path) - + if not install_extras: # Verify requests is actually not installed session.run("python", "-c", "import requests", success_codes=[1]) diff --git a/packages/google-auth/tests/compute_engine/test__metadata.py b/packages/google-auth/tests/compute_engine/test__metadata.py index c8eb9e128ea5..7626fc53903b 100644 --- a/packages/google-auth/tests/compute_engine/test__metadata.py +++ b/packages/google-auth/tests/compute_engine/test__metadata.py @@ -970,12 +970,13 @@ def test__prepare_request_for_mds_mtls_http_request(mock_mds_mtls_adapter): assert mock_mds_mtls_adapter.call_count == 0 + @mock.patch("google.auth.compute_engine._mtls._HAS_REQUESTS", False) @mock.patch("google.auth.compute_engine._mtls._parse_mds_mode") def test_get_without_requests_default(mock_parse_mds_mode): mock_parse_mds_mode.return_value = _metadata._mtls.MdsMtlsMode.DEFAULT request = make_request("foobar", headers={"content-type": "text/plain"}) - + result = _metadata.get(request, PATH) assert result == "foobar" # Ensure no session is created since requests is missing @@ -987,7 +988,6 @@ def test_get_without_requests_default(mock_parse_mds_mode): def test_get_without_requests_strict(mock_parse_mds_mode): mock_parse_mds_mode.return_value = _metadata._mtls.MdsMtlsMode.STRICT request = make_request("foobar", headers={"content-type": "text/plain"}) - + with pytest.raises(exceptions.MutualTLSChannelError, match="requests library"): _metadata.get(request, PATH) - diff --git a/packages/google-auth/tests/compute_engine/test__mtls.py b/packages/google-auth/tests/compute_engine/test__mtls.py index f09052a5d18c..d33cc9719dcf 100644 --- a/packages/google-auth/tests/compute_engine/test__mtls.py +++ b/packages/google-auth/tests/compute_engine/test__mtls.py @@ -287,6 +287,7 @@ def test_mds_mtls_adapter_send_no_fallback_strict_mode( with pytest.raises(requests.exceptions.SSLError): adapter.send(request) + @mock.patch("google.auth.compute_engine._mtls._HAS_REQUESTS", False) @mock.patch("google.auth.compute_engine._mtls._parse_mds_mode") def test_should_use_mds_mtls_without_requests_strict(mock_parse_mds_mode):