Repository: googleapis/google-auth-library-python

Issue:
Errors in sign_bytes are not clear
 **Is your feature request related to a problem? Please describe.**
When there is an error in the request made in the function [sign_bytes](https://github.com/googleapis/google-auth-library-python/blob/main/google/auth/impersonated_credentials.py#L274) is not clear what the real error is. It is  just throwing  a `KeyError: 'signedBlob'`:

```
File "/usr/local/lib/python3.9/site-packages/storages/backends/gcloud.py", line 255, in url
return blob.generate_signed_url(self.expiration)
partner-marketing_web_1 |   File "/usr/local/lib/python3.9/site-packages/google/cloud/storage/blob.py", line 622, in generate_signed_url
return helper(
partner-marketing_web_1 |   File "/usr/local/lib/python3.9/site-packages/google/cloud/storage/_signing.py", line 395, in generate_signed_url_v2
signed_query_params = get_signed_query_params_v2(
partner-marketing_web_1 |   File "/usr/local/lib/python3.9/site-packages/google/cloud/storage/_signing.py", line 80, in get_signed_query_params_v2
signature_bytes = credentials.sign_bytes(string_to_sign.encode("ascii"))
partner-marketing_web_1 |   File "/usr/local/lib/python3.9/site-packages/google/auth/impersonated_credentials.py", line 293, in sign_bytes
return base64.b64decode(response.json()["signedBlob"])
KeyError: 'signedBlob'
```
 **Describe the solution you'd like**
It would be great to have a more clear understanding of the real error. In my case the response was returning 
```
{'error': {'code': 403, 'message': 'The caller does not have permission', 'status': 'PERMISSION_DENIED'}}
```

One option is to validate the response (`response.json()`), before returning the `signedBlob`. Similar to https://github.com/googleapis/google-auth-library-python/blob/main/google/auth/iam.py#L79

```
def sign_bytes(self, message):
    ...
    response = authed_session.post(
            url=iam_sign_endpoint, headers=headers, json=body
        )
    body = response.json()
    
    if "error" in body:
        # raise a more appropriate error
        
    return base64.b64decode(body["signedBlob"])

```

Discussion:
busunkim96: ```
self = <tests.test_impersonated_credentials.TestImpersonatedCredentials object at 0x7f3ac1d41710>

    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("{'error': {'code': 403, 'message': 'unauthorized'}")
E           AssertionError: Pattern "{'error': {'code': 403, 'message': 'unauthorized'}" not found in "Error calling sign_bytes: {'error': {'message': 'unauthorized', 'code': 403}}"

```

The order seems to change sometimes - I think loading the JSON into a dict would make the comparision stable.
arithmetic1728: > ```
> self = <tests.test_impersonated_credentials.TestImpersonatedCredentials object at 0x7f3ac1d41710>
> 
>     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("{'error': {'code': 403, 'message': 'unauthorized'}")
> E           AssertionError: Pattern "{'error': {'code': 403, 'message': 'unauthorized'}" not found in "Error calling sign_bytes: {'error': {'message': 'unauthorized', 'code': 403}}"
> ```
> 
> The order seems to change sometimes - I think loading the JSON into a dict would make the comparision stable.

I updated the test to just match `'code': 403` instead of the whole `"{'error': {'code': 403, 'message': 'unauthorized'}")` string so I don't need to do the JSON convertion.

I also noticed that python nox cover session always includes the `google-auth` bundled in `/google-cloud-sdk/lib/third_party/` so I also updated the `coveragerc` to ignore that as well.

Patch:
diff --git a/.coveragerc b/.coveragerc
index 494c03f..9ba3d3f 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -5,6 +5,7 @@ branch = True
 omit =
     */samples/*
     */conftest.py
+    */google-cloud-sdk/lib/*
 exclude_lines =
     # Re-enable the standard pragma
     pragma: NO COVER
diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py
index b8a6c49..80d6fdf 100644
--- a/google/auth/impersonated_credentials.py
+++ b/google/auth/impersonated_credentials.py
@@ -290,6 +290,11 @@ class Credentials(credentials.CredentialsWithQuotaProject, credentials.Signing):
             url=iam_sign_endpoint, headers=headers, json=body
         )
 
+        if response.status_code != http_client.OK:
+            raise exceptions.TransportError(
+                "Error calling sign_bytes: {}".format(response.json())
+            )
+
         return base64.b64decode(response.json()["signedBlob"])
 
     @property


Test patch:
diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py
index bceaeba..bc404e3 100644
--- a/tests/test_impersonated_credentials.py
+++ b/tests/test_impersonated_credentials.py
@@ -345,6 +345,19 @@ class TestImpersonatedCredentials(object):
         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()