Skip to content

Commit 36015c6

Browse files
committed
Support self-signed jwt and refactor refresh to handle refreshing trust boundary in the base class.
1 parent 1d26a76 commit 36015c6

File tree

6 files changed

+108
-54
lines changed

6 files changed

+108
-54
lines changed

google/auth/_helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ def get_bool_from_env(variable_name, default=False):
295295
(case-insensitive) rules:
296296
- "true", "1" are considered true.
297297
- "false", "0" are considered false.
298+
Any other values will raise an exception.
298299
299300
Args:
300301
variable_name (str): The name of the environment variable.

google/auth/compute_engine/credentials.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def __init__(
9898
def _metric_header_for_usage(self):
9999
return metrics.CRED_TYPE_SA_MDS
100100

101-
def refresh(self, request):
101+
def _refresh_token(self, request):
102102
"""Refresh the access token and scopes.
103103
104104
Args:
@@ -119,8 +119,6 @@ def refresh(self, request):
119119
new_exc = exceptions.RefreshError(caught_exc)
120120
raise new_exc from caught_exc
121121

122-
self._refresh_trust_boundary(request)
123-
124122
def _build_trust_boundary_lookup_url(self):
125123
"""Builds and returns the URL for the trust boundary lookup API for GCE."""
126124
# If the service account email is 'default', we need to get the

google/auth/credentials.py

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,20 @@ def with_universe_domain(self, universe_domain):
292292
class CredentialsWithTrustBoundary(Credentials):
293293
"""Abstract base for credentials supporting ``with_trust_boundary`` factory"""
294294

295+
@abc.abstractmethod
296+
def _refresh_token(self, request):
297+
"""Refreshes the access token.
298+
299+
Args:
300+
request (google.auth.transport.Request): The object used to make
301+
HTTP requests.
302+
303+
Raises:
304+
google.auth.exceptions.RefreshError: If the credentials could
305+
not be refreshed.
306+
"""
307+
raise NotImplementedError("_refresh_token must be implemented")
308+
295309
def with_trust_boundary(self, trust_boundary):
296310
"""Returns a copy of these credentials with a modified trust boundary.
297311
@@ -306,6 +320,29 @@ def with_trust_boundary(self, trust_boundary):
306320
"""
307321
raise NotImplementedError("This credential does not support trust boundaries.")
308322

323+
def _is_trust_boundary_lookup_required(self):
324+
"""Checks if a trust boundary lookup is required.
325+
326+
A lookup is required if the feature is enabled via an environment
327+
variable, the universe domain is supported, and a no-op boundary
328+
is not already cached.
329+
330+
Returns:
331+
bool: True if a trust boundary lookup is required, False otherwise.
332+
"""
333+
# 1. Check if the feature is enabled via environment variable.
334+
if not _helpers.get_bool_from_env(
335+
environment_vars.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED, default=False
336+
):
337+
return False
338+
339+
# 2. Skip trust boundary flow for non-default universe domains.
340+
if self.universe_domain != DEFAULT_UNIVERSE_DOMAIN:
341+
return False
342+
343+
# 3. Do not trigger refresh if credential has a cached no-op trust boundary.
344+
return not self._has_no_op_trust_boundary()
345+
309346
def _get_trust_boundary_header(self):
310347
if self._trust_boundary is not None:
311348
if self._has_no_op_trust_boundary():
@@ -320,6 +357,15 @@ def apply(self, headers, token=None):
320357
super().apply(headers, token)
321358
headers.update(self._get_trust_boundary_header())
322359

360+
def refresh(self, request):
361+
"""Refreshes the access token and the trust boundary.
362+
363+
This method calls the subclass's token refresh logic and then
364+
refreshes the trust boundary if applicable.
365+
"""
366+
self._refresh_token(request)
367+
self._refresh_trust_boundary(request)
368+
323369
def _refresh_trust_boundary(self, request):
324370
"""Triggers a refresh of the trust boundary and updates the cache if necessary.
325371
@@ -331,12 +377,10 @@ def _refresh_trust_boundary(self, request):
331377
google.auth.exceptions.RefreshError: If the trust boundary could
332378
not be refreshed and no cached value is available.
333379
"""
334-
# Do not trigger refresh if credential has a cached no-op trust boundary.
335-
if self._has_no_op_trust_boundary():
380+
if not self._is_trust_boundary_lookup_required():
336381
return
337-
new_trust_boundary = {}
338382
try:
339-
new_trust_boundary = self._lookup_trust_boundary(request)
383+
self._trust_boundary = self._lookup_trust_boundary(request)
340384
except exceptions.RefreshError as error:
341385
# If the call to the lookup API failed, check if there is a trust boundary
342386
# already cached. If there is, do nothing. If not, then throw the error.
@@ -347,8 +391,6 @@ def _refresh_trust_boundary(self, request):
347391
"Using cached trust boundary due to refresh error: %s", error
348392
)
349393
return
350-
else:
351-
self._trust_boundary = new_trust_boundary
352394

353395
def _lookup_trust_boundary(self, request):
354396
"""Calls the trust boundary lookup API to refresh the trust boundary cache.
@@ -366,24 +408,13 @@ def _lookup_trust_boundary(self, request):
366408
"""
367409
from google.oauth2 import _client
368410

369-
# Verify the trust boundary feature flag is enabled.
370-
if not _helpers.get_bool_from_env(
371-
environment_vars.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED, default=False
372-
):
373-
# Skip the lookup and return early if it's not explicitly enabled.
374-
return None
375-
376-
# Skip trust boundary flow for non-gdu universe domain.
377-
if self.universe_domain != DEFAULT_UNIVERSE_DOMAIN:
378-
return None
379-
380411
url = self._build_trust_boundary_lookup_url()
381412
if not url:
382413
raise exceptions.InvalidValue("Failed to build trust boundary lookup URL.")
383414

384415
headers = {}
385416
self.apply(headers)
386-
return _client.lookup_trust_boundary(request, url, headers=headers)
417+
return _client._lookup_trust_boundary(request, url, headers=headers)
387418

388419
@abc.abstractmethod
389420
def _build_trust_boundary_lookup_url(self):

google/auth/impersonated_credentials.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,7 @@ def __init__(
264264
def _metric_header_for_usage(self):
265265
return metrics.CRED_TYPE_SA_IMPERSONATE
266266

267-
@_helpers.copy_docstring(credentials.Credentials)
268-
def refresh(self, request):
269-
self._update_token(request)
270-
self._refresh_trust_boundary(request)
271-
272-
def _update_token(self, request):
267+
def _refresh_token(self, request):
273268
"""Updates credentials with a new access_token representing
274269
the impersonated account.
275270

google/oauth2/_client.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ def refresh_grant(
515515
return _handle_refresh_grant_response(response_data, refresh_token)
516516

517517

518-
def lookup_trust_boundary(request, url, headers=None):
518+
def _lookup_trust_boundary(request, url, headers=None):
519519
"""Implements the global lookup of a credential trust boundary.
520520
For the lookup, we send a request to the global lookup endpoint and then
521521
parse the response. Service account credentials, workload identity
@@ -556,9 +556,7 @@ def lookup_trust_boundary(request, url, headers=None):
556556
return response_data
557557

558558

559-
def _lookup_trust_boundary_request(
560-
request, url, can_retry=True, headers=None, **kwargs
561-
):
559+
def _lookup_trust_boundary_request(request, url, can_retry=True, headers=None):
562560
"""Makes a request to the trust boundary lookup endpoint.
563561
564562
Args:
@@ -567,13 +565,6 @@ def _lookup_trust_boundary_request(
567565
url (str): The trust boundary lookup url.
568566
can_retry (bool): Enable or disable request retry behavior. Defaults to true.
569567
headers (Optional[Mapping[str, str]]): The headers for the request.
570-
kwargs: Additional arguments passed on to the request method. The
571-
kwargs will be passed to `requests.request` method, see:
572-
https://docs.python-requests.org/en/latest/api/#requests.request.
573-
For example, you can use `cert=("cert_pem_path", "key_pem_path")`
574-
to set up client side SSL certificate, and use
575-
`verify="ca_bundle_path"` to set up the CA certificates for sever
576-
side SSL certificate verification.
577568
578569
Returns:
579570
Mapping[str, str]: The JSON-decoded response data.
@@ -583,18 +574,14 @@ def _lookup_trust_boundary_request(
583574
an error.
584575
"""
585576
response_status_ok, response_data, retryable_error = (
586-
_lookup_trust_boundary_request_no_throw(
587-
request, url, can_retry, headers, **kwargs
588-
)
577+
_lookup_trust_boundary_request_no_throw(request, url, can_retry, headers)
589578
)
590579
if not response_status_ok:
591580
_handle_error_response(response_data, retryable_error)
592581
return response_data
593582

594583

595-
def _lookup_trust_boundary_request_no_throw(
596-
request, url, can_retry=True, headers=None, **kwargs
597-
):
584+
def _lookup_trust_boundary_request_no_throw(request, url, can_retry=True, headers=None):
598585
"""Makes a request to the trust boundary lookup endpoint. This
599586
function doesn't throw on response errors.
600587
@@ -604,13 +591,6 @@ def _lookup_trust_boundary_request_no_throw(
604591
url (str): The trust boundary lookup url.
605592
can_retry (bool): Enable or disable request retry behavior. Defaults to true.
606593
headers (Optional[Mapping[str, str]]): The headers for the request.
607-
kwargs: Additional arguments passed on to the request method. The
608-
kwargs will be passed to `requests.request` method, see:
609-
https://docs.python-requests.org/en/latest/api/#requests.request.
610-
For example, you can use `cert=("cert_pem_path", "key_pem_path")`
611-
to set up client side SSL certificate, and use
612-
`verify="ca_bundle_path"` to set up the CA certificates for sever
613-
side SSL certificate verification.
614594
615595
Returns:
616596
Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
@@ -624,7 +604,7 @@ def _lookup_trust_boundary_request_no_throw(
624604

625605
retries = _exponential_backoff.ExponentialBackoff()
626606
for _ in retries:
627-
response = request(method="GET", url=url, headers=headers, **kwargs)
607+
response = request(method="GET", url=url, headers=headers)
628608
response_body = (
629609
response.data.decode("utf-8")
630610
if hasattr(response.data, "decode")

google/oauth2/service_account.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ def _metric_header_for_usage(self):
436436
return metrics.CRED_TYPE_SA_ASSERTION
437437

438438
@_helpers.copy_docstring(credentials.Credentials)
439-
def refresh(self, request):
439+
def _refresh_token(self, request):
440440
if self._always_use_jwt_access and not self._jwt_credentials:
441441
# If self signed jwt should be used but jwt credential is not
442442
# created, try to create one with scopes
@@ -461,7 +461,50 @@ def refresh(self, request):
461461
)
462462
self.token = access_token
463463
self.expiry = expiry
464-
self._refresh_trust_boundary(request)
464+
465+
def refresh(self, request):
466+
"""Refreshes the credential's access token.
467+
468+
This method is overridden to provide special handling for credentials that
469+
use a self-signed JWT and have a trust boundary configured. In this
470+
scenario, it first generates a temporary, IAM-specific self-signed JWT
471+
to perform the trust boundary lookup, and then generates the final
472+
self-signed JWT for the target API.
473+
474+
For all other cases, it falls back to the standard refresh behavior
475+
from the parent class.
476+
477+
Args:
478+
request (google.auth.transport.Request): The object used to make
479+
HTTP requests.
480+
481+
Raises:
482+
google.auth.exceptions.RefreshError: If the credentials could
483+
not be refreshed.
484+
"""
485+
# Determine if we're going to use a self-signed JWT.
486+
use_ssjwt = self._use_self_signed_jwt()
487+
488+
if use_ssjwt and self._is_trust_boundary_lookup_required():
489+
# Special case: self-signed JWT with trust boundary.
490+
# 1. Create a temporary self-signed JWT for the IAM API.
491+
iam_audience = "https://iamcredentials.{}/".format(self._universe_domain)
492+
iam_jwt_creds = jwt.Credentials.from_signing_credentials(self, iam_audience)
493+
iam_jwt_creds.refresh(request)
494+
495+
# 2. Use this JWT to perform the trust boundary lookup.
496+
# We temporarily set self.token for the base lookup method.
497+
# The base lookup method will call self.apply() which adds the
498+
# authorization header.
499+
with _helpers.update_property(self, "token", iam_jwt_creds.token.decode()):
500+
# This will call _lookup_trust_boundary and set self._trust_boundary
501+
self._refresh_trust_boundary(request)
502+
503+
# 3. Now, refresh the original self-signed JWT for the target API.
504+
self._refresh_token(request)
505+
else:
506+
# For all other cases, use the standard refresh mechanism.
507+
super(Credentials, self).refresh(request)
465508

466509
def _create_self_signed_jwt(self, audience):
467510
"""Create a self-signed JWT from the credentials if requirements are met.
@@ -798,6 +841,12 @@ def with_trust_boundary(self, trust_boundary):
798841
cred._trust_boundary = trust_boundary
799842
return cred
800843

844+
def _refresh_token(self, request):
845+
"""Not used by this class, which overrides refresh() directly."""
846+
# This is required to satisfy the abstract base class, but this
847+
# class's refresh() method is called directly and does not use this.
848+
pass
849+
801850
def _build_trust_boundary_lookup_url(self):
802851
"""Builds and returns the URL for the trust boundary lookup API.
803852

0 commit comments

Comments
 (0)