Skip to content

Commit c34452e

Browse files
fix: fix fetch_id_token credential lookup order to match adc (#748)
* fix: fix fetch_id_token credential lookup order to match adc * fix tests * fix linter * update * update * add comments
1 parent 6bf460f commit c34452e

File tree

4 files changed

+227
-155
lines changed

4 files changed

+227
-155
lines changed

google/oauth2/_id_token_async.py

+46-47
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,14 @@ async def verify_firebase_token(id_token, request, audience=None):
180180
async def fetch_id_token(request, audience):
181181
"""Fetch the ID Token from the current environment.
182182
183-
This function acquires ID token from the environment in the following order:
183+
This function acquires ID token from the environment in the following order.
184+
See https://google.aip.dev/auth/4110.
184185
185-
1. If the application is running in Compute Engine, App Engine or Cloud Run,
186-
then the ID token are obtained from the metadata server.
187-
2. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
186+
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
188187
to the path of a valid service account JSON file, then ID token is
189188
acquired using this service account credentials.
189+
2. If the application is running in Compute Engine, App Engine or Cloud Run,
190+
then the ID token are obtained from the metadata server.
190191
3. If metadata server doesn't exist and no valid service account credentials
191192
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
192193
be raised.
@@ -214,54 +215,52 @@ async def fetch_id_token(request, audience):
214215
If metadata server doesn't exist and no valid service account
215216
credentials are found.
216217
"""
217-
# 1. First try to fetch ID token from metadata server if it exists. The code
218-
# works for GAE and Cloud Run metadata server as well.
219-
try:
220-
from google.auth import compute_engine
221-
222-
request_new = requests.Request()
223-
credentials = compute_engine.IDTokenCredentials(
224-
request_new, audience, use_metadata_identity_endpoint=True
225-
)
226-
credentials.refresh(request_new)
227-
228-
return credentials.token
229-
230-
except (ImportError, exceptions.TransportError, exceptions.RefreshError):
231-
pass
232-
233-
# 2. Try to use service account credentials to get ID token.
234-
235-
# Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
218+
# 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
236219
# variable.
237220
credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
238-
if not (
239-
credentials_filename
240-
and os.path.exists(credentials_filename)
241-
and os.path.isfile(credentials_filename)
242-
):
243-
raise exceptions.DefaultCredentialsError(
244-
"Neither metadata server or valid service account credentials are found."
245-
)
221+
if credentials_filename:
222+
if not (
223+
os.path.exists(credentials_filename)
224+
and os.path.isfile(credentials_filename)
225+
):
226+
raise exceptions.DefaultCredentialsError(
227+
"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
228+
)
246229

247-
try:
248-
with open(credentials_filename, "r") as f:
249-
info = json.load(f)
250-
credentials_content = (
251-
(info.get("type") == "service_account") and info or None
230+
try:
231+
with open(credentials_filename, "r") as f:
232+
from google.oauth2 import _service_account_async as service_account
233+
234+
info = json.load(f)
235+
if info.get("type") == "service_account":
236+
credentials = service_account.IDTokenCredentials.from_service_account_info(
237+
info, target_audience=audience
238+
)
239+
await credentials.refresh(request)
240+
return credentials.token
241+
except ValueError as caught_exc:
242+
new_exc = exceptions.DefaultCredentialsError(
243+
"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
244+
caught_exc,
252245
)
246+
six.raise_from(new_exc, caught_exc)
253247

254-
from google.oauth2 import _service_account_async as service_account
248+
# 2. Try to fetch ID token from metada server if it exists. The code works for GAE and
249+
# Cloud Run metadata server as well.
250+
try:
251+
from google.auth import compute_engine
252+
from google.auth.compute_engine import _metadata
255253

256-
credentials = service_account.IDTokenCredentials.from_service_account_info(
257-
credentials_content, target_audience=audience
254+
request_new = requests.Request()
255+
if _metadata.ping(request_new):
256+
credentials = compute_engine.IDTokenCredentials(
257+
request_new, audience, use_metadata_identity_endpoint=True
258258
)
259-
except ValueError as caught_exc:
260-
new_exc = exceptions.DefaultCredentialsError(
261-
"Neither metadata server or valid service account credentials are found.",
262-
caught_exc,
263-
)
264-
six.raise_from(new_exc, caught_exc)
259+
credentials.refresh(request_new)
260+
return credentials.token
261+
except (ImportError, exceptions.TransportError):
262+
pass
265263

266-
await credentials.refresh(request)
267-
return credentials.token
264+
raise exceptions.DefaultCredentialsError(
265+
"Neither metadata server or valid service account credentials are found."
266+
)

google/oauth2/id_token.py

+45-44
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,14 @@ def verify_firebase_token(id_token, request, audience=None):
179179
def fetch_id_token(request, audience):
180180
"""Fetch the ID Token from the current environment.
181181
182-
This function acquires ID token from the environment in the following order:
182+
This function acquires ID token from the environment in the following order.
183+
See https://google.aip.dev/auth/4110.
183184
184-
1. If the application is running in Compute Engine, App Engine or Cloud Run,
185-
then the ID token are obtained from the metadata server.
186-
2. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
185+
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
187186
to the path of a valid service account JSON file, then ID token is
188187
acquired using this service account credentials.
188+
2. If the application is running in Compute Engine, App Engine or Cloud Run,
189+
then the ID token are obtained from the metadata server.
189190
3. If metadata server doesn't exist and no valid service account credentials
190191
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
191192
be raised.
@@ -213,51 +214,51 @@ def fetch_id_token(request, audience):
213214
If metadata server doesn't exist and no valid service account
214215
credentials are found.
215216
"""
216-
# 1. First try to fetch ID token from metada server if it exists. The code
217-
# works for GAE and Cloud Run metadata server as well.
218-
try:
219-
from google.auth import compute_engine
220-
221-
credentials = compute_engine.IDTokenCredentials(
222-
request, audience, use_metadata_identity_endpoint=True
223-
)
224-
credentials.refresh(request)
225-
return credentials.token
226-
except (ImportError, exceptions.TransportError, exceptions.RefreshError):
227-
pass
228-
229-
# 2. Try to use service account credentials to get ID token.
230-
231-
# Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
217+
# 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
232218
# variable.
233219
credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
234-
if not (
235-
credentials_filename
236-
and os.path.exists(credentials_filename)
237-
and os.path.isfile(credentials_filename)
238-
):
239-
raise exceptions.DefaultCredentialsError(
240-
"Neither metadata server or valid service account credentials are found."
241-
)
220+
if credentials_filename:
221+
if not (
222+
os.path.exists(credentials_filename)
223+
and os.path.isfile(credentials_filename)
224+
):
225+
raise exceptions.DefaultCredentialsError(
226+
"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
227+
)
242228

243-
try:
244-
with open(credentials_filename, "r") as f:
245-
info = json.load(f)
246-
credentials_content = (
247-
(info.get("type") == "service_account") and info or None
229+
try:
230+
with open(credentials_filename, "r") as f:
231+
from google.oauth2 import service_account
232+
233+
info = json.load(f)
234+
if info.get("type") == "service_account":
235+
credentials = service_account.IDTokenCredentials.from_service_account_info(
236+
info, target_audience=audience
237+
)
238+
credentials.refresh(request)
239+
return credentials.token
240+
except ValueError as caught_exc:
241+
new_exc = exceptions.DefaultCredentialsError(
242+
"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
243+
caught_exc,
248244
)
245+
six.raise_from(new_exc, caught_exc)
249246

250-
from google.oauth2 import service_account
247+
# 2. Try to fetch ID token from metada server if it exists. The code works for GAE and
248+
# Cloud Run metadata server as well.
249+
try:
250+
from google.auth import compute_engine
251+
from google.auth.compute_engine import _metadata
251252

252-
credentials = service_account.IDTokenCredentials.from_service_account_info(
253-
credentials_content, target_audience=audience
253+
if _metadata.ping(request):
254+
credentials = compute_engine.IDTokenCredentials(
255+
request, audience, use_metadata_identity_endpoint=True
254256
)
255-
except ValueError as caught_exc:
256-
new_exc = exceptions.DefaultCredentialsError(
257-
"Neither metadata server or valid service account credentials are found.",
258-
caught_exc,
259-
)
260-
six.raise_from(new_exc, caught_exc)
257+
credentials.refresh(request)
258+
return credentials.token
259+
except (ImportError, exceptions.TransportError):
260+
pass
261261

262-
credentials.refresh(request)
263-
return credentials.token
262+
raise exceptions.DefaultCredentialsError(
263+
"Neither metadata server or valid service account credentials are found."
264+
)

tests/oauth2/test_id_token.py

+65-33
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from google.auth import transport
2424
import google.auth.compute_engine._metadata
2525
from google.oauth2 import id_token
26+
from google.oauth2 import service_account
2627

2728
SERVICE_ACCOUNT_FILE = os.path.join(
2829
os.path.dirname(__file__), "../data/service_account.json"
@@ -134,62 +135,93 @@ def test_verify_firebase_token(verify_token):
134135
)
135136

136137

137-
def test_fetch_id_token_from_metadata_server():
138+
def test_fetch_id_token_from_metadata_server(monkeypatch):
139+
monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
140+
138141
def mock_init(self, request, audience, use_metadata_identity_endpoint):
139142
assert use_metadata_identity_endpoint
140143
self.token = "id_token"
141144

142-
with mock.patch.multiple(
143-
google.auth.compute_engine.IDTokenCredentials,
144-
__init__=mock_init,
145-
refresh=mock.Mock(),
146-
):
147-
request = mock.Mock()
148-
token = id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
149-
assert token == "id_token"
145+
with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True):
146+
with mock.patch.multiple(
147+
google.auth.compute_engine.IDTokenCredentials,
148+
__init__=mock_init,
149+
refresh=mock.Mock(),
150+
):
151+
request = mock.Mock()
152+
token = id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
153+
assert token == "id_token"
150154

151155

152-
@mock.patch.object(
153-
google.auth.compute_engine.IDTokenCredentials,
154-
"__init__",
155-
side_effect=exceptions.TransportError(),
156-
)
157-
def test_fetch_id_token_from_explicit_cred_json_file(mock_init, monkeypatch):
156+
def test_fetch_id_token_from_explicit_cred_json_file(monkeypatch):
158157
monkeypatch.setenv(environment_vars.CREDENTIALS, SERVICE_ACCOUNT_FILE)
159158

160159
def mock_refresh(self, request):
161160
self.token = "id_token"
162161

163-
with mock.patch.object(
164-
google.oauth2.service_account.IDTokenCredentials, "refresh", mock_refresh
165-
):
162+
with mock.patch.object(service_account.IDTokenCredentials, "refresh", mock_refresh):
166163
request = mock.Mock()
167164
token = id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
168165
assert token == "id_token"
169166

170167

171-
@mock.patch.object(
172-
google.auth.compute_engine.IDTokenCredentials,
173-
"__init__",
174-
side_effect=exceptions.TransportError(),
175-
)
176-
def test_fetch_id_token_no_cred_json_file(mock_init, monkeypatch):
168+
def test_fetch_id_token_no_cred_exists(monkeypatch):
177169
monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
178170

179-
with pytest.raises(exceptions.DefaultCredentialsError):
171+
with mock.patch(
172+
"google.auth.compute_engine._metadata.ping",
173+
side_effect=exceptions.TransportError(),
174+
):
175+
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
176+
request = mock.Mock()
177+
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
178+
assert excinfo.match(
179+
r"Neither metadata server or valid service account credentials are found."
180+
)
181+
182+
with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
183+
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
184+
request = mock.Mock()
185+
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
186+
assert excinfo.match(
187+
r"Neither metadata server or valid service account credentials are found."
188+
)
189+
190+
191+
def test_fetch_id_token_invalid_cred_file_type(monkeypatch):
192+
user_credentials_file = os.path.join(
193+
os.path.dirname(__file__), "../data/authorized_user.json"
194+
)
195+
monkeypatch.setenv(environment_vars.CREDENTIALS, user_credentials_file)
196+
197+
with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
198+
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
199+
request = mock.Mock()
200+
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
201+
assert excinfo.match(
202+
r"Neither metadata server or valid service account credentials are found."
203+
)
204+
205+
206+
def test_fetch_id_token_invalid_json(monkeypatch):
207+
not_json_file = os.path.join(os.path.dirname(__file__), "../data/public_cert.pem")
208+
monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
209+
210+
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
180211
request = mock.Mock()
181212
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
213+
assert excinfo.match(
214+
r"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials."
215+
)
182216

183217

184-
@mock.patch.object(
185-
google.auth.compute_engine.IDTokenCredentials,
186-
"__init__",
187-
side_effect=exceptions.TransportError(),
188-
)
189-
def test_fetch_id_token_invalid_cred_file(mock_init, monkeypatch):
190-
not_json_file = os.path.join(os.path.dirname(__file__), "../data/public_cert.pem")
218+
def test_fetch_id_token_invalid_cred_path(monkeypatch):
219+
not_json_file = os.path.join(os.path.dirname(__file__), "../data/not_exists.json")
191220
monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
192221

193-
with pytest.raises(exceptions.DefaultCredentialsError):
222+
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
194223
request = mock.Mock()
195224
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
225+
assert excinfo.match(
226+
r"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
227+
)

0 commit comments

Comments
 (0)