Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b9b8316
feat: add async support for RAB to ServiceAccountCredentials and impl…
nbayati May 7, 2026
a5b9adf
Add unit tests for the async RAB implementation.
nbayati May 7, 2026
09b9f41
fix async unit tests
nbayati May 7, 2026
59dbb2c
Update unit tests to accept both mtls and standard allowedLocations e…
nbayati May 7, 2026
be55639
test: verify iam endpoint constant resolution in mTLS environments
nbayati May 7, 2026
d38da42
refactor: introduce _after_refresh hook in Credentials base class to …
nbayati May 7, 2026
44f1110
add __setstate__ to the base RAB class for backward compatibility
nbayati May 8, 2026
aeb426c
Implement RAB support for jwt credentials
nbayati May 8, 2026
6e0c00f
fix lint errors
nbayati May 11, 2026
6a7bb27
fix: preserve refresh manager type when copying RAB manager
nbayati May 11, 2026
2682925
refactor(auth): optimize RAB manager copy logic to only share boundar…
nbayati May 14, 2026
140ee7e
fix(auth): enhance client lookup robustness with defensive checks and…
nbayati May 14, 2026
ad5baca
refactor(auth): centralize async RAB lifecycle via _after_refresh bas…
nbayati May 14, 2026
529355a
feat: add pickling support for _AsyncRegionalAccessBoundaryRefreshMan…
nbayati May 14, 2026
fbe32fe
revert changes to the _token_endpoint_request_no_throw to keep PR foc…
nbayati May 14, 2026
6676fd1
fix(auth): align async client with AIO transport spec and add unit tests
nbayati May 14, 2026
832ae7b
test(auth): assert closed session safety in async RAB refresh and fix…
nbayati May 15, 2026
106e72b
docs(auth): clarify async RAB transport requirements in docstrings
nbayati May 15, 2026
7e65ee7
feat(auth): support async blocking RAB lookups and add support to asy…
nbayati May 16, 2026
9475f9e
update unit tests
nbayati May 19, 2026
ef80089
test(auth): assert RAB blocking flag and data copying on subclass cre…
nbayati May 26, 2026
bb117de
fix(auth): allow status-based retries on non-JSON RAB lookup failures
nbayati May 27, 2026
69d8300
fix: catch generic exceptions during async body streaming
nbayati May 27, 2026
4c9796f
refactor(auth): move RAB endpoints to dynamic utils to support runtim…
nbayati May 27, 2026
3177328
fix(auth): prevent sync-on-async crash in RAB manager blocking refresh
nbayati May 28, 2026
56542c4
fix(auth): skip compute engine RAB lookup for non-SA based identities
nbayati May 29, 2026
fb10a3f
fix(auth): add async before_request to _jwt_async.OnDemandCredentials…
nbayati May 29, 2026
1ef1f1f
refactor(auth): move RAB helper imports to top level
nbayati Jun 4, 2026
8087ff0
fix(auth): retry RAB lookup on timeout and transport errors in async …
nbayati Jun 4, 2026
1a5004e
fix(auth): propagate blocking RAB config to impersonated credentials
nbayati Jun 4, 2026
05c9942
fix(auth): restrict metadata email check to standard email format
nbayati Jun 4, 2026
64a6723
fix lint errors
nbayati Jun 4, 2026
9917119
test(auth): mock sleep in client retry tests to speed up execution
nbayati Jun 4, 2026
0ec659e
style: fix lint issues
nbayati Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions packages/google-auth/google/auth/_credentials_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import abc
import inspect

from google.auth import _regional_access_boundary_utils
from google.auth import credentials


Expand Down Expand Up @@ -64,8 +65,28 @@ async def before_request(self, request, method, url, headers):
await self.refresh(request)
else:
self.refresh(request)

if inspect.iscoroutinefunction(self._after_refresh):
await self._after_refresh(request, method, url, headers)
else:
self._after_refresh(request, method, url, headers)

self.apply(headers)

def _after_refresh(self, request, method, url, headers):
"""Hook for subclasses to perform actions after refresh but before
applying credentials to headers.

Args:
request (google.auth.transport.Request): The object used to make
HTTP requests.
method (str): The request's HTTP method or the RPC method being
invoked.
url (str): The request's URI or the RPC service's URI.
headers (Mapping[str, str]): The request's headers.
"""
pass


class CredentialsWithQuotaProject(credentials.CredentialsWithQuotaProject):
"""Abstract base for credentials supporting ``with_quota_project`` factory"""
Expand Down Expand Up @@ -169,3 +190,74 @@ def with_scopes_if_required(credentials, scopes):

class Signing(credentials.Signing, metaclass=abc.ABCMeta):
"""Interface for credentials that can cryptographically sign messages."""


class CredentialsWithRegionalAccessBoundary(
Credentials, credentials.CredentialsWithRegionalAccessBoundary
):
"""Async base for credentials supporting regional access boundary configuration."""

def __init__(self):
super().__init__()
self._rab_manager.refresh_manager = (
_regional_access_boundary_utils._AsyncRegionalAccessBoundaryRefreshManager()
)

def __setstate__(self, state):
super().__setstate__(state)
self._rab_manager.refresh_manager = (
_regional_access_boundary_utils._AsyncRegionalAccessBoundaryRefreshManager()
)

async def _after_refresh(self, request, method, url, headers):
"""Triggers the Regional Access Boundary lookup asynchronously if necessary."""
await self._maybe_start_regional_access_boundary_refresh_async(request, url)

async def _maybe_start_regional_access_boundary_refresh_async(self, request, url):
"""Starts a background refresh or performs a blocking refresh asynchronously.

Args:
request (google.auth.aio.transport.Request): The object used to make
HTTP requests.
url (str): The URL of the request.
"""
# Do not perform a lookup if the request is for a regional endpoint.
if self._is_regional_endpoint(url):
return

# A refresh is only needed if the feature is enabled.
if not self._is_regional_access_boundary_lookup_required():
return

# Trigger background or blocking refresh if needed.
await self._rab_manager.maybe_start_refresh_async(self, request)

async def _lookup_regional_access_boundary(self, request, fail_fast=False):
"""Calls the Regional Access Boundary lookup API asynchronously.

Args:
request (google.auth.aio.transport.Request): The object used to make
HTTP requests.
fail_fast (bool): Whether the lookup should fail fast (short timeout, no retries).

Returns:
Optional[Dict[str, str]]: The Regional Access Boundary information
returned by the lookup API, or None if the lookup failed.
"""
url_builder = self._build_regional_access_boundary_lookup_url
if inspect.iscoroutinefunction(url_builder):
url = await url_builder(request=request)
else:
url = url_builder(request=request)

if not url:
return None

headers = {}
self._apply(headers)

from google.oauth2 import _client_async

return await _client_async._lookup_regional_access_boundary(
request, url, headers=headers, fail_fast=fail_fast
)
2 changes: 2 additions & 0 deletions packages/google-auth/google/auth/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from google.auth import exceptions


DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"

# _BASE_LOGGER_NAME is the base logger for all google-based loggers.
_BASE_LOGGER_NAME = "google"

Expand Down
18 changes: 17 additions & 1 deletion packages/google-auth/google/auth/_jwt_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
"""

from google.auth import _credentials_async
from google.auth import _helpers
from google.auth import _regional_access_boundary_utils
from google.auth import jwt


Expand Down Expand Up @@ -91,7 +93,9 @@ def decode(token, certs=None, verify=True, audience=None):


class Credentials(
jwt.Credentials, _credentials_async.Signing, _credentials_async.Credentials
jwt.Credentials,
_credentials_async.Signing,
_credentials_async.CredentialsWithRegionalAccessBoundary,
):
"""Credentials that use a JWT as the bearer token.

Expand Down Expand Up @@ -142,6 +146,14 @@ class Credentials(
new_credentials = credentials.with_claims(audience=new_audience)
"""

def __setstate__(self, state):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_jwt_async.OnDemandCredentials inherits from jwt.OnDemandCredentials first. Because jwt.OnDemandCredentials implements before_request synchronously, Python's MRO resolves before_request to the synchronous parent, shadowing the async base.
When called via the experimental async HTTP transport (_aiohttp_requests.py), which has no initialization type-checking and explicitly awaits before_request(...), the synchronous method executes, returns None, and the event loop attempts to await None, raising:
TypeError: object NoneType can't be used in 'await' expression

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. I've resolved this by adding an async def before_request override to the OnDemandCredentials class that delegates to the parent class, and updated the corresponding async test cases to properly await the call.

As a side note, I have OnDemandCredentials on my radar as a class that might need to support RAB in the future, but I have decided to keep it out of scope for this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok sounds good. I think we have a few open items to follow up on IIRC. Where are they being tracked?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tracking the additional credential types in the RAB google sheet tracker for a follow up release.
For the items that weren't RAB specific, I have made a github issue:

"""Restores the credential state and ensures the async refresh manager is attached."""
super().__setstate__(state)

self._rab_manager.refresh_manager = (
_regional_access_boundary_utils._AsyncRegionalAccessBoundaryRefreshManager()
)


class OnDemandCredentials(
jwt.OnDemandCredentials, _credentials_async.Signing, _credentials_async.Credentials
Expand All @@ -162,3 +174,7 @@ class OnDemandCredentials(

.. _grpc: http://www.grpc.io/
"""

@_helpers.copy_docstring(jwt.OnDemandCredentials)
async def before_request(self, request, method, url, headers):
super(OnDemandCredentials, self).before_request(request, method, url, headers)
Loading
Loading