Repository: googleapis/google-auth-library-python

Issue:
Password prompt opened by `google.auth.default()` when credentials have expired
When you have a reauthentication policy set in Google Cloud and your crendentials have expired, calling `google.auth.default()` will open a `Please enter your password:` password prompt, instead if raising a `DefaultCredentialsError` as I believe would be expected.

The password prompt interrupts the flow of the program, with no way to catch or handle it actively.

If credentials are _revoked_ (instead of expired), you don't get the password prompt, and instead get an exception as expected.

For example, when creating a `bigquery.Client`, its `__init__` calls `super(Client, self).__init__()` which in turn calls `google.auth.default()`, which in turn [calls the external program `gcloud config config-helper --format json` in a `subprocess`](https://github.com/googleapis/google-auth-library-python/blob/main/google/auth/_cloud_sdk.py#L108), which finally opens the prompt.

Full traceback after starting it with `python -m pdb ...` and hitting Ctrl-C from when the prompt is open:

```
Traceback (most recent call last):
  ".../code_under_test.py", line 90, in _bqclient
    client = bigquery.Client(
  File ".../.venv/lib/python3.10/site-packages/google/cloud/bigquery/client.py", line 244, in __init__
    super(Client, self).__init__(
  File ".../.venv/lib/python3.10/site-packages/google/cloud/client/__init__.py", line 321, in __init__
    Client.__init__(
  File ".../.venv/lib/python3.10/site-packages/google/cloud/client/__init__.py", line 178, in __init__
    credentials, _ = google.auth.default(scopes=scopes)
  File ".../.venv/lib/python3.10/site-packages/google/auth/_default.py", line 620, in default
    credentials, project_id = checker()
  File ".../.venv/lib/python3.10/site-packages/google/auth/_default.py", line 614, in <lambda>
    lambda: _get_gcloud_sdk_credentials(quota_project_id=quota_project_id),
  File ".../.venv/lib/python3.10/site-packages/google/auth/_default.py", line 205, in _get_gcloud_sdk_credentials
    project_id = _cloud_sdk.get_project_id()
  File ".../.venv/lib/python3.10/site-packages/google/auth/_cloud_sdk.py", line 108, in get_project_id
    output = _run_subprocess_ignore_stderr((command,) + _CLOUD_SDK_CONFIG_COMMAND)
  File ".../.venv/lib/python3.10/site-packages/google/auth/_cloud_sdk.py", line 90, in _run_subprocess_ignore_stderr
    output = subprocess.check_output(command, stderr=devnull)
  ...
```

#### Environment details

  - OS: macOS 13.2.1
  - Python version: 3.10.8
  - pip version: 23.0.1
  - `google-auth` version: 2.16.1

#### Steps to reproduce

  1. Set a [Reauthentication policy](https://admin.google.com/ac/security/reauth/admin-tools) in Google Cloud.
  2. Authenticate using `gcloud auth login --update-adc`.
  3. Let your access token expire.
  4. Now, `python -c "import google.auth; google.auth.default()"` will prompt you with `Please enter your password:`.

If instead of letting your token expire, you actively revoke it with `gcloud auth application-default revoke` and `gcloud auth revoke`, then you get the following exception raised:

```
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File ".../.venv/lib/python3.10/site-packages/google/auth/_default.py", line 653, in default
    raise exceptions.DefaultCredentialsError(_HELP_MESSAGE)
google.auth.exceptions.DefaultCredentialsError: Could not automatically determine credentials. Please set GOOGLE_APPLICATION_CREDENTIALS or explicitly create credentials and re-run the application. For more information, please see https://cloud.google.com/docs/authentication/getting-started
```

Also, for some reason the password prompt is **not** opened if you capture `stdin` like `subprocess.check_output(["gcloud", "config", "config-helper", "--format", "json"], stdin=subprocess.PIPE)`. This has the interesting effect that if you run code in `pytest`(that captures `stdin`), you don't get the password prompt, but if you run it with `pytest -s` you do.


Discussion:

Patch:
diff --git a/google/auth/_cloud_sdk.py b/google/auth/_cloud_sdk.py
index 40e6aec..36c5b01 100644
--- a/google/auth/_cloud_sdk.py
+++ b/google/auth/_cloud_sdk.py
@@ -14,12 +14,12 @@
 
 """Helpers for reading the Google Cloud SDK's configuration."""
 
-import json
 import os
 import subprocess
 
 import six
 
+from google.auth import _helpers
 from google.auth import environment_vars
 from google.auth import exceptions
 
@@ -35,7 +35,7 @@ _CREDENTIALS_FILENAME = "application_default_credentials.json"
 _CLOUD_SDK_POSIX_COMMAND = "gcloud"
 _CLOUD_SDK_WINDOWS_COMMAND = "gcloud.cmd"
 # The command to get the Cloud SDK configuration
-_CLOUD_SDK_CONFIG_COMMAND = ("config", "config-helper", "--format", "json")
+_CLOUD_SDK_CONFIG_GET_PROJECT_COMMAND = ("config", "get", "project")
 # The command to get google user access token
 _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND = ("auth", "print-access-token")
 # Cloud SDK's application-default client ID
@@ -105,18 +105,14 @@ def get_project_id():
     try:
         # Ignore the stderr coming from gcloud, so it won't be mixed into the output.
         # https://github.com/googleapis/google-auth-library-python/issues/673
-        output = _run_subprocess_ignore_stderr((command,) + _CLOUD_SDK_CONFIG_COMMAND)
-    except (subprocess.CalledProcessError, OSError, IOError):
-        return None
-
-    try:
-        configuration = json.loads(output.decode("utf-8"))
-    except ValueError:
-        return None
+        project = _run_subprocess_ignore_stderr(
+            (command,) + _CLOUD_SDK_CONFIG_GET_PROJECT_COMMAND
+        )
 
-    try:
-        return configuration["configuration"]["properties"]["core"]["project"]
-    except KeyError:
+        # Turn bytes into a string and remove "\n"
+        project = _helpers.from_bytes(project).strip()
+        return project if project else None
+    except (subprocess.CalledProcessError, OSError, IOError):
         return None
 
 


Test patch:
diff --git a/tests/data/cloud_sdk_config.json b/tests/data/cloud_sdk_config.json
deleted file mode 100644
index a5fe4a9..0000000
--- a/tests/data/cloud_sdk_config.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-  "configuration": {
-    "active_configuration": "default",
-    "properties": {
-      "core": {
-        "account": "user@example.com",
-        "disable_usage_reporting": "False",
-        "project": "example-project"
-      }
-    }
-  },
-  "credential": {
-    "access_token": "don't use me",
-    "token_expiry": "2017-03-23T23:09:49Z"
-  },
-  "sentinels": {
-    "config_sentinel": "/Users/example/.config/gcloud/config_sentinel"
-  }
-}
diff --git a/tests/test__cloud_sdk.py b/tests/test__cloud_sdk.py
index 160e1f1..e45c65b 100644
--- a/tests/test__cloud_sdk.py
+++ b/tests/test__cloud_sdk.py
@@ -37,17 +37,10 @@ SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
 with io.open(SERVICE_ACCOUNT_FILE, "rb") as fh:
     SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
 
-with io.open(os.path.join(DATA_DIR, "cloud_sdk_config.json"), "rb") as fh:
-    CLOUD_SDK_CONFIG_FILE_DATA = fh.read()
-
 
 @pytest.mark.parametrize(
     "data, expected_project_id",
-    [
-        (CLOUD_SDK_CONFIG_FILE_DATA, "example-project"),
-        (b"I am some bad json", None),
-        (b"{}", None),
-    ],
+    [(b"example-project\n", "example-project"), (b"", None)],
 )
 def test_get_project_id(data, expected_project_id):
     check_output_patch = mock.patch(
@@ -94,9 +87,7 @@ def test__run_subprocess_ignore_stderr():
 @mock.patch("os.name", new="nt")
 def test_get_project_id_windows():
     check_output_patch = mock.patch(
-        "subprocess.check_output",
-        autospec=True,
-        return_value=CLOUD_SDK_CONFIG_FILE_DATA,
+        "subprocess.check_output", autospec=True, return_value=b"example-project\n"
     )
 
     with check_output_patch as check_output: