feat: add fetch_id_token to support id_token adc (#469)

feat: id_token adc with gcloud cred
diff --git a/docs/user-guide.rst b/docs/user-guide.rst
index 3877bff..08e7167 100644
--- a/docs/user-guide.rst
+++ b/docs/user-guide.rst
@@ -291,6 +291,20 @@
                                       target_credentials,
                                       target_audience=target_audience)
 
+If your application runs on `App Engine`_, `Cloud Run`_, `Compute Engine`_, or
+has application default credentials set via `GOOGLE_APPLICATION_CREDENTIALS`
+environment variable, you can also use `google.oauth2.id_token.fetch_id_token`
+to obtain an ID token from your current running environment. The following is an
+example ::
+
+    import google.oauth2.id_token
+    import google.auth.transport.requests
+
+    request = google.auth.transport.requests.Request()
+    target_audience = "https://pubsub.googleapis.com"
+
+    id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)
+
 IDToken verification can be done for various type of IDTokens using the
 :class:`google.oauth2.id_token` module. It supports ID token signed with RS256
 and ES256 algorithms. However, ES256 algorithm won't be available unless
@@ -334,8 +348,10 @@
     print(token)
     print(id_token.verify_token(token,request))
 
+.. _App Engine: https://cloud.google.com/appengine/
 .. _Cloud Functions: https://cloud.google.com/functions/
 .. _Cloud Run: https://cloud.google.com/run/
+.. _Compute Engine: https://cloud.google.com/compute/
 .. _Identity Aware Proxy: https://cloud.google.com/iap/
 .. _Google OpenID Connect: https://developers.google.com/identity/protocols/OpenIDConnect
 .. _Google ID Token: https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken
diff --git a/google/oauth2/id_token.py b/google/oauth2/id_token.py
index 1dbfb20..f559c6c 100644
--- a/google/oauth2/id_token.py
+++ b/google/oauth2/id_token.py
@@ -59,12 +59,16 @@
 """
 
 import json
+import os
 
+import six
 from six.moves import http_client
 
+from google.auth import environment_vars
 from google.auth import exceptions
 from google.auth import jwt
 
+
 # The URL that provides public certificates for verifying ID tokens issued
 # by Google's OAuth 2.0 authorization server.
 _GOOGLE_OAUTH2_CERTS_URL = "https://www.googleapis.com/oauth2/v1/certs"
@@ -159,3 +163,90 @@
     return verify_token(
         id_token, request, audience=audience, certs_url=_GOOGLE_APIS_CERTS_URL
     )
+
+
+def fetch_id_token(request, audience):
+    """Fetch the ID Token from the current environment.
+
+    This function acquires ID token from the environment in the following order:
+
+    1. If the application is running in Compute Engine, App Engine or Cloud Run,
+       then the ID token are obtained from the metadata server.
+    2. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+       to the path of a valid service account JSON file, then ID token is
+       acquired using this service account credentials.
+    3. If metadata server doesn't exist and no valid service account credentials
+       are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
+       be raised.
+
+    Example::
+
+        import google.oauth2.id_token
+        import google.auth.transport.requests
+
+        request = google.auth.transport.requests.Request()
+        target_audience = "https://pubsub.googleapis.com"
+
+        id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        audience (str): The audience that this ID token is intended for.
+
+    Returns:
+        str: The ID token.
+
+    Raises:
+        ~google.auth.exceptions.DefaultCredentialsError:
+            If metadata server doesn't exist and no valid service account
+            credentials are found.
+    """
+    # 1. First try to fetch ID token from metada server if it exists. The code
+    # works for GAE and Cloud Run metadata server as well.
+    try:
+        from google.auth import compute_engine
+
+        credentials = compute_engine.IDTokenCredentials(
+            request, audience, use_metadata_identity_endpoint=True
+        )
+        credentials.refresh(request)
+        return credentials.token
+    except (ImportError, exceptions.TransportError):
+        pass
+
+    # 2. Try to use service account credentials to get ID token.
+
+    # Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
+    # variable.
+    credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
+    if not (
+        credentials_filename
+        and os.path.exists(credentials_filename)
+        and os.path.isfile(credentials_filename)
+    ):
+        raise exceptions.DefaultCredentialsError(
+            "Neither metadata server or valid service account credentials are found."
+        )
+
+    try:
+        with open(credentials_filename, "r") as f:
+            info = json.load(f)
+            credentials_content = (
+                (info.get("type") == "service_account") and info or None
+            )
+
+            from google.oauth2 import service_account
+
+            credentials = service_account.IDTokenCredentials.from_service_account_info(
+                credentials_content, target_audience=audience
+            )
+    except ValueError as caught_exc:
+        new_exc = exceptions.DefaultCredentialsError(
+            "Neither metadata server or valid service account credentials are found.",
+            caught_exc,
+        )
+        six.raise_from(new_exc, caught_exc)
+
+    credentials.refresh(request)
+    return credentials.token
diff --git a/system_tests/noxfile.py b/system_tests/noxfile.py
index 6e66eb4..14cd3db 100644
--- a/system_tests/noxfile.py
+++ b/system_tests/noxfile.py
@@ -200,7 +200,7 @@
     session.env[EXPECT_PROJECT_ENV] = "1"
     session.install(*TEST_DEPENDENCIES)
     session.install(LIBRARY_DIR)
-    session.run("pytest", "test_default.py")
+    session.run("pytest", "test_default.py", "test_id_token.py")
 
 
 @nox.session(python=PYTHON_VERSIONS)
diff --git a/system_tests/test_compute_engine.py b/system_tests/test_compute_engine.py
index bcfdfd6..b0d42f3 100644
--- a/system_tests/test_compute_engine.py
+++ b/system_tests/test_compute_engine.py
@@ -20,6 +20,9 @@
 from google.auth import exceptions
 from google.auth import jwt
 from google.auth.compute_engine import _metadata
+import google.oauth2.id_token
+
+AUDIENCE = "https://pubsub.googleapis.com"
 
 
 @pytest.fixture(autouse=True)
@@ -53,10 +56,17 @@
 
 def test_id_token_from_metadata(http_request):
     credentials = compute_engine.IDTokenCredentials(
-        http_request, "target_audience", use_metadata_identity_endpoint=True
+        http_request, AUDIENCE, use_metadata_identity_endpoint=True
     )
     credentials.refresh(http_request)
 
     _, payload, _, _ = jwt._unverified_decode(credentials.token)
-    assert payload["aud"] == "target_audience"
+    assert payload["aud"] == AUDIENCE
     assert payload["exp"] == credentials.expiry
+
+
+def test_fetch_id_token(http_request):
+    token = google.oauth2.id_token.fetch_id_token(http_request, AUDIENCE)
+
+    _, payload, _, _ = jwt._unverified_decode(token)
+    assert payload["aud"] == AUDIENCE
diff --git a/system_tests/test_id_token.py b/system_tests/test_id_token.py
new file mode 100644
index 0000000..b07cefc
--- /dev/null
+++ b/system_tests/test_id_token.py
@@ -0,0 +1,25 @@
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import pytest
+
+from google.auth import jwt
+import google.oauth2.id_token
+
+
+def test_fetch_id_token(http_request):
+    audience = "https://pubsub.googleapis.com"
+    token = google.oauth2.id_token.fetch_id_token(http_request, audience)
+
+    _, payload, _, _ = jwt._unverified_decode(token)
+    assert payload["aud"] == audience
diff --git a/tests/oauth2/test_id_token.py b/tests/oauth2/test_id_token.py
index 980a8e9..ff85807 100644
--- a/tests/oauth2/test_id_token.py
+++ b/tests/oauth2/test_id_token.py
@@ -13,14 +13,21 @@
 # limitations under the License.
 
 import json
+import os
 
 import mock
 import pytest
 
+from google.auth import environment_vars
 from google.auth import exceptions
 from google.auth import transport
+import google.auth.compute_engine._metadata
 from google.oauth2 import id_token
 
+SERVICE_ACCOUNT_FILE = os.path.join(
+    os.path.dirname(__file__), "../data/service_account.json"
+)
+
 
 def make_request(status, data=None):
     response = mock.create_autospec(transport.Response, instance=True)
@@ -114,3 +121,64 @@
         audience=mock.sentinel.audience,
         certs_url=id_token._GOOGLE_APIS_CERTS_URL,
     )
+
+
+def test_fetch_id_token_from_metadata_server():
+    def mock_init(self, request, audience, use_metadata_identity_endpoint):
+        assert use_metadata_identity_endpoint
+        self.token = "id_token"
+
+    with mock.patch.multiple(
+        google.auth.compute_engine.IDTokenCredentials,
+        __init__=mock_init,
+        refresh=mock.Mock(),
+    ):
+        request = mock.Mock()
+        token = id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
+        assert token == "id_token"
+
+
+@mock.patch.object(
+    google.auth.compute_engine.IDTokenCredentials,
+    "__init__",
+    side_effect=exceptions.TransportError(),
+)
+def test_fetch_id_token_from_explicit_cred_json_file(mock_init, monkeypatch):
+    monkeypatch.setenv(environment_vars.CREDENTIALS, SERVICE_ACCOUNT_FILE)
+
+    def mock_refresh(self, request):
+        self.token = "id_token"
+
+    with mock.patch.object(
+        google.oauth2.service_account.IDTokenCredentials, "refresh", mock_refresh
+    ):
+        request = mock.Mock()
+        token = id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
+        assert token == "id_token"
+
+
+@mock.patch.object(
+    google.auth.compute_engine.IDTokenCredentials,
+    "__init__",
+    side_effect=exceptions.TransportError(),
+)
+def test_fetch_id_token_no_cred_json_file(mock_init, monkeypatch):
+    monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
+
+    with pytest.raises(exceptions.DefaultCredentialsError):
+        request = mock.Mock()
+        id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
+
+
+@mock.patch.object(
+    google.auth.compute_engine.IDTokenCredentials,
+    "__init__",
+    side_effect=exceptions.TransportError(),
+)
+def test_fetch_id_token_invalid_cred_file(mock_init, monkeypatch):
+    not_json_file = os.path.join(os.path.dirname(__file__), "../data/public_cert.pem")
+    monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
+
+    with pytest.raises(exceptions.DefaultCredentialsError):
+        request = mock.Mock()
+        id_token.fetch_id_token(request, "https://pubsub.googleapis.com")