Skip to content

Commit 8f1e9cf

Browse files
fix: add fetch_id_token_credentials (#866)
1 parent 77d7f1b commit 8f1e9cf

File tree

2 files changed

+116
-47
lines changed

2 files changed

+116
-47
lines changed

google/oauth2/id_token.py

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
from google.auth import environment_vars
6565
from google.auth import exceptions
6666
from google.auth import jwt
67+
import google.auth.transport.requests
6768

6869

6970
# The URL that provides public certificates for verifying ID tokens issued
@@ -201,8 +202,8 @@ def verify_firebase_token(id_token, request, audience=None, clock_skew_in_second
201202
)
202203

203204

204-
def fetch_id_token(request, audience):
205-
"""Fetch the ID Token from the current environment.
205+
def fetch_id_token_credentials(audience, request=None):
206+
"""Create the ID Token credentials from the current environment.
206207
207208
This function acquires ID token from the environment in the following order.
208209
See https://google.aip.dev/auth/4110.
@@ -224,15 +225,22 @@ def fetch_id_token(request, audience):
224225
request = google.auth.transport.requests.Request()
225226
target_audience = "https://pubsub.googleapis.com"
226227
227-
id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)
228+
# Create ID token credentials.
229+
credentials = google.oauth2.id_token.fetch_id_token_credentials(target_audience, request=request)
230+
231+
# Refresh the credential to obtain an ID token.
232+
credentials.refresh(request)
233+
234+
id_token = credentials.token
235+
id_token_expiry = credentials.expiry
228236
229237
Args:
230-
request (google.auth.transport.Request): A callable used to make
231-
HTTP requests.
232238
audience (str): The audience that this ID token is intended for.
239+
request (Optional[google.auth.transport.Request]): A callable used to make
240+
HTTP requests. A request object will be created if not provided.
233241
234242
Returns:
235-
str: The ID token.
243+
google.auth.credentials.Credentials: The ID token credentials.
236244
237245
Raises:
238246
~google.auth.exceptions.DefaultCredentialsError:
@@ -257,11 +265,9 @@ def fetch_id_token(request, audience):
257265

258266
info = json.load(f)
259267
if info.get("type") == "service_account":
260-
credentials = service_account.IDTokenCredentials.from_service_account_info(
268+
return service_account.IDTokenCredentials.from_service_account_info(
261269
info, target_audience=audience
262270
)
263-
credentials.refresh(request)
264-
return credentials.token
265271
except ValueError as caught_exc:
266272
new_exc = exceptions.DefaultCredentialsError(
267273
"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
@@ -275,15 +281,60 @@ def fetch_id_token(request, audience):
275281
from google.auth import compute_engine
276282
from google.auth.compute_engine import _metadata
277283

284+
# Create a request object if not provided.
285+
if not request:
286+
request = google.auth.transport.requests.Request()
287+
278288
if _metadata.ping(request):
279-
credentials = compute_engine.IDTokenCredentials(
289+
return compute_engine.IDTokenCredentials(
280290
request, audience, use_metadata_identity_endpoint=True
281291
)
282-
credentials.refresh(request)
283-
return credentials.token
284292
except (ImportError, exceptions.TransportError):
285293
pass
286294

287295
raise exceptions.DefaultCredentialsError(
288296
"Neither metadata server or valid service account credentials are found."
289297
)
298+
299+
300+
def fetch_id_token(request, audience):
301+
"""Fetch the ID Token from the current environment.
302+
303+
This function acquires ID token from the environment in the following order.
304+
See https://google.aip.dev/auth/4110.
305+
306+
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
307+
to the path of a valid service account JSON file, then ID token is
308+
acquired using this service account credentials.
309+
2. If the application is running in Compute Engine, App Engine or Cloud Run,
310+
then the ID token are obtained from the metadata server.
311+
3. If metadata server doesn't exist and no valid service account credentials
312+
are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
313+
be raised.
314+
315+
Example::
316+
317+
import google.oauth2.id_token
318+
import google.auth.transport.requests
319+
320+
request = google.auth.transport.requests.Request()
321+
target_audience = "https://pubsub.googleapis.com"
322+
323+
id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)
324+
325+
Args:
326+
request (google.auth.transport.Request): A callable used to make
327+
HTTP requests.
328+
audience (str): The audience that this ID token is intended for.
329+
330+
Returns:
331+
str: The ID token.
332+
333+
Raises:
334+
~google.auth.exceptions.DefaultCredentialsError:
335+
If metadata server doesn't exist and no valid service account
336+
credentials are found.
337+
"""
338+
id_token_credentials = fetch_id_token_credentials(audience, request=request)
339+
id_token_credentials.refresh(request)
340+
return id_token_credentials.token

tests/oauth2/test_id_token.py

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
from google.auth import environment_vars
2222
from google.auth import exceptions
2323
from google.auth import transport
24-
import google.auth.compute_engine._metadata
2524
from google.oauth2 import id_token
2625
from google.oauth2 import service_account
2726

2827
SERVICE_ACCOUNT_FILE = os.path.join(
2928
os.path.dirname(__file__), "../data/service_account.json"
3029
)
30+
ID_TOKEN_AUDIENCE = "https://pubsub.googleapis.com"
3131

3232

3333
def make_request(status, data=None):
@@ -201,93 +201,111 @@ def test_verify_firebase_token_clock_skew(verify_token):
201201
)
202202

203203

204-
def test_fetch_id_token_from_metadata_server(monkeypatch):
204+
def test_fetch_id_token_credentials_optional_request(monkeypatch):
205205
monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
206206

207-
def mock_init(self, request, audience, use_metadata_identity_endpoint):
208-
assert use_metadata_identity_endpoint
209-
self.token = "id_token"
210-
207+
# Test a request object is created if not provided
211208
with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True):
212-
with mock.patch.multiple(
213-
google.auth.compute_engine.IDTokenCredentials,
214-
__init__=mock_init,
215-
refresh=mock.Mock(),
209+
with mock.patch(
210+
"google.auth.compute_engine.IDTokenCredentials.__init__", return_value=None
216211
):
217-
request = mock.Mock()
218-
token = id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
219-
assert token == "id_token"
212+
with mock.patch(
213+
"google.auth.transport.requests.Request.__init__", return_value=None
214+
) as mock_request:
215+
id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
216+
mock_request.assert_called()
220217

221218

222-
def test_fetch_id_token_from_explicit_cred_json_file(monkeypatch):
223-
monkeypatch.setenv(environment_vars.CREDENTIALS, SERVICE_ACCOUNT_FILE)
219+
def test_fetch_id_token_credentials_from_metadata_server(monkeypatch):
220+
monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
221+
222+
mock_req = mock.Mock()
223+
224+
with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True):
225+
with mock.patch(
226+
"google.auth.compute_engine.IDTokenCredentials.__init__", return_value=None
227+
) as mock_init:
228+
id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE, request=mock_req)
229+
mock_init.assert_called_once_with(
230+
mock_req, ID_TOKEN_AUDIENCE, use_metadata_identity_endpoint=True
231+
)
224232

225-
def mock_refresh(self, request):
226-
self.token = "id_token"
227233

228-
with mock.patch.object(service_account.IDTokenCredentials, "refresh", mock_refresh):
229-
request = mock.Mock()
230-
token = id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
231-
assert token == "id_token"
234+
def test_fetch_id_token_credentials_from_explicit_cred_json_file(monkeypatch):
235+
monkeypatch.setenv(environment_vars.CREDENTIALS, SERVICE_ACCOUNT_FILE)
236+
237+
cred = id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
238+
assert isinstance(cred, service_account.IDTokenCredentials)
239+
assert cred._target_audience == ID_TOKEN_AUDIENCE
232240

233241

234-
def test_fetch_id_token_no_cred_exists(monkeypatch):
242+
def test_fetch_id_token_credentials_no_cred_exists(monkeypatch):
235243
monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
236244

237245
with mock.patch(
238246
"google.auth.compute_engine._metadata.ping",
239247
side_effect=exceptions.TransportError(),
240248
):
241249
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
242-
request = mock.Mock()
243-
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
250+
id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
244251
assert excinfo.match(
245252
r"Neither metadata server or valid service account credentials are found."
246253
)
247254

248255
with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
249256
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
250-
request = mock.Mock()
251-
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
257+
id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
252258
assert excinfo.match(
253259
r"Neither metadata server or valid service account credentials are found."
254260
)
255261

256262

257-
def test_fetch_id_token_invalid_cred_file_type(monkeypatch):
263+
def test_fetch_id_token_credentials_invalid_cred_file_type(monkeypatch):
258264
user_credentials_file = os.path.join(
259265
os.path.dirname(__file__), "../data/authorized_user.json"
260266
)
261267
monkeypatch.setenv(environment_vars.CREDENTIALS, user_credentials_file)
262268

263269
with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
264270
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
265-
request = mock.Mock()
266-
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
271+
id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
267272
assert excinfo.match(
268273
r"Neither metadata server or valid service account credentials are found."
269274
)
270275

271276

272-
def test_fetch_id_token_invalid_json(monkeypatch):
277+
def test_fetch_id_token_credentials_invalid_json(monkeypatch):
273278
not_json_file = os.path.join(os.path.dirname(__file__), "../data/public_cert.pem")
274279
monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
275280

276281
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
277-
request = mock.Mock()
278-
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
282+
id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
279283
assert excinfo.match(
280284
r"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials."
281285
)
282286

283287

284-
def test_fetch_id_token_invalid_cred_path(monkeypatch):
288+
def test_fetch_id_token_credentials_invalid_cred_path(monkeypatch):
285289
not_json_file = os.path.join(os.path.dirname(__file__), "../data/not_exists.json")
286290
monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
287291

288292
with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
289-
request = mock.Mock()
290-
id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
293+
id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
291294
assert excinfo.match(
292295
r"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
293296
)
297+
298+
299+
def test_fetch_id_token(monkeypatch):
300+
mock_cred = mock.MagicMock()
301+
mock_cred.token = "token"
302+
303+
mock_req = mock.Mock()
304+
305+
with mock.patch(
306+
"google.oauth2.id_token.fetch_id_token_credentials", return_value=mock_cred
307+
) as mock_fetch:
308+
token = id_token.fetch_id_token(mock_req, ID_TOKEN_AUDIENCE)
309+
mock_fetch.assert_called_once_with(ID_TOKEN_AUDIENCE, request=mock_req)
310+
mock_cred.refresh.assert_called_once_with(mock_req)
311+
assert token == "token"

0 commit comments

Comments
 (0)