Skip to content

Commit 57269d5

Browse files
fix(auth): configure mTLS for impersonated credentials (#17404)
### Description This PR configures `AuthorizedSession` to support mutual TLS (mTLS) when refreshing impersonated ID tokens or signing bytes. ### Context When using impersonated credentials (e.g., via `gcloud auth print-identity-token --impersonate-service-account=...`) in environments where mTLS is enforced by Context Aware Access (CAA) policies, the requests fail with `401 UNAUTHENTICATED` (specifically `ACCESS_TOKEN_TYPE_UNSUPPORTED`). Although the endpoint correctly resolves to the mTLS domain (`iamcredentials.mtls.googleapis.com`), the underlying `AuthorizedSession` created in `impersonated_credentials.py` is never configured with the client certificate, causing the TLS handshake to lack the required client cert. ### Changes * **`google/auth/impersonated_credentials.py`**: * Added `authed_session.configure_mtls_channel()` in `Credentials.sign_bytes` right after the session is created. * Added `authed_session.configure_mtls_channel()` in `IDTokenCredentials.refresh` right after the session is created. * **`tests/test_impersonated_credentials.py`**: * Added `test_sign_bytes_configures_mtls` and `test_id_token_refresh_configures_mtls` unit tests to verify `configure_mtls_channel` is invoked. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: amtk3 <254821816+amtk3@users.noreply.github.com>
1 parent 59fe7cf commit 57269d5

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

packages/google-auth/google/auth/impersonated_credentials.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ def sign_bytes(self, message):
388388
headers = {"Content-Type": "application/json"}
389389

390390
authed_session = AuthorizedSession(self._source_credentials)
391+
authed_session.configure_mtls_channel()
391392

392393
try:
393394
retries = _exponential_backoff.ExponentialBackoff()
@@ -627,6 +628,7 @@ def refresh(self, request):
627628
authed_session = AuthorizedSession(
628629
self._target_credentials._source_credentials, auth_request=request
629630
)
631+
authed_session.configure_mtls_channel()
630632

631633
try:
632634
response = authed_session.post(

packages/google-auth/tests/test_impersonated_credentials.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,26 @@ def _sign_bytes_helper(
639639

640640
assert signature == b"signature"
641641

642+
@mock.patch(
643+
"google.auth.transport.requests.AuthorizedSession.configure_mtls_channel",
644+
autospec=True,
645+
)
646+
def test_sign_bytes_configures_mtls(
647+
self, mock_configure_mtls, mock_donor_credentials, mock_authorizedsession_sign
648+
):
649+
credentials = self.make_credentials(lifetime=None)
650+
# Refresh is needed to make credentials valid before signing
651+
request = self.make_request(
652+
data=json.dumps(
653+
{"accessToken": "token", "expireTime": "2026-06-09T00:00:00Z"}
654+
),
655+
status=http_client.OK,
656+
)
657+
credentials.refresh(request)
658+
659+
credentials.sign_bytes(b"signed bytes")
660+
mock_configure_mtls.assert_called_once()
661+
642662
def test_sign_bytes_failure(self):
643663
credentials = self.make_credentials(lifetime=None)
644664

@@ -751,6 +771,29 @@ def test_with_scopes_provide_default_scopes(self):
751771
)
752772
assert credentials._target_scopes == ["fake_scope1"]
753773

774+
@mock.patch(
775+
"google.auth.transport.requests.AuthorizedSession.configure_mtls_channel",
776+
autospec=True,
777+
)
778+
def test_id_token_refresh_configures_mtls(
779+
self, mock_configure_mtls, mock_donor_credentials
780+
):
781+
credentials = self.make_credentials(lifetime=None)
782+
credentials.token = "token"
783+
id_creds = impersonated_credentials.IDTokenCredentials(
784+
credentials, target_audience="https://foo.bar"
785+
)
786+
787+
with mock.patch(
788+
"google.auth.transport.requests.AuthorizedSession.post", autospec=True
789+
) as mock_post:
790+
mock_post.return_value = MockResponse(
791+
{"token": ID_TOKEN_DATA}, http_client.OK
792+
)
793+
id_creds.refresh(None)
794+
795+
mock_configure_mtls.assert_called_once()
796+
754797
def test_id_token_success(
755798
self, mock_donor_credentials, mock_authorizedsession_idtoken
756799
):

0 commit comments

Comments
 (0)