Skip to content

Commit

Permalink
fix: fix fetch_id_token credential lookup order to match adc (#748)
Browse files Browse the repository at this point in the history
* fix: fix fetch_id_token credential lookup order to match adc

* fix tests

* fix linter

* update

* update

* add comments
  • Loading branch information
arithmetic1728 committed Jul 14, 2021
1 parent 6bf460f commit c34452e
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 155 deletions.
93 changes: 46 additions & 47 deletions google/oauth2/_id_token_async.py
Expand Up @@ -180,13 +180,14 @@ async def verify_firebase_token(id_token, request, audience=None):
async 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:
This function acquires ID token from the environment in the following order.
See https://google.aip.dev/auth/4110.
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
1. 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.
2. If the application is running in Compute Engine, App Engine or Cloud Run,
then the ID token are obtained from the metadata server.
3. If metadata server doesn't exist and no valid service account credentials
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
be raised.
Expand Down Expand Up @@ -214,54 +215,52 @@ async def fetch_id_token(request, audience):
If metadata server doesn't exist and no valid service account
credentials are found.
"""
# 1. First try to fetch ID token from metadata server if it exists. The code
# works for GAE and Cloud Run metadata server as well.
try:
from google.auth import compute_engine

request_new = requests.Request()
credentials = compute_engine.IDTokenCredentials(
request_new, audience, use_metadata_identity_endpoint=True
)
credentials.refresh(request_new)

return credentials.token

except (ImportError, exceptions.TransportError, exceptions.RefreshError):
pass

# 2. Try to use service account credentials to get ID token.

# Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
# 1. 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."
)
if credentials_filename:
if not (
os.path.exists(credentials_filename)
and os.path.isfile(credentials_filename)
):
raise exceptions.DefaultCredentialsError(
"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
)

try:
with open(credentials_filename, "r") as f:
info = json.load(f)
credentials_content = (
(info.get("type") == "service_account") and info or None
try:
with open(credentials_filename, "r") as f:
from google.oauth2 import _service_account_async as service_account

info = json.load(f)
if info.get("type") == "service_account":
credentials = service_account.IDTokenCredentials.from_service_account_info(
info, target_audience=audience
)
await credentials.refresh(request)
return credentials.token
except ValueError as caught_exc:
new_exc = exceptions.DefaultCredentialsError(
"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
caught_exc,
)
six.raise_from(new_exc, caught_exc)

from google.oauth2 import _service_account_async as service_account
# 2. 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
from google.auth.compute_engine import _metadata

credentials = service_account.IDTokenCredentials.from_service_account_info(
credentials_content, target_audience=audience
request_new = requests.Request()
if _metadata.ping(request_new):
credentials = compute_engine.IDTokenCredentials(
request_new, audience, use_metadata_identity_endpoint=True
)
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_new)
return credentials.token
except (ImportError, exceptions.TransportError):
pass

await credentials.refresh(request)
return credentials.token
raise exceptions.DefaultCredentialsError(
"Neither metadata server or valid service account credentials are found."
)
89 changes: 45 additions & 44 deletions google/oauth2/id_token.py
Expand Up @@ -179,13 +179,14 @@ def verify_firebase_token(id_token, request, audience=None):
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:
This function acquires ID token from the environment in the following order.
See https://google.aip.dev/auth/4110.
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
1. 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.
2. If the application is running in Compute Engine, App Engine or Cloud Run,
then the ID token are obtained from the metadata server.
3. If metadata server doesn't exist and no valid service account credentials
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
be raised.
Expand Down Expand Up @@ -213,51 +214,51 @@ def fetch_id_token(request, audience):
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, exceptions.RefreshError):
pass

# 2. Try to use service account credentials to get ID token.

# Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
# 1. 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."
)
if credentials_filename:
if not (
os.path.exists(credentials_filename)
and os.path.isfile(credentials_filename)
):
raise exceptions.DefaultCredentialsError(
"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
)

try:
with open(credentials_filename, "r") as f:
info = json.load(f)
credentials_content = (
(info.get("type") == "service_account") and info or None
try:
with open(credentials_filename, "r") as f:
from google.oauth2 import service_account

info = json.load(f)
if info.get("type") == "service_account":
credentials = service_account.IDTokenCredentials.from_service_account_info(
info, target_audience=audience
)
credentials.refresh(request)
return credentials.token
except ValueError as caught_exc:
new_exc = exceptions.DefaultCredentialsError(
"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
caught_exc,
)
six.raise_from(new_exc, caught_exc)

from google.oauth2 import service_account
# 2. 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
from google.auth.compute_engine import _metadata

credentials = service_account.IDTokenCredentials.from_service_account_info(
credentials_content, target_audience=audience
if _metadata.ping(request):
credentials = compute_engine.IDTokenCredentials(
request, audience, use_metadata_identity_endpoint=True
)
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
except (ImportError, exceptions.TransportError):
pass

credentials.refresh(request)
return credentials.token
raise exceptions.DefaultCredentialsError(
"Neither metadata server or valid service account credentials are found."
)
98 changes: 65 additions & 33 deletions tests/oauth2/test_id_token.py
Expand Up @@ -23,6 +23,7 @@
from google.auth import transport
import google.auth.compute_engine._metadata
from google.oauth2 import id_token
from google.oauth2 import service_account

SERVICE_ACCOUNT_FILE = os.path.join(
os.path.dirname(__file__), "../data/service_account.json"
Expand Down Expand Up @@ -134,62 +135,93 @@ def test_verify_firebase_token(verify_token):
)


def test_fetch_id_token_from_metadata_server():
def test_fetch_id_token_from_metadata_server(monkeypatch):
monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)

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"
with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True):
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):
def test_fetch_id_token_from_explicit_cred_json_file(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
):
with mock.patch.object(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):
def test_fetch_id_token_no_cred_exists(monkeypatch):
monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)

with pytest.raises(exceptions.DefaultCredentialsError):
with mock.patch(
"google.auth.compute_engine._metadata.ping",
side_effect=exceptions.TransportError(),
):
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
request = mock.Mock()
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
assert excinfo.match(
r"Neither metadata server or valid service account credentials are found."
)

with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
request = mock.Mock()
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
assert excinfo.match(
r"Neither metadata server or valid service account credentials are found."
)


def test_fetch_id_token_invalid_cred_file_type(monkeypatch):
user_credentials_file = os.path.join(
os.path.dirname(__file__), "../data/authorized_user.json"
)
monkeypatch.setenv(environment_vars.CREDENTIALS, user_credentials_file)

with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
request = mock.Mock()
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
assert excinfo.match(
r"Neither metadata server or valid service account credentials are found."
)


def test_fetch_id_token_invalid_json(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) as excinfo:
request = mock.Mock()
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
assert excinfo.match(
r"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials."
)


@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")
def test_fetch_id_token_invalid_cred_path(monkeypatch):
not_json_file = os.path.join(os.path.dirname(__file__), "../data/not_exists.json")
monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)

with pytest.raises(exceptions.DefaultCredentialsError):
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
request = mock.Mock()
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
assert excinfo.match(
r"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
)

0 comments on commit c34452e

Please sign in to comment.