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")