Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit 45a3682

Browse files
authored
feat: Async Refresh for Regional Access Boundaries (#1880)
* Added async logic for RAB refresh/ Now self-signed JWT are in RAB scope. Updated tests. * Lint fixes. * Url for RAB to include only GDU. Only 500, 502, 503 and 504 lookup errors are now retryable. * Addressed some PR comments. * Made cooldown into an AtomicReference removed the synchronized reference. * Exclude user-seeded RAB and stale RAB error handling * Removed the user-seeded and stale RAB errors. Soft-expiry now implemented. Nit fixes. * getRAB() made package-private * RAB headers now uses guava.ImmutableMap. * Self-signed JWT now carry RAB headers, unit test updated. RABManager now considers expiryTime as opposed to start time. Minor fixes. * forkJoin.common pool is no longer used -> now using the executor (in case of async token refresh) or instantiate a new thread. * Removed Env variables gating RAB. * Test fix to account for RAB refresh. * self-signed RAB refresh moved to a helper. Added comments to explain thread vs CompletebleFuture.runAsync. * Added doc for cooldown logic. * RAB staleness due to Oauth caching and flaky tests fixed. * Fixed -> 1. getRequestMetadata calls refreshTrustBoundaryIfExpired without a try catch block. 2. Lock acquiral for refreshFuture.compareAndSet(null, future) now fixed. 3. Oauth2Credentials isn't caching RAB which was earlier leading to serialization issues. * Using per-instance clocks. * Reintroduced env variables. * lint fix. * Added a readObject metod which reinitializes the clock to Clock.system upon deserialization. * Changed to guava future. * Executor is no longer used to execute RAB refresh. * Addressed Lawrence's comments on RAB PR.
1 parent 4b2bdf8 commit 45a3682

29 files changed

Lines changed: 1652 additions & 719 deletions

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@ target/
1616
.vscode/
1717

1818
# MacOS
19-
.DS_Store
19+
.DS_Store
20+
21+
# Conductor and Gemini
22+
conductor/
23+
Gemini/

oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
* <p>These credentials use the IAM API to sign data. See {@link #sign(byte[])} for more details.
8484
*/
8585
public class ComputeEngineCredentials extends GoogleCredentials
86-
implements ServiceAccountSigner, IdTokenProvider, TrustBoundaryProvider {
86+
implements ServiceAccountSigner, IdTokenProvider, RegionalAccessBoundaryProvider {
8787

8888
static final String METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE =
8989
"Empty content from metadata token server request.";
@@ -386,11 +386,7 @@ public AccessToken refreshAccessToken() throws IOException {
386386
int expiresInSeconds =
387387
OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
388388
long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000;
389-
AccessToken newAccessToken = new AccessToken(accessToken, new Date(expiresAtMilliseconds));
390-
391-
refreshTrustBoundary(newAccessToken, transportFactory);
392-
393-
return newAccessToken;
389+
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
394390
}
395391

396392
/**
@@ -694,6 +690,11 @@ public static Builder newBuilder() {
694690
*
695691
* @throws RuntimeException if the default service account cannot be read
696692
*/
693+
@Override
694+
HttpTransportFactory getTransportFactory() {
695+
return transportFactory;
696+
}
697+
697698
@Override
698699
// todo(#314) getAccount should not throw a RuntimeException
699700
public String getAccount() {
@@ -709,11 +710,9 @@ public String getAccount() {
709710

710711
@InternalApi
711712
@Override
712-
public String getTrustBoundaryUrl() throws IOException {
713+
public String getRegionalAccessBoundaryUrl() throws IOException {
713714
return String.format(
714-
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
715-
getUniverseDomain(),
716-
getAccount());
715+
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, getAccount());
717716
}
718717

719718
/**

oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
* </pre>
8181
*/
8282
public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials
83-
implements TrustBoundaryProvider {
83+
implements RegionalAccessBoundaryProvider {
8484

8585
private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
8686

@@ -214,28 +214,28 @@ public AccessToken refreshAccessToken() throws IOException {
214214
this.refreshToken = refreshToken;
215215
}
216216

217-
AccessToken newAccessToken =
218-
AccessToken.newBuilder()
219-
.setExpirationTime(expiresAtMilliseconds)
220-
.setTokenValue(accessToken)
221-
.build();
222-
223-
refreshTrustBoundary(newAccessToken, transportFactory);
224-
return newAccessToken;
217+
return AccessToken.newBuilder()
218+
.setExpirationTime(expiresAtMilliseconds)
219+
.setTokenValue(accessToken)
220+
.build();
225221
}
226222

227223
@InternalApi
228224
@Override
229-
public String getTrustBoundaryUrl() throws IOException {
225+
public String getRegionalAccessBoundaryUrl() throws IOException {
230226
Matcher matcher = WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience());
231227
if (!matcher.matches()) {
232228
throw new IllegalStateException(
233229
"The provided audience is not in the correct format for a workforce pool. "
234230
+ "Refer: https://docs.cloud.google.com/iam/docs/principal-identifiers");
235231
}
236232
String poolId = matcher.group("pool");
237-
return String.format(
238-
IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL, getUniverseDomain(), poolId);
233+
return String.format(IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL, poolId);
234+
}
235+
236+
@Override
237+
HttpTransportFactory getTransportFactory() {
238+
return transportFactory;
239239
}
240240

241241
@Nullable

oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
* account impersonation.
7070
*/
7171
public abstract class ExternalAccountCredentials extends GoogleCredentials
72-
implements TrustBoundaryProvider {
72+
implements RegionalAccessBoundaryProvider {
7373

7474
private static final long serialVersionUID = 8049126194174465023L;
7575

@@ -532,11 +532,7 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
532532
this.impersonatedCredentials = this.buildImpersonatedCredentials();
533533
}
534534
if (this.impersonatedCredentials != null) {
535-
AccessToken accessToken = this.impersonatedCredentials.refreshAccessToken();
536-
// After the impersonated credential refreshes, its trust boundary is
537-
// also refreshed. That is the trust boundary we will use.
538-
this.trustBoundary = this.impersonatedCredentials.getTrustBoundary();
539-
return accessToken;
535+
return this.impersonatedCredentials.refreshAccessToken();
540536
}
541537

542538
StsRequestHandler.Builder requestHandler =
@@ -565,9 +561,7 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
565561
}
566562

567563
StsTokenExchangeResponse response = requestHandler.build().exchangeToken();
568-
AccessToken accessToken = response.getAccessToken();
569-
refreshTrustBoundary(accessToken, transportFactory);
570-
return accessToken;
564+
return response.getAccessToken();
571565
}
572566

573567
/**
@@ -581,6 +575,11 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
581575
*/
582576
public abstract String retrieveSubjectToken() throws IOException;
583577

578+
@Override
579+
HttpTransportFactory getTransportFactory() {
580+
return transportFactory;
581+
}
582+
584583
public String getAudience() {
585584
return audience;
586585
}
@@ -626,14 +625,18 @@ public String getServiceAccountEmail() {
626625

627626
@InternalApi
628627
@Override
629-
public String getTrustBoundaryUrl() {
628+
public String getRegionalAccessBoundaryUrl() throws IOException {
629+
if (getServiceAccountEmail() != null) {
630+
return String.format(
631+
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
632+
getServiceAccountEmail());
633+
}
634+
630635
Matcher workforceMatcher = WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience());
631636
if (workforceMatcher.matches()) {
632637
String poolId = workforceMatcher.group("pool");
633638
return String.format(
634-
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL,
635-
getUniverseDomain(),
636-
poolId);
639+
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL, poolId);
637640
}
638641

639642
Matcher workloadMatcher = WORKLOAD_AUDIENCE_PATTERN.matcher(getAudience());
@@ -642,7 +645,6 @@ public String getTrustBoundaryUrl() {
642645
String poolId = workloadMatcher.group("pool");
643646
return String.format(
644647
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKLOAD_POOL,
645-
getUniverseDomain(),
646648
projectNumber,
647649
poolId);
648650
}

0 commit comments

Comments
 (0)