diff --git a/google/auth/iam.py b/google/auth/iam.py
index a438726..0ab5b55 100644
--- a/google/auth/iam.py
+++ b/google/auth/iam.py
@@ -70,7 +70,9 @@ class Signer(crypt.Signer):
         method = "POST"
         url = _SIGN_BLOB_URI.format(self._service_account_email)
         headers = {}
-        body = json.dumps({"bytesToSign": base64.b64encode(message).decode("utf-8")})
+        body = json.dumps(
+            {"bytesToSign": base64.b64encode(message).decode("utf-8")}
+        ).encode("utf-8")
 
         self._credentials.before_request(self._request, method, url, headers)
         response = self._request(url=url, method=method, body=body, headers=headers)
diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py
index 70fa5dc..bc7031e 100644
--- a/google/auth/impersonated_credentials.py
+++ b/google/auth/impersonated_credentials.py
@@ -84,7 +84,7 @@ def _make_iam_token_request(request, principal, headers, body):
     """
     iam_endpoint = _IAM_ENDPOINT.format(principal)
 
-    body = json.dumps(body)
+    body = json.dumps(body).encode("utf-8")
 
     response = request(url=iam_endpoint, method="POST", headers=headers, body=body)
 
diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py
index 4cf7a7f..4ba31a8 100644
--- a/google/oauth2/_client.py
+++ b/google/oauth2/_client.py
@@ -95,7 +95,7 @@ def _token_endpoint_request(request, token_uri, body):
         google.auth.exceptions.RefreshError: If the token endpoint returned
             an error.
     """
-    body = urllib.parse.urlencode(body)
+    body = urllib.parse.urlencode(body).encode("utf-8")
     headers = {"content-type": _URLENCODED_CONTENT_TYPE}
 
     retry = 0

google.auth.transport.Request is called with type(body) == str
According to eg https://google-auth.readthedocs.io/en/latest/reference/google.auth.html#google.auth.default, classes implementing google.auth.transport.Request will be called with a *body* parameter of type bytes. This makes sense, because the request agent is in no position to decide how a str body should be encoded for transport.

Unfortunately, in some cases the googl.auth module does pass str value. One such call is made in https://github.com/googleapis/google-auth-library-python/blob/137b43ba002d422c68e7bfacdafa54986f38500b/google/oauth2/_client.py#L100, but there may be more.
diff --git a/tests/oauth2/test__client.py b/tests/oauth2/test__client.py
index 9cf59eb..052390a 100644
--- a/tests/oauth2/test__client.py
+++ b/tests/oauth2/test__client.py
@@ -96,7 +96,7 @@ def test__token_endpoint_request():
         method="POST",
         url="http://example.com",
         headers={"content-type": "application/x-www-form-urlencoded"},
-        body="test=params",
+        body="test=params".encode("utf-8"),
     )
 
     # Check result
@@ -131,7 +131,7 @@ def test__token_endpoint_request_internal_failure_error():
 
 
 def verify_request_params(request, params):
-    request_body = request.call_args[1]["body"]
+    request_body = request.call_args[1]["body"].decode("utf-8")
     request_params = urllib.parse.parse_qs(request_body)
 
     for key, value in six.iteritems(params):

pip install -e .[dev]
parse_log_pytest
requirements.txt
flask freezegun mock oauth2client pytest pytest-cov pytest-localserver requests urllib3 cryptography grpcio
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
blinker==1.9.0
cachetools==4.2.4
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
coverage==7.8.0
cryptography==44.0.2
exceptiongroup==1.2.2
Flask==3.1.0
freezegun==1.5.1
-e git+https://github.com/googleapis/google-auth-library-python.git@af29c1a9fd9282b38867961e4053f74f018a3815#egg=google_auth
grpcio==1.71.0
httplib2==0.22.0
idna==3.10
importlib_metadata==8.6.1
iniconfig==2.1.0
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
mock==5.2.0
oauth2client==4.1.3
packaging==24.2
pluggy==1.5.0
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycparser==2.22
pyparsing==3.2.3
pytest==8.3.5
pytest-cov==6.0.0
pytest-localserver==0.9.0.post0
python-dateutil==2.9.0.post0
requests==2.32.3
rsa==4.0
six==1.17.0
tomli==2.2.1
urllib3==2.3.0
Werkzeug==3.1.3
zipp==3.21.0

name: google-auth-library-python
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - blinker==1.9.0
      - cachetools==4.2.4
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - click==8.1.8
      - coverage==7.8.0
      - cryptography==44.0.2
      - exceptiongroup==1.2.2
      - flask==3.1.0
      - freezegun==1.5.1
      - grpcio==1.71.0
      - httplib2==0.22.0
      - idna==3.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - itsdangerous==2.2.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - mock==5.2.0
      - oauth2client==4.1.3
      - packaging==24.2
      - pluggy==1.5.0
      - pyasn1==0.6.1
      - pyasn1-modules==0.4.2
      - pycparser==2.22
      - pyparsing==3.2.3
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pytest-localserver==0.9.0.post0
      - python-dateutil==2.9.0.post0
      - requests==2.32.3
      - rsa==4.0
      - six==1.17.0
      - tomli==2.2.1
      - urllib3==2.3.0
      - werkzeug==3.1.3
      - zipp==3.21.0
prefix: /opt/conda/envs/google-auth-library-python

tests/oauth2/test__client.py::test__token_endpoint_request tests/oauth2/test__client.py::test_jwt_grant tests/oauth2/test__client.py::test_id_token_jwt_grant tests/oauth2/test__client.py::test_refresh_grant tests/oauth2/test__client.py::test_refresh_grant_with_scopes
tests/oauth2/test__client.py::test__handle_error_response tests/oauth2/test__client.py::test__handle_error_response_non_json tests/oauth2/test__client.py::test__parse_expiry tests/oauth2/test__client.py::test__parse_expiry_none tests/oauth2/test__client.py::test__token_endpoint_request_error tests/oauth2/test__client.py::test__token_endpoint_request_internal_failure_error tests/oauth2/test__client.py::test_jwt_grant_no_access_token tests/oauth2/test__client.py::test_id_token_jwt_grant_no_access_token tests/oauth2/test__client.py::test_refresh_grant_no_access_token
google/auth/impersonated_credentials.py google/auth/iam.py google/oauth2/_client.py
google/auth/impersonated_credentials.py google/auth/iam.py google/oauth2/_client.py