Skip to content

Commit 93d681e

Browse files
fix: fix id_token iam endpoint for non-gdu service credentials (#1506)
* fix: fix id_token iam endpoint for non-gdu service credentials * chore: address comments
1 parent 089206e commit 93d681e

File tree

8 files changed

+77
-37
lines changed

8 files changed

+77
-37
lines changed

google/auth/iam.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,23 @@
2727
from google.auth import crypt
2828
from google.auth import exceptions
2929

30-
_IAM_API_ROOT_URI = "https://iamcredentials.googleapis.com/v1"
31-
_SIGN_BLOB_URI = _IAM_API_ROOT_URI + "/projects/-/serviceAccounts/{}:signBlob?alt=json"
30+
31+
_IAM_SCOPE = ["https://www.googleapis.com/auth/iam"]
32+
33+
_IAM_ENDPOINT = (
34+
"https://iamcredentials.googleapis.com/v1/projects/-"
35+
+ "/serviceAccounts/{}:generateAccessToken"
36+
)
37+
38+
_IAM_SIGN_ENDPOINT = (
39+
"https://iamcredentials.googleapis.com/v1/projects/-"
40+
+ "/serviceAccounts/{}:signBlob"
41+
)
42+
43+
_IAM_IDTOKEN_ENDPOINT = (
44+
"https://iamcredentials.googleapis.com/v1/"
45+
+ "projects/-/serviceAccounts/{}:generateIdToken"
46+
)
3247

3348

3449
class Signer(crypt.Signer):
@@ -67,7 +82,7 @@ def _make_signing_request(self, message):
6782
message = _helpers.to_bytes(message)
6883

6984
method = "POST"
70-
url = _SIGN_BLOB_URI.format(self._service_account_email)
85+
url = _IAM_SIGN_ENDPOINT.format(self._service_account_email)
7186
headers = {"Content-Type": "application/json"}
7287
body = json.dumps(
7388
{"payload": base64.b64encode(message).decode("utf-8")}

google/auth/impersonated_credentials.py

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,32 +34,15 @@
3434
from google.auth import _helpers
3535
from google.auth import credentials
3636
from google.auth import exceptions
37+
from google.auth import iam
3738
from google.auth import jwt
3839
from google.auth import metrics
3940

40-
_IAM_SCOPE = ["https://www.googleapis.com/auth/iam"]
41-
42-
_IAM_ENDPOINT = (
43-
"https://iamcredentials.googleapis.com/v1/projects/-"
44-
+ "/serviceAccounts/{}:generateAccessToken"
45-
)
46-
47-
_IAM_SIGN_ENDPOINT = (
48-
"https://iamcredentials.googleapis.com/v1/projects/-"
49-
+ "/serviceAccounts/{}:signBlob"
50-
)
51-
52-
_IAM_IDTOKEN_ENDPOINT = (
53-
"https://iamcredentials.googleapis.com/v1/"
54-
+ "projects/-/serviceAccounts/{}:generateIdToken"
55-
)
5641

5742
_REFRESH_ERROR = "Unable to acquire impersonated credentials"
5843

5944
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
6045

61-
_DEFAULT_TOKEN_URI = "https://oauth2.googleapis.com/token"
62-
6346

6447
def _make_iam_token_request(
6548
request, principal, headers, body, iam_endpoint_override=None
@@ -83,7 +66,7 @@ def _make_iam_token_request(
8366
`iamcredentials.googleapis.com` is not enabled or the
8467
`Service Account Token Creator` is not assigned
8568
"""
86-
iam_endpoint = iam_endpoint_override or _IAM_ENDPOINT.format(principal)
69+
iam_endpoint = iam_endpoint_override or iam._IAM_ENDPOINT.format(principal)
8770

8871
body = json.dumps(body).encode("utf-8")
8972

@@ -225,7 +208,9 @@ def __init__(
225208
# added to refresh correctly. User credentials cannot have
226209
# their original scopes modified.
227210
if isinstance(self._source_credentials, credentials.Scoped):
228-
self._source_credentials = self._source_credentials.with_scopes(_IAM_SCOPE)
211+
self._source_credentials = self._source_credentials.with_scopes(
212+
iam._IAM_SCOPE
213+
)
229214
# If the source credential is service account and self signed jwt
230215
# is needed, we need to create a jwt credential inside it
231216
if (
@@ -290,7 +275,7 @@ def _update_token(self, request):
290275
def sign_bytes(self, message):
291276
from google.auth.transport.requests import AuthorizedSession
292277

293-
iam_sign_endpoint = _IAM_SIGN_ENDPOINT.format(self._target_principal)
278+
iam_sign_endpoint = iam._IAM_SIGN_ENDPOINT.format(self._target_principal)
294279

295280
body = {
296281
"payload": base64.b64encode(message).decode("utf-8"),
@@ -425,7 +410,7 @@ def with_quota_project(self, quota_project_id):
425410
def refresh(self, request):
426411
from google.auth.transport.requests import AuthorizedSession
427412

428-
iam_sign_endpoint = _IAM_IDTOKEN_ENDPOINT.format(
413+
iam_sign_endpoint = iam._IAM_IDTOKEN_ENDPOINT.format(
429414
self._target_credentials.signer_email
430415
)
431416

google/oauth2/_client.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@
3939
_JSON_CONTENT_TYPE = "application/json"
4040
_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
4141
_REFRESH_GRANT_TYPE = "refresh_token"
42-
_IAM_IDTOKEN_ENDPOINT = (
43-
"https://iamcredentials.googleapis.com/v1/"
44-
+ "projects/-/serviceAccounts/{}:generateIdToken"
45-
)
4642

4743

4844
def _handle_error_response(response_data, retryable_error):
@@ -328,12 +324,15 @@ def jwt_grant(request, token_uri, assertion, can_retry=True):
328324
return access_token, expiry, response_data
329325

330326

331-
def call_iam_generate_id_token_endpoint(request, signer_email, audience, access_token):
327+
def call_iam_generate_id_token_endpoint(
328+
request, iam_id_token_endpoint, signer_email, audience, access_token
329+
):
332330
"""Call iam.generateIdToken endpoint to get ID token.
333331
334332
Args:
335333
request (google.auth.transport.Request): A callable used to make
336334
HTTP requests.
335+
iam_id_token_endpoint (str): The IAM ID token endpoint to use.
337336
signer_email (str): The signer email used to form the IAM
338337
generateIdToken endpoint.
339338
audience (str): The audience for the ID token.
@@ -346,7 +345,7 @@ def call_iam_generate_id_token_endpoint(request, signer_email, audience, access_
346345

347346
response_data = _token_endpoint_request(
348347
request,
349-
_IAM_IDTOKEN_ENDPOINT.format(signer_email),
348+
iam_id_token_endpoint.format(signer_email),
350349
body,
351350
access_token=access_token,
352351
use_json=True,

google/oauth2/service_account.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
from google.auth import _service_account_info
7878
from google.auth import credentials
7979
from google.auth import exceptions
80+
from google.auth import iam
8081
from google.auth import jwt
8182
from google.auth import metrics
8283
from google.oauth2 import _client
@@ -595,8 +596,11 @@ def __init__(
595596
self._universe_domain = credentials.DEFAULT_UNIVERSE_DOMAIN
596597
else:
597598
self._universe_domain = universe_domain
599+
self._iam_id_token_endpoint = iam._IAM_IDTOKEN_ENDPOINT.replace(
600+
"googleapis.com", self._universe_domain
601+
)
598602

599-
if universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
603+
if self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
600604
self._use_iam_endpoint = True
601605

602606
if additional_claims is not None:
@@ -792,6 +796,7 @@ def _refresh_with_iam_endpoint(self, request):
792796
jwt_credentials.refresh(request)
793797
self.token, self.expiry = _client.call_iam_generate_id_token_endpoint(
794798
request,
799+
self._iam_id_token_endpoint,
795800
self.signer_email,
796801
self._target_audience,
797802
jwt_credentials.token.decode(),

system_tests/secrets.tar.enc

0 Bytes
Binary file not shown.

tests/compute_engine/test_credentials.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ def test_with_target_audience_integration(self):
499499
responses.add(
500500
responses.POST,
501501
"https://iamcredentials.googleapis.com/v1/projects/-/"
502-
"serviceAccounts/[email protected]:signBlob?alt=json",
502+
"serviceAccounts/[email protected]:signBlob",
503503
status=200,
504504
content_type="application/json",
505505
json={"keyId": "some-key-id", "signedBlob": signature},
@@ -657,7 +657,7 @@ def test_with_quota_project_integration(self):
657657
responses.add(
658658
responses.POST,
659659
"https://iamcredentials.googleapis.com/v1/projects/-/"
660-
"serviceAccounts/[email protected]:signBlob?alt=json",
660+
"serviceAccounts/[email protected]:signBlob",
661661
status=200,
662662
content_type="application/json",
663663
json={"keyId": "some-key-id", "signedBlob": signature},

tests/oauth2/test__client.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from google.auth import _helpers
2525
from google.auth import crypt
2626
from google.auth import exceptions
27+
from google.auth import iam
2728
from google.auth import jwt
2829
from google.auth import transport
2930
from google.oauth2 import _client
@@ -318,7 +319,11 @@ def test_call_iam_generate_id_token_endpoint():
318319
request = make_request({"token": id_token})
319320

320321
token, expiry = _client.call_iam_generate_id_token_endpoint(
321-
request, "fake_email", "fake_audience", "fake_access_token"
322+
request,
323+
iam._IAM_IDTOKEN_ENDPOINT,
324+
"fake_email",
325+
"fake_audience",
326+
"fake_access_token",
322327
)
323328

324329
assert (
@@ -351,7 +356,11 @@ def test_call_iam_generate_id_token_endpoint_no_id_token():
351356

352357
with pytest.raises(exceptions.RefreshError) as excinfo:
353358
_client.call_iam_generate_id_token_endpoint(
354-
request, "fake_email", "fake_audience", "fake_access_token"
359+
request,
360+
iam._IAM_IDTOKEN_ENDPOINT,
361+
"fake_email",
362+
"fake_audience",
363+
"fake_access_token",
355364
)
356365
assert excinfo.match("No ID token in response")
357366

tests/oauth2/test_service_account.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from google.auth import _helpers
2323
from google.auth import crypt
2424
from google.auth import exceptions
25+
from google.auth import iam
2526
from google.auth import jwt
2627
from google.auth import transport
2728
from google.auth.credentials import DEFAULT_UNIVERSE_DOMAIN
@@ -771,10 +772,36 @@ def test_refresh_iam_flow(self, call_iam_generate_id_token_endpoint):
771772
)
772773
request = mock.Mock()
773774
credentials.refresh(request)
774-
req, signer_email, target_audience, access_token = call_iam_generate_id_token_endpoint.call_args[
775+
req, iam_endpoint, signer_email, target_audience, access_token = call_iam_generate_id_token_endpoint.call_args[
775776
0
776777
]
777778
assert req == request
779+
assert iam_endpoint == iam._IAM_IDTOKEN_ENDPOINT
780+
assert signer_email == "[email protected]"
781+
assert target_audience == "https://example.com"
782+
decoded_access_token = jwt.decode(access_token, verify=False)
783+
assert decoded_access_token["scope"] == "https://www.googleapis.com/auth/iam"
784+
785+
@mock.patch(
786+
"google.oauth2._client.call_iam_generate_id_token_endpoint", autospec=True
787+
)
788+
def test_refresh_iam_flow_non_gdu(self, call_iam_generate_id_token_endpoint):
789+
credentials = self.make_credentials(universe_domain="fake-universe")
790+
token = "id_token"
791+
call_iam_generate_id_token_endpoint.return_value = (
792+
token,
793+
_helpers.utcnow() + datetime.timedelta(seconds=500),
794+
)
795+
request = mock.Mock()
796+
credentials.refresh(request)
797+
req, iam_endpoint, signer_email, target_audience, access_token = call_iam_generate_id_token_endpoint.call_args[
798+
0
799+
]
800+
assert req == request
801+
assert (
802+
iam_endpoint
803+
== "https://iamcredentials.fake-universe/v1/projects/-/serviceAccounts/{}:generateIdToken"
804+
)
778805
assert signer_email == "[email protected]"
779806
assert target_audience == "https://example.com"
780807
decoded_access_token = jwt.decode(access_token, verify=False)

0 commit comments

Comments
 (0)