feat(auth): implement regional access boundary support for standalone JWT and async service accounts#17025
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements asynchronous support for Regional Access Boundary (RAB) management, including background refresh tasks and mTLS endpoint support. Key changes include the addition of _AsyncRegionalAccessBoundaryRefreshManager, updates to JWT and service account credentials to handle RAB state during cloning and serialization, and comprehensive test coverage for these new flows. However, a critical issue was identified where the refresh method in google/auth/jwt.py was renamed to _perform_refresh_token, which will break token updates as the base class expects a refresh method. Additionally, a typo was found in a test assertion URL.
…ement async refresh manager
…support Regional Access Boundary logic
…y data and config
…ager in service account credentials
be67ab6 to
7e65ee7
Compare
| new_credentials = credentials.with_claims(audience=new_audience) | ||
| """ | ||
|
|
||
| def __setstate__(self, state): |
There was a problem hiding this comment.
_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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Ok sounds good. I think we have a few open items to follow up on IIRC. Where are they being tracked?
There was a problem hiding this comment.
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:
- Updating token grant methods is tracked in Auth: Align async token endpoint request helper with google.auth.aio.transport spec #17139.
- Migrating the remaining endpoints is tracked in google-auth: Fix static module-level IAM endpoints and propagate mTLS certificates to internal helper sessions #17282.
… to prevent TypeError
|
Note to reviewers regarding mTLS transport gap: The internal helper transport adapter (self._auth_request / self._request) passed to _regional_access_boundary_utils.py is not configured with the client certificate. I have filed a tracking issue to address this transport gap: #17282 Because resolving the transport gap requires structural changes to google-auth's transport adapter initialization (requests.py and urllib3.py), we will track and merge the transport fix separately to keep the scope of this PR clean and focused on the core RAB logic. Once the transport issue is resolved, the RAB lookups in this feature will automatically execute over secure mTLS channels. |
lsirac
left a comment
There was a problem hiding this comment.
Great job!
last thing - consider validating lookup response, but can be done in a followup
daniel-sanche
left a comment
There was a problem hiding this comment.
A few comments from my first pass. I'll try to take another look soon
| self.process_regional_access_boundary_info(regional_access_boundary_info) | ||
|
|
||
| async def start_blocking_refresh_async(self, credentials, request): | ||
| """Initiates a blocking lookup of the Regional Access Boundary asynchronously. |
There was a problem hiding this comment.
This seems confusing. If it's async, is it really blocking?
There was a problem hiding this comment.
In this context, "blocking" refers to the logical flow of the request (i.e., we wait for the RAB lookup to complete before proceeding with the main request), as opposed to the background refresh mode. In the async implementation, this is achieved by awaiting the lookup, which suspends the coroutine but does not block the OS thread. The name matches the _use_blocking_regional_access_boundary_lookup configuration.
There was a problem hiding this comment.
I'd suggest trying to clarify that in the docstring
| request, url, can_retry=True, headers=None, fail_fast=False | ||
| ): | ||
| """Makes a request to the Regional Access Boundary lookup endpoint. This | ||
| function doesn't throw on response errors. |
There was a problem hiding this comment.
I find tracing some of these functions a bit confusing. There's a _lookup_regional_access_boundary_request and _lookup_regional_access_boundary_request_no_throw, but it seems like neither of them throw?
I see this is the same pattern from the sync files though, so it can probably stay as-is
There was a problem hiding this comment.
Correct, neither throws. The _no_throw version handles the low-level exceptions (timeouts, transport errors) and returns the detailed status tuple (status, data, retryable) used for retry logic. The other function acts as a wrapper that simplifies the interface by returning just data or None.
We kept this pattern to remain consistent with the sync implementation in _client.py.
| assert "x-goog-user-project" in hdrs | ||
|
|
||
| def test_build_regional_access_boundary_lookup_url(self): | ||
| def test_copy_regional_access_boundary_manager_state_and_config_with_scopes(self): |
There was a problem hiding this comment.
We should add a test for external account + SA impersonation flow. This implementation will create an Impersonated cred with a new RAB manager. I think we may be dropping state that is needed (e.g. is blocking lookup enabled?).
Please add some coverage for this + validate that we are properly propagating the relevant RAB data.
There was a problem hiding this comment.
Good catch! We were indeed dropping the blocking flag. I double checked the properties and I think that's the only thing we should propagate. Our helper method in the base class copies the blocking flag and the RAB data, but I decided not to use that, since the RAB data of the external account if it was seeded, would not necessarily be the RAB data of the impersonated account.
I resolved this by propagating the blocking RAB lookup configuration (_use_blocking_regional_access_boundary_lookup) from the outer ExternalAccountCredentials to the inner ImpersonatedCredentials during initialization in _initialize_impersonated_credentials. Also added a unit test (test_refresh_impersonation_propagates_rab_config in tests/test_external_account.py) to verify the propagation.
| return token_json["access_token"], token_expiry | ||
|
|
||
|
|
||
| def _is_email(email): |
There was a problem hiding this comment.
I think is_email is overpromising here for what we're checking. I think we can rename to is_service_account_email and implement with a simple regex check
There was a problem hiding this comment.
Done! I renamed the method and used ^[^@]+@[^@]+\.[^@]+$ which was recommended in the design for all SDKs to follow.
PR created by the Librarian CLI to initialize a release. Merging this PR will auto trigger a release. Librarian Version: v0.19.0 Language Image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:234b9d1f2ddb057ed7ac6a38db0bf8163d839c65c6cf88ade52530cddebce59e <details><summary>gapic-generator: v1.35.0</summary> ## [v1.35.0](gapic-generator-v1.34.1...gapic-generator-v1.35.0) (2026-06-11) ### Features * setup.py matches prerelease versions (#17370) ([25b857e](25b857e1)) ### Bug Fixes * require protobuf 6.33.5 to address CVE-2026-0994 (#17349) ([6642263](66422636)) </details> <details><summary>google-auth: v2.54.0</summary> ## [v2.54.0](google-auth-v2.53.0...google-auth-v2.54.0) (2026-06-11) ### Features * implement regional access boundary support for standalone JWT and async service accounts (#17025) ([35af616](35af6168)) ### Bug Fixes * configure mTLS for impersonated credentials (#17404) ([57269d5](57269d56)) * fail-fast on missing ECP config file to avoid 30s hang (#17377) ([e096127](e0961270)) * Rename the 'seed' argument for setting an initial regional access boundary for clarity (#17186) ([e5c8cf9](e5c8cf92)) * update incorrect urls in setup.py to point at monorepo vs splitrepo (#17237) ([eaed04b](eaed04ba)) </details> <details><summary>google-cloud-alloydb: v0.11.0</summary> ## [v0.11.0](google-cloud-alloydb-v0.10.0...google-cloud-alloydb-v0.11.0) (2026-06-11) ### Features * update API sources and regenerate (#17413) ([59fe7cf](59fe7cf8)) </details> <details><summary>google-cloud-biglake: v0.5.0</summary> ## [v0.5.0](google-cloud-biglake-v0.4.0...google-cloud-biglake-v0.5.0) (2026-06-11) ### Features * update API sources and regenerate (#17431) ([2e75c78](2e75c78c)) </details> <details><summary>google-cloud-ces: v0.7.0</summary> ## [v0.7.0](google-cloud-ces-v0.6.0...google-cloud-ces-v0.7.0) (2026-06-11) ### Features * update API sources and regenerate (#17413) ([59fe7cf](59fe7cf8)) </details> <details><summary>google-cloud-confidentialcomputing: v0.11.0</summary> ## [v0.11.0](google-cloud-confidentialcomputing-v0.10.0...google-cloud-confidentialcomputing-v0.11.0) (2026-06-11) ### Features * update API sources and regenerate (#17413) ([59fe7cf](59fe7cf8)) </details> <details><summary>google-cloud-modelarmor: v0.7.0</summary> ## [v0.7.0](google-cloud-modelarmor-v0.6.0...google-cloud-modelarmor-v0.7.0) (2026-06-11) ### Features * update API sources and regenerate (#17413) ([59fe7cf](59fe7cf8)) </details> <details><summary>google-cloud-network-services: v0.10.0</summary> ## [v0.10.0](google-cloud-network-services-v0.9.0...google-cloud-network-services-v0.10.0) (2026-06-11) ### Features * update API sources and regenerate (#17431) ([2e75c78](2e75c78c)) </details> <details><summary>google-cloud-oracledatabase: v0.6.0</summary> ## [v0.6.0](google-cloud-oracledatabase-v0.5.0...google-cloud-oracledatabase-v0.6.0) (2026-06-11) ### Features * update API sources and regenerate (#17413) ([59fe7cf](59fe7cf8)) </details> <details><summary>google-cloud-spanner: v3.68.0</summary> ## [v3.68.0](google-cloud-spanner-v3.67.0...google-cloud-spanner-v3.68.0) (2026-06-11) ### Features * add asynchronous code snippets and minor cleanup changes (#17337) ([d6aaf61](d6aaf610)) ### Performance Improvements * optimize query result decoding (#17375) ([3f70b2f](3f70b2ff)) </details> <details><summary>google-cloud-storage: v3.12.0</summary> ## [v3.12.0](google-cloud-storage-v3.11.0...google-cloud-storage-v3.12.0) (2026-06-11) ### Features * full object checksum: implement rolling checksum and verification in reads resumption strategy (#17262) ([2361ba6](2361ba6e)) * Enable full object checksum PR 1/3 : parse finalize_time and server crc32c in async object stream (#17261) ([72c7a27](72c7a272)) * full object checksum: integrate full-object checksum in AsyncMultiRangeDownloader (#17263) ([b6a85e4](b6a85e49)) </details> <details><summary>google-developer-knowledge: v0.1.0</summary> ## [v0.1.0](google-developer-knowledge-v0.0.0...google-developer-knowledge-v0.1.0) (2026-06-11) ### Features * add google-developer-knowledge (#17417) ([ca02afc](ca02afce)) </details>
This PR implements the following changes: