From 821c70ac19b6e94e397b70db24c1e58a4fd7837f Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 1 Jun 2026 18:06:47 -0700 Subject: [PATCH 1/4] feat: Skip RAB lookup in case of non-email response from MDS. --- .../auth/oauth2/ComputeEngineCredentials.java | 9 +++++- .../oauth2/RegionalAccessBoundaryManager.java | 4 +++ .../oauth2/ComputeEngineCredentialsTest.java | 31 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index bcfe916c3168..c71a233ce962 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -800,8 +800,15 @@ public String getAccount() { @InternalApi @Override public String getRegionalAccessBoundaryUrl() throws IOException { + String account = getAccount(); + // In GKE environments, the default service account might return a non-email placeholder. + // Since RAB lookup requires a valid email-based service account, we skip RAB lookup + // in non-email scenarios by returning null. + if (account == null || !account.contains("@")) { + return null; + } return String.format( - OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, getAccount()); + OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, account); } /** diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java index e35efe86f7a0..d44a3d168c7d 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java @@ -196,6 +196,10 @@ void triggerAsyncRefresh( () -> { try { String url = provider.getRegionalAccessBoundaryUrl(); + if (url == null) { + future.set(null); + return; + } RegionalAccessBoundary newRAB = RegionalAccessBoundary.refresh( transportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis); diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index 78bfd5ddaaa4..a6f861feb6a7 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -1242,6 +1242,37 @@ void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedExce Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION)); } + @org.junit.jupiter.api.Test + void refresh_regionalAccessBoundaryNonEmail() throws IOException, InterruptedException { + + String nonEmailAccount = "non-email-account-value"; + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + RegionalAccessBoundary regionalAccessBoundary = + new RegionalAccessBoundary( + TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION, + TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS, + null); + transportFactory.transport.setRegionalAccessBoundary(regionalAccessBoundary); + transportFactory.transport.setServiceAccountEmail(nonEmailAccount); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + // First call: should NOT initiate async refresh (or should skip it gracefully). + Map> headers = credentials.getRequestMetadata(); + assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY)); + + // Wait a brief time to make sure no background tasks execute and set it. + Thread.sleep(500); + + // It should still be null. + assertNull(credentials.getRegionalAccessBoundary()); + + // And header should still be missing on second call. + headers = credentials.getRequestMetadata(); + assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY)); + } + private void waitForRegionalAccessBoundary(GoogleCredentials credentials) throws InterruptedException { long deadline = System.currentTimeMillis() + 5000; From e610ef43268d2e35628f1714671546229e7aac20 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Tue, 2 Jun 2026 17:52:55 -0700 Subject: [PATCH 2/4] Skip MDS lookup in RAB flow when non-email based account. Added logs. --- .../auth/oauth2/ComputeEngineCredentials.java | 5 ++++ .../oauth2/RegionalAccessBoundaryManager.java | 11 +++++++- .../oauth2/ComputeEngineCredentialsTest.java | 25 +++++++++++++------ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index c71a233ce962..eff14ea47b40 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -805,6 +805,11 @@ public String getRegionalAccessBoundaryUrl() throws IOException { // Since RAB lookup requires a valid email-based service account, we skip RAB lookup // in non-email scenarios by returning null. if (account == null || !account.contains("@")) { + LoggingUtils.log( + LOGGER_PROVIDER, + Level.INFO, + Collections.emptyMap(), + "Regional Access Boundary lookup is skipped for this instance because it is a non-email instance."); return null; } return String.format( diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java index d44a3d168c7d..683015614782 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java @@ -42,6 +42,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; @@ -85,6 +86,8 @@ final class RegionalAccessBoundaryManager { private final AtomicReference cooldownState = new AtomicReference<>(new CooldownState(0, INITIAL_COOLDOWN_MILLIS)); + private final AtomicBoolean skipRAB = new AtomicBoolean(false); + // Unbounded thread creation is discouraged in library code to avoid resource // exhaustion. A shared, bounded executor service ensures a hard limit (5) // on concurrent refresh tasks, while threadCount provides unique names @@ -178,7 +181,7 @@ void triggerAsyncRefresh( final HttpTransportFactory transportFactory, final RegionalAccessBoundaryProvider provider, final AccessToken accessToken) { - if (isCooldownActive()) { + if (skipRAB.get() || isCooldownActive()) { return; } @@ -197,6 +200,7 @@ void triggerAsyncRefresh( try { String url = provider.getRegionalAccessBoundaryUrl(); if (url == null) { + skipRAB.set(true); future.set(null); return; } @@ -283,6 +287,11 @@ long getCurrentCooldownMillis() { return cooldownState.get().durationMillis; } + @VisibleForTesting + boolean isSkipRAB() { + return skipRAB.get(); + } + private static class CooldownState { /** The time (in milliseconds from epoch) when the current cooldown period expires. */ final long expiryTime; diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index a6f861feb6a7..31eba072c2c5 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -1243,8 +1243,8 @@ void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedExce } @org.junit.jupiter.api.Test - void refresh_regionalAccessBoundaryNonEmail() throws IOException, InterruptedException { - + void refresh_regionalAccessBoundaryNonEmail_skipsRABLookup() + throws IOException, InterruptedException { String nonEmailAccount = "non-email-account-value"; MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); RegionalAccessBoundary regionalAccessBoundary = @@ -1258,17 +1258,28 @@ void refresh_regionalAccessBoundaryNonEmail() throws IOException, InterruptedExc ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - // First call: should NOT initiate async refresh (or should skip it gracefully). + // Before any call, skipRAB flag should be false + assertFalse(credentials.regionalAccessBoundaryManager.isSkipRAB()); + + // First call: triggers lookup which determines non-email, returns null, and sets skipRAB to + // true Map> headers = credentials.getRequestMetadata(); assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY)); - // Wait a brief time to make sure no background tasks execute and set it. - Thread.sleep(500); + // Since the task is scheduled asynchronously on the shared executor, wait for it to complete + long deadline = System.currentTimeMillis() + 5000; + while (!credentials.regionalAccessBoundaryManager.isSkipRAB() + && System.currentTimeMillis() < deadline) { + Thread.sleep(50); + } + + // Verify skipRAB flag has been set to true + assertTrue(credentials.regionalAccessBoundaryManager.isSkipRAB()); - // It should still be null. + // Verify RAB is still null assertNull(credentials.getRegionalAccessBoundary()); - // And header should still be missing on second call. + // Second call: should bypass triggerAsyncRefresh completely and remain null headers = credentials.getRequestMetadata(); assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY)); } From d5658baba5b1f9221d2c219b007007b54dcf72a8 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 3 Jun 2026 14:49:37 -0700 Subject: [PATCH 3/4] Added email regex check. --- .../auth/oauth2/ComputeEngineCredentials.java | 4 ++- .../oauth2/ComputeEngineCredentialsTest.java | 32 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index eff14ea47b40..1e286d5f4980 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -72,6 +72,7 @@ import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; /** * OAuth2 credentials representing the built-in service account for a Google Compute Engine VM. @@ -117,6 +118,7 @@ public class ComputeEngineCredentials extends GoogleCredentials private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. "; private static final String PARSE_ERROR_ACCOUNT = "Error parsing service account response. "; + private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@[^@]+\\.[^@]+$"); private static final long serialVersionUID = -4113476462526554235L; private final String transportFactoryClassName; @@ -804,7 +806,7 @@ public String getRegionalAccessBoundaryUrl() throws IOException { // In GKE environments, the default service account might return a non-email placeholder. // Since RAB lookup requires a valid email-based service account, we skip RAB lookup // in non-email scenarios by returning null. - if (account == null || !account.contains("@")) { + if (account == null || !EMAIL_PATTERN.matcher(account).matches()) { LoggingUtils.log( LOGGER_PROVIDER, Level.INFO, diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index 31eba072c2c5..b6de475ae510 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -1213,7 +1213,7 @@ void getProjectId_explicitSet_noMDsCall() { assertEquals(0, transportFactory.transport.getRequestCount()); } - @org.junit.jupiter.api.Test + @Test void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException { String defaultAccountEmail = "default@email.com"; @@ -1242,7 +1242,7 @@ void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedExce Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION)); } - @org.junit.jupiter.api.Test + @Test void refresh_regionalAccessBoundaryNonEmail_skipsRABLookup() throws IOException, InterruptedException { String nonEmailAccount = "non-email-account-value"; @@ -1284,6 +1284,34 @@ void refresh_regionalAccessBoundaryNonEmail_skipsRABLookup() assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY)); } + @Test + void getRegionalAccessBoundaryUrl_validEmail_returnsUrl() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String expectedUrl = + String.format( + OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, + defaultAccountEmail); + assertEquals(expectedUrl, credentials.getRegionalAccessBoundaryUrl()); + } + + @Test + void getRegionalAccessBoundaryUrl_invalidEmail_returnsNull() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "default"; // non-email account format + + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + assertNull(credentials.getRegionalAccessBoundaryUrl()); + } + private void waitForRegionalAccessBoundary(GoogleCredentials credentials) throws InterruptedException { long deadline = System.currentTimeMillis() + 5000; From 420a0fd264f91cea7247a095f76ed5819670bd5c Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 3 Jun 2026 15:06:25 -0700 Subject: [PATCH 4/4] Changed refresh de-duplication signal from SettableFuture to AtomicBoolean for ease of reading. --- .../oauth2/RegionalAccessBoundaryManager.java | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java index 683015614782..b2237fae6d13 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java @@ -37,7 +37,6 @@ import com.google.api.core.InternalApi; import com.google.auth.http.HttpTransportFactory; import com.google.common.annotations.VisibleForTesting; -import com.google.common.util.concurrent.SettableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -76,12 +75,10 @@ final class RegionalAccessBoundaryManager { private final AtomicReference cachedRAB = new AtomicReference<>(); /** - * refreshFuture acts as an atomic gate for request de-duplication. If a future is present, it - * indicates a background refresh is already in progress. It also provides a handle for - * observability and unit testing to track the background task's lifecycle. + * isRefreshing acts as an atomic gate for request de-duplication. If true, it indicates a + * background refresh is already in progress. */ - private final AtomicReference> refreshFuture = - new AtomicReference<>(); + private final AtomicBoolean isRefreshing = new AtomicBoolean(false); private final AtomicReference cooldownState = new AtomicReference<>(new CooldownState(0, INITIAL_COOLDOWN_MILLIS)); @@ -190,18 +187,16 @@ void triggerAsyncRefresh( return; } - SettableFuture future = SettableFuture.create(); // Atomically check if a refresh is already running. If compareAndSet returns true, // this thread "won the race" and is responsible for starting the background task. // All other concurrent threads will return false and exit immediately. - if (refreshFuture.compareAndSet(null, future)) { + if (isRefreshing.compareAndSet(false, true)) { Runnable refreshTask = () -> { try { String url = provider.getRegionalAccessBoundaryUrl(); if (url == null) { skipRAB.set(true); - future.set(null); return; } RegionalAccessBoundary newRAB = @@ -209,14 +204,11 @@ void triggerAsyncRefresh( transportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis); cachedRAB.set(newRAB); resetCooldown(); - // Complete the future so monitors (like unit tests) know we are done. - future.set(newRAB); } catch (Exception e) { handleRefreshFailure(e); - future.setException(e); } finally { // Open the gate again for future refresh requests. - refreshFuture.set(null); + isRefreshing.set(false); } }; @@ -232,8 +224,7 @@ void triggerAsyncRefresh( "Could not submit background refresh task for Regional Access Boundary. " + "This is non-blocking and the library will attempt to refresh on the next access. Error: " + e.getMessage()); - future.setException(e); - refreshFuture.set(null); + isRefreshing.set(false); } } }