diff --git a/oauth2_http/java/com/google/auth/http/HttpCredentialsAdapter.java b/oauth2_http/java/com/google/auth/http/HttpCredentialsAdapter.java
index 90a6c86f1..0d0170334 100644
--- a/oauth2_http/java/com/google/auth/http/HttpCredentialsAdapter.java
+++ b/oauth2_http/java/com/google/auth/http/HttpCredentialsAdapter.java
@@ -39,6 +39,8 @@
import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
import com.google.api.client.util.Preconditions;
import com.google.auth.Credentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.RegionalAccessBoundary;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
@@ -61,6 +63,8 @@ public class HttpCredentialsAdapter
private static final Pattern INVALID_TOKEN_ERROR =
Pattern.compile("\\s*error\\s*=\\s*\"?invalid_token\"?");
+ private static final String STALE_RAB_ERROR_MESSAGE = "stale regional access boundary";
+
private final Credentials credentials;
/**
@@ -119,6 +123,12 @@ public void initialize(HttpRequest request) throws IOException {
*/
@Override
public boolean handleResponse(HttpRequest request, HttpResponse response, boolean supportsRetry) {
+ if (response.getStatusCode() == HttpStatusCodes.STATUS_CODE_BAD_REQUEST) {
+ if (handleStaleRegionalAccessBoundaryError(request, response)) {
+ return true;
+ }
+ }
+
boolean refreshToken = false;
boolean bearer = false;
@@ -152,4 +162,43 @@ public boolean handleResponse(HttpRequest request, HttpResponse response, boolea
}
return false;
}
+
+ private boolean handleStaleRegionalAccessBoundaryError(
+ HttpRequest request, HttpResponse response) {
+ if (!(credentials instanceof GoogleCredentials)) {
+ return false;
+ }
+ GoogleCredentials googleCredentials = (GoogleCredentials) credentials;
+
+ // Only check for stale RAB error if we actually sent the header.
+ if (request.getHeaders().get(RegionalAccessBoundary.HEADER_KEY) == null) {
+ return false;
+ }
+
+ // Skip check for STS and IAM Credentials endpoints as per design.
+ String url = request.getUrl().toString();
+ if (url.contains("sts.googleapis.com") || url.contains("iamcredentials.googleapis.com")) {
+ return false;
+ }
+
+ try {
+ // Check for the stale regional access boundary error message in the response body.
+ // Note: This consumes the response stream.
+ String content = response.parseAsString();
+ if (content != null && content.toLowerCase().contains(STALE_RAB_ERROR_MESSAGE)) {
+ URI uri = null;
+ if (request.getUrl() != null) {
+ uri = request.getUrl().toURI();
+ }
+ googleCredentials.reactiveRefreshRegionalAccessBoundary(
+ uri, googleCredentials.getAccessToken());
+ // Re-initialize headers (this will remove the stale header since cache is cleared)
+ initialize(request);
+ return true;
+ }
+ } catch (Exception e) {
+ LOGGER.log(Level.FINE, "Error while checking for stale regional access boundary", e);
+ }
+ return false;
+ }
}
diff --git a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java
index 208a7d529..44a20d436 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java
@@ -83,7 +83,7 @@
*
These credentials use the IAM API to sign data. See {@link #sign(byte[])} for more details.
*/
public class ComputeEngineCredentials extends GoogleCredentials
- implements ServiceAccountSigner, IdTokenProvider, TrustBoundaryProvider {
+ implements ServiceAccountSigner, IdTokenProvider, RegionalAccessBoundaryProvider {
static final String METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE =
"Empty content from metadata token server request.";
@@ -386,11 +386,7 @@ public AccessToken refreshAccessToken() throws IOException {
int expiresInSeconds =
OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000;
- AccessToken newAccessToken = new AccessToken(accessToken, new Date(expiresAtMilliseconds));
-
- refreshTrustBoundary(newAccessToken, transportFactory);
-
- return newAccessToken;
+ return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
}
/**
@@ -694,6 +690,11 @@ public static Builder newBuilder() {
*
* @throws RuntimeException if the default service account cannot be read
*/
+ @Override
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
@Override
// todo(#314) getAccount should not throw a RuntimeException
public String getAccount() {
@@ -709,7 +710,7 @@ public String getAccount() {
@InternalApi
@Override
- public String getTrustBoundaryUrl() throws IOException {
+ public String getRegionalAccessBoundaryUrl() throws IOException {
return String.format(
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
getUniverseDomain(),
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java
index fc8bf8aef..7b67b4f72 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java
@@ -80,7 +80,7 @@
*
*/
public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials
- implements TrustBoundaryProvider {
+ implements RegionalAccessBoundaryProvider {
private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
@@ -214,19 +214,15 @@ public AccessToken refreshAccessToken() throws IOException {
this.refreshToken = refreshToken;
}
- AccessToken newAccessToken =
- AccessToken.newBuilder()
- .setExpirationTime(expiresAtMilliseconds)
- .setTokenValue(accessToken)
- .build();
-
- refreshTrustBoundary(newAccessToken, transportFactory);
- return newAccessToken;
+ return AccessToken.newBuilder()
+ .setExpirationTime(expiresAtMilliseconds)
+ .setTokenValue(accessToken)
+ .build();
}
@InternalApi
@Override
- public String getTrustBoundaryUrl() throws IOException {
+ public String getRegionalAccessBoundaryUrl() throws IOException {
Matcher matcher = WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience());
if (!matcher.matches()) {
throw new IllegalStateException(
@@ -238,6 +234,11 @@ public String getTrustBoundaryUrl() throws IOException {
IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL, getUniverseDomain(), poolId);
}
+ @Override
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
@Nullable
public String getAudience() {
return audience;
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
index 7b4e3664d..ba86a1b69 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
@@ -69,7 +69,7 @@
* account impersonation.
*/
public abstract class ExternalAccountCredentials extends GoogleCredentials
- implements TrustBoundaryProvider {
+ implements RegionalAccessBoundaryProvider {
private static final long serialVersionUID = 8049126194174465023L;
@@ -532,11 +532,7 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
this.impersonatedCredentials = this.buildImpersonatedCredentials();
}
if (this.impersonatedCredentials != null) {
- AccessToken accessToken = this.impersonatedCredentials.refreshAccessToken();
- // After the impersonated credential refreshes, its trust boundary is
- // also refreshed. That is the trust boundary we will use.
- this.trustBoundary = this.impersonatedCredentials.getTrustBoundary();
- return accessToken;
+ return this.impersonatedCredentials.refreshAccessToken();
}
StsRequestHandler.Builder requestHandler =
@@ -565,9 +561,7 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
}
StsTokenExchangeResponse response = requestHandler.build().exchangeToken();
- AccessToken accessToken = response.getAccessToken();
- refreshTrustBoundary(accessToken, transportFactory);
- return accessToken;
+ return response.getAccessToken();
}
/**
@@ -581,6 +575,11 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
*/
public abstract String retrieveSubjectToken() throws IOException;
+ @Override
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
public String getAudience() {
return audience;
}
@@ -626,7 +625,14 @@ public String getServiceAccountEmail() {
@InternalApi
@Override
- public String getTrustBoundaryUrl() {
+ public String getRegionalAccessBoundaryUrl() throws IOException {
+ if (getServiceAccountEmail() != null) {
+ return String.format(
+ OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
+ getUniverseDomain(),
+ getServiceAccountEmail());
+ }
+
Matcher workforceMatcher = WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience());
if (workforceMatcher.matches()) {
String poolId = workforceMatcher.group("pool");
diff --git a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
index f5f208db3..eeaf82616 100644
--- a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
@@ -35,9 +35,9 @@
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.Preconditions;
-import com.google.api.core.InternalApi;
import com.google.api.core.ObsoleteApi;
import com.google.auth.Credentials;
+import com.google.auth.RequestMetadataCallback;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
@@ -48,6 +48,8 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collection;
@@ -56,6 +58,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.Executor;
import javax.annotation.Nullable;
/** Base type for credentials for authorizing calls to Google APIs using OAuth2. */
@@ -108,7 +111,7 @@ String getFileType() {
private final String universeDomain;
private final boolean isExplicitUniverseDomain;
- TrustBoundary trustBoundary;
+ transient RABManager rabManager = new RABManager();
protected final String quotaProjectId;
@@ -334,58 +337,157 @@ public GoogleCredentials createWithQuotaProject(String quotaProject) {
return this.toBuilder().setQuotaProjectId(quotaProject).build();
}
- @VisibleForTesting
- TrustBoundary getTrustBoundary() {
- return trustBoundary;
+ /**
+ * Returns the currently cached regional access boundary, or null if none is available or if it
+ * has expired.
+ *
+ * @return The cached regional access boundary, or null.
+ */
+ public final RegionalAccessBoundary getRegionalAccessBoundary() {
+ return rabManager.getCachedRAB();
+ }
+
+ /**
+ * Manually sets the regional access boundary for this credential. This seeds the internal cache
+ * and bypasses the initial lookup. The manually provided data will follow the standard 6-hour TTL
+ * expiration logic.
+ *
+ * @param rab The regional access boundary to set.
+ */
+ public final void setRegionalAccessBoundary(RegionalAccessBoundary rab) {
+ rabManager.setManualOverride(rab);
}
/**
- * Refreshes the trust boundary by making a call to the trust boundary URL.
+ * Invalidates the regional access boundary cache and triggers an immediate asynchronous refresh.
*
- * @param newAccessToken The new access token to be used for the refresh.
- * @param transportFactory The HTTP transport factory to be used for the refresh.
- * @throws IOException If the refresh fails and no cached value is available.
+ * @param uri The URI of the outbound request.
+ * @param token The access token to use for the refresh.
+ * @throws IOException If getting the universe domain fails.
*/
- @InternalApi
- void refreshTrustBoundary(AccessToken newAccessToken, HttpTransportFactory transportFactory)
+ public final void reactiveRefreshRegionalAccessBoundary(@Nullable URI uri, AccessToken token)
throws IOException {
+ if (!RegionalAccessBoundary.isEnabled()) {
+ return;
+ }
- if (!(this instanceof TrustBoundaryProvider)
- || !TrustBoundary.isTrustBoundaryEnabled()
- || !isDefaultUniverseDomain()) {
+ String rabUrl = ((RegionalAccessBoundaryProvider) this).getRegionalAccessBoundaryUrl();
+ HttpTransportFactory transportFactory = getTransportFactory();
+ if (transportFactory == null) {
return;
}
- String trustBoundaryUrl = ((TrustBoundaryProvider) this).getTrustBoundaryUrl();
- TrustBoundary cachedTrustBoundary;
+ rabManager.reactiveRefresh(transportFactory, rabUrl, token);
+ }
+
+ /**
+ * Refreshes the Regional Access Boundary if it is expired or not yet fetched.
+ *
+ * @param uri The URI of the outbound request.
+ * @param token The access token to use for the refresh.
+ * @throws IOException If getting the universe domain fails.
+ */
+ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessToken token)
+ throws IOException {
+ if (!(this instanceof RegionalAccessBoundaryProvider)
+ || !RegionalAccessBoundary.isEnabled()
+ || !isDefaultUniverseDomain()) {
+ return;
+ }
- synchronized (lock) {
- // Do not refresh if the cached value is already NO_OP.
- if (trustBoundary != null && trustBoundary.isNoOp()) {
+ if (this instanceof ExternalAccountCredentials) {
+ if (((ExternalAccountCredentials) this).getAudience() == null) {
return;
}
- cachedTrustBoundary = trustBoundary;
- }
-
- TrustBoundary newTrustBoundary;
- try {
- newTrustBoundary =
- TrustBoundary.refresh(
- transportFactory, trustBoundaryUrl, newAccessToken, cachedTrustBoundary);
- } catch (IOException e) {
- // If refresh fails, check for a cached value.
- if (cachedTrustBoundary == null) {
- // No cached value, so fail hard.
- throw new IOException(
- "Failed to refresh trust boundary and no cached value is available.", e);
+ }
+
+ // Skip refresh for regional endpoints.
+ if (uri != null && uri.getHost() != null) {
+ String host = uri.getHost();
+ if (host.endsWith(".rep.googleapis.com") || host.endsWith(".rep.sandbox.googleapis.com")) {
+ return;
}
+ }
+
+ // We need a valid access token for the refresh.
+ if (token == null
+ || (token.getExpirationTime() != null
+ && token.getExpirationTime().before(new java.util.Date()))) {
+ return;
+ }
+
+ String rabUrl = ((RegionalAccessBoundaryProvider) this).getRegionalAccessBoundaryUrl();
+ HttpTransportFactory transportFactory = getTransportFactory();
+ if (transportFactory == null) {
return;
}
- // A lock is required to safely update the shared field.
- synchronized (lock) {
- trustBoundary = newTrustBoundary;
+ rabManager.triggerAsyncRefresh(transportFactory, rabUrl, token);
+ }
+
+ /**
+ * Synchronously provides the request metadata.
+ *
+ *
This method is blocking and will wait for a token refresh if necessary. It also ensures any
+ * available Regional Access Boundary information is included in the metadata.
+ *
+ * @param uri The URI of the request.
+ * @return The request metadata containing the authorization header and potentially regional
+ * access boundary.
+ * @throws IOException If an error occurs while fetching the token.
+ */
+ @Override
+ public Map> getRequestMetadata(URI uri) throws IOException {
+ Map> metadata = super.getRequestMetadata(uri);
+ RegionalAccessBoundary rab = getRegionalAccessBoundary();
+ if (rab != null) {
+ metadata = new HashMap<>(metadata);
+ metadata.put(
+ RegionalAccessBoundary.HEADER_KEY, Collections.singletonList(rab.getEncodedLocations()));
}
+ refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken());
+ return metadata;
+ }
+
+ /**
+ * Asynchronously provides the request metadata.
+ *
+ * This method is non-blocking. It ensures any available Regional Access Boundary information
+ * is included in the metadata.
+ *
+ * @param uri The URI of the request.
+ * @param executor The executor to use for any required background tasks.
+ * @param callback The callback to receive the metadata or any error.
+ */
+ @Override
+ public void getRequestMetadata(
+ final URI uri, Executor executor, final RequestMetadataCallback callback) {
+ super.getRequestMetadata(
+ uri,
+ executor,
+ new RequestMetadataCallback() {
+ @Override
+ public void onSuccess(Map> metadata) {
+ RegionalAccessBoundary rab = getRegionalAccessBoundary();
+ if (rab != null) {
+ metadata = new HashMap<>(metadata);
+ metadata.put(
+ RegionalAccessBoundary.HEADER_KEY,
+ Collections.singletonList(rab.getEncodedLocations()));
+ }
+ try {
+ refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken());
+ } catch (IOException e) {
+ // Ignore failure in async refresh trigger.
+ }
+ callback.onSuccess(metadata);
+ }
+
+ @Override
+ public void onFailure(Throwable exception) {
+ callback.onFailure(exception);
+ }
+ });
}
/**
@@ -443,9 +545,10 @@ static Map> addQuotaProjectIdToRequestMetadata(
protected Map> getAdditionalHeaders() {
Map> headers = new HashMap<>(super.getAdditionalHeaders());
- if (this.trustBoundary != null) {
- String headerValue = trustBoundary.isNoOp() ? "" : trustBoundary.getEncodedLocations();
- headers.put(TrustBoundary.TRUST_BOUNDARY_KEY, Collections.singletonList(headerValue));
+ RegionalAccessBoundary rab = rabManager.getCachedRAB();
+ if (rab != null) {
+ headers.put(
+ RegionalAccessBoundary.HEADER_KEY, Collections.singletonList(rab.getEncodedLocations()));
}
String quotaProjectId = this.getQuotaProjectId();
@@ -503,6 +606,9 @@ protected GoogleCredentials(Builder builder) {
}
this.source = builder.source;
+ if (builder.regionalAccessBoundary != null) {
+ setRegionalAccessBoundary(builder.regionalAccessBoundary);
+ }
}
/**
@@ -560,6 +666,11 @@ public int hashCode() {
return Objects.hash(this.quotaProjectId, this.universeDomain, this.isExplicitUniverseDomain);
}
+ private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
+ input.defaultReadObject();
+ rabManager = new RABManager();
+ }
+
public static Builder newBuilder() {
return new Builder();
}
@@ -695,10 +806,21 @@ public Map getCredentialInfo() {
return ImmutableMap.copyOf(infoMap);
}
+ /**
+ * Returns the transport factory used by the credential.
+ *
+ * @return the transport factory, or null if not available.
+ */
+ @Nullable
+ HttpTransportFactory getTransportFactory() {
+ return null;
+ }
+
public static class Builder extends OAuth2Credentials.Builder {
@Nullable protected String quotaProjectId;
@Nullable protected String universeDomain;
@Nullable String source;
+ @Nullable protected RegionalAccessBoundary regionalAccessBoundary;
protected Builder() {}
@@ -708,12 +830,14 @@ protected Builder(GoogleCredentials credentials) {
if (credentials.isExplicitUniverseDomain) {
this.universeDomain = credentials.universeDomain;
}
+ this.regionalAccessBoundary = credentials.getRegionalAccessBoundary();
}
protected Builder(GoogleCredentials.Builder builder) {
setAccessToken(builder.getAccessToken());
this.quotaProjectId = builder.quotaProjectId;
this.universeDomain = builder.universeDomain;
+ this.regionalAccessBoundary = builder.regionalAccessBoundary;
}
@Override
@@ -732,6 +856,19 @@ public Builder setUniverseDomain(String universeDomain) {
return this;
}
+ /**
+ * Manually sets the regional access boundary for this credential. This seeds the internal cache
+ * and bypasses the initial lookup.
+ *
+ * @param regionalAccessBoundary The regional access boundary to set.
+ * @return this {@code Builder} object.
+ */
+ @CanIgnoreReturnValue
+ public Builder setRegionalAccessBoundary(RegionalAccessBoundary regionalAccessBoundary) {
+ this.regionalAccessBoundary = regionalAccessBoundary;
+ return this;
+ }
+
public String getQuotaProjectId() {
return this.quotaProjectId;
}
diff --git a/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java
index eb19f5cbb..ee846e016 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java
@@ -96,7 +96,7 @@
*
*/
public class ImpersonatedCredentials extends GoogleCredentials
- implements ServiceAccountSigner, IdTokenProvider, TrustBoundaryProvider {
+ implements ServiceAccountSigner, IdTokenProvider, RegionalAccessBoundaryProvider {
private static final long serialVersionUID = -2133257318957488431L;
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";
@@ -328,7 +328,7 @@ public GoogleCredentials getSourceCredentials() {
@InternalApi
@Override
- public String getTrustBoundaryUrl() throws IOException {
+ public String getRegionalAccessBoundaryUrl() throws IOException {
return String.format(
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
getUniverseDomain(),
@@ -339,6 +339,11 @@ int getLifetime() {
return this.lifetime;
}
+ @Override
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
public void setTransportFactory(HttpTransportFactory httpTransportFactory) {
this.transportFactory = httpTransportFactory;
}
@@ -603,11 +608,7 @@ public AccessToken refreshAccessToken() throws IOException {
format.setCalendar(calendar);
try {
Date date = format.parse(expireTime);
- AccessToken newAccessToken = new AccessToken(accessToken, date);
-
- refreshTrustBoundary(newAccessToken, transportFactory);
-
- return newAccessToken;
+ return new AccessToken(accessToken, date);
} catch (ParseException pe) {
throw new IOException("Error parsing expireTime: " + pe.getMessage());
}
diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java b/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java
index 7ef5485d0..0835f6dd7 100644
--- a/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java
@@ -163,6 +163,16 @@ Duration getExpirationMargin() {
return this.expirationMargin;
}
+ /**
+ * Asynchronously provides the request metadata by ensuring there is a current access token and
+ * providing it as an authorization bearer token.
+ *
+ * This method is non-blocking. The results are provided through the given callback.
+ *
+ * @param uri The URI of the request.
+ * @param executor The executor to use for any required background tasks.
+ * @param callback The callback to receive the metadata or any error.
+ */
@Override
public void getRequestMetadata(
final URI uri, Executor executor, final RequestMetadataCallback callback) {
@@ -174,8 +184,14 @@ public void getRequestMetadata(
}
/**
- * Provide the request metadata by ensuring there is a current access token and providing it as an
- * authorization bearer token.
+ * Synchronously provides the request metadata by ensuring there is a current access token and
+ * providing it as an authorization bearer token.
+ *
+ *
This method is blocking and will wait for a token refresh if necessary.
+ *
+ * @param uri The URI of the request.
+ * @return The request metadata containing the authorization header.
+ * @throws IOException If an error occurs while fetching the token.
*/
@Override
public Map> getRequestMetadata(URI uri) throws IOException {
diff --git a/oauth2_http/java/com/google/auth/oauth2/RABManager.java b/oauth2_http/java/com/google/auth/oauth2/RABManager.java
new file mode 100644
index 000000000..4cd067385
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/RABManager.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2026, Google LLC
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.google.auth.oauth2;
+
+import com.google.api.client.util.Clock;
+import com.google.api.core.InternalApi;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.common.annotations.VisibleForTesting;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * Manages the lifecycle of Regional Access Boundaries (RAB) for a credential.
+ *
+ * This class handles caching, asynchronous refreshing, and cooldown logic to ensure that API
+ * requests are not blocked by lookup failures and that the lookup service is not overwhelmed.
+ */
+@InternalApi
+final class RABManager {
+
+ private static final Logger LOGGER = Logger.getLogger(RABManager.class.getName());
+ private static final LoggerProvider LOGGER_PROVIDER = LoggerProvider.forClazz(RABManager.class);
+
+ static final long INITIAL_COOLDOWN_MILLIS = 15 * 60 * 1000L; // 15 minutes
+ static final long MAX_COOLDOWN_MILLIS = 24 * 60 * 60 * 1000L; // 24 hours
+
+ /**
+ * cachedRAB uses AtomicReference to provide thread-safe, lock-free access to the cached data for
+ * high-concurrency request threads.
+ */
+ 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.
+ */
+ private final AtomicReference> refreshFuture =
+ new AtomicReference<>();
+
+ private long cooldownStartTime = 0;
+ private long currentCooldownMillis = INITIAL_COOLDOWN_MILLIS;
+ private static Clock clock = Clock.SYSTEM;
+
+ /**
+ * Returns the currently cached RegionalAccessBoundary, or null if none is available or if it has
+ * expired.
+ *
+ * @return The cached RAB, or null.
+ */
+ @Nullable
+ RegionalAccessBoundary getCachedRAB() {
+ RegionalAccessBoundary rab = cachedRAB.get();
+ if (rab != null && !rab.isExpired()) {
+ return rab;
+ }
+ return null;
+ }
+
+ /**
+ * Sets a manual override for the Regional Access Boundary. This seeds the cache.
+ *
+ * @param rab The Regional Access Boundary to cache.
+ */
+ void setManualOverride(RegionalAccessBoundary rab) {
+ cachedRAB.set(rab);
+ }
+
+ /**
+ * Triggers an asynchronous refresh of the RegionalAccessBoundary if it is not already being
+ * refreshed and if the cooldown period is not active.
+ *
+ * This method is entirely non-blocking for the calling thread. If a refresh is already in
+ * progress or a cooldown is active, it returns immediately.
+ *
+ * @param transportFactory The HTTP transport factory to use for the lookup.
+ * @param url The lookup endpoint URL.
+ * @param accessToken The access token for authentication.
+ */
+ void triggerAsyncRefresh(
+ final HttpTransportFactory transportFactory,
+ final String url,
+ final AccessToken accessToken) {
+ if (isCooldownActive()) {
+ return;
+ }
+
+ RegionalAccessBoundary currentRab = cachedRAB.get();
+ if (currentRab != null && !currentRab.isExpired()) {
+ return;
+ }
+
+ CompletableFuture future = new CompletableFuture<>();
+ // 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)) {
+ CompletableFuture.runAsync(
+ () -> {
+ try {
+ RegionalAccessBoundary newRAB =
+ RegionalAccessBoundary.refresh(
+ transportFactory, url, accessToken, cachedRAB.get());
+ cachedRAB.set(newRAB);
+ resetCooldown();
+ // Complete the future so monitors (like unit tests) know we are done.
+ future.complete(newRAB);
+ } catch (Exception e) {
+ handleRefreshFailure(e);
+ future.completeExceptionally(e);
+ } finally {
+ // Open the gate again for future refresh requests.
+ refreshFuture.set(null);
+ }
+ });
+ }
+ }
+
+ /** Invalidates the current cache. Useful for reactive refresh on stale error. */
+ void invalidateCache() {
+ cachedRAB.set(null);
+ }
+
+ /**
+ * Invalidates the cache and triggers an immediate asynchronous refresh.
+ *
+ * @param transportFactory The HTTP transport factory to use for the lookup.
+ * @param url The lookup endpoint URL.
+ * @param accessToken The access token for authentication.
+ */
+ void reactiveRefresh(
+ final HttpTransportFactory transportFactory,
+ final String url,
+ final AccessToken accessToken) {
+ invalidateCache();
+ triggerAsyncRefresh(transportFactory, url, accessToken);
+ }
+
+ private synchronized void handleRefreshFailure(Exception e) {
+ if (cooldownStartTime == 0) {
+ cooldownStartTime = clock.currentTimeMillis();
+ currentCooldownMillis = INITIAL_COOLDOWN_MILLIS;
+ LoggingUtils.log(
+ LOGGER_PROVIDER,
+ Level.INFO,
+ null,
+ "RAB lookup failed; entering cooldown for "
+ + (currentCooldownMillis / 60000)
+ + "m. Error: "
+ + e.getMessage());
+ } else {
+ // Extend cooldown
+ currentCooldownMillis = Math.min(currentCooldownMillis * 2, MAX_COOLDOWN_MILLIS);
+ cooldownStartTime = clock.currentTimeMillis();
+ LoggingUtils.log(
+ LOGGER_PROVIDER,
+ Level.INFO,
+ null,
+ "RAB lookup failed again; extending cooldown to "
+ + (currentCooldownMillis / 60000)
+ + "m. Error: "
+ + e.getMessage());
+ }
+ }
+
+ private synchronized void resetCooldown() {
+ cooldownStartTime = 0;
+ currentCooldownMillis = INITIAL_COOLDOWN_MILLIS;
+ }
+
+ synchronized boolean isCooldownActive() {
+ if (cooldownStartTime == 0) {
+ return false;
+ }
+ return clock.currentTimeMillis() < cooldownStartTime + currentCooldownMillis;
+ }
+
+ @VisibleForTesting
+ synchronized long getCurrentCooldownMillis() {
+
+ return currentCooldownMillis;
+ }
+
+ @VisibleForTesting
+ static void setClockForTest(Clock testClock) {
+ clock = testClock;
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/TrustBoundary.java b/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java
similarity index 59%
rename from oauth2_http/java/com/google/auth/oauth2/TrustBoundary.java
rename to oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java
index ebbb110de..d59e0bb67 100644
--- a/oauth2_http/java/com/google/auth/oauth2/TrustBoundary.java
+++ b/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java
@@ -41,6 +41,7 @@
import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonParser;
+import com.google.api.client.util.Clock;
import com.google.api.client.util.ExponentialBackOff;
import com.google.api.client.util.Key;
import com.google.auth.http.HttpTransportFactory;
@@ -48,39 +49,57 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import java.io.IOException;
+import java.io.Serializable;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.annotation.Nullable;
/**
- * Represents the trust boundary configuration for a credential. This class holds the information
- * retrieved from the IAM `allowedLocations` endpoint. This data is then used to populate the
- * `x-allowed-locations` header in outgoing API requests, which in turn allows Google's
+ * Represents the regional access boundary configuration for a credential. This class holds the
+ * information retrieved from the IAM `allowedLocations` endpoint. This data is then used to
+ * populate the `x-allowed-locations` header in outgoing API requests, which in turn allows Google's
* infrastructure to enforce regional security restrictions. This class does not perform any
* client-side validation or enforcement.
*/
-final class TrustBoundary {
+public final class RegionalAccessBoundary implements Serializable {
+
+ public static final String HEADER_KEY = "x-allowed-locations";
+ private static final long serialVersionUID = -2428522338274020302L;
+
+ static final String ENABLE_EXPERIMENT_ENV_VAR = "GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT";
+ static final long TTL_MILLIS = 6 * 60 * 60 * 1000L; // 6 hours
+ private static int maxRetryElapsedTimeMillis = 60000; // 1 minute
- static final String TRUST_BOUNDARY_KEY = "x-allowed-locations";
- static final String GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR =
- "GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT";
- private static final String NO_OP_VALUE = "0x0";
private final String encodedLocations;
private final List locations;
+ private final long refreshTime;
+ private static Clock clock = Clock.SYSTEM;
+
+ /**
+ * Creates a new RegionalAccessBoundary instance.
+ *
+ * @param encodedLocations The encoded string representation of the allowed locations.
+ * @param locations A list of human-readable location strings.
+ */
+ RegionalAccessBoundary(String encodedLocations, List locations) {
+ this(encodedLocations, locations, clock.currentTimeMillis());
+ }
/**
- * Creates a new TrustBoundary instance.
+ * Internal constructor for testing and manual creation with refresh time.
*
* @param encodedLocations The encoded string representation of the allowed locations.
* @param locations A list of human-readable location strings.
+ * @param refreshTime The time at which the information was last refreshed.
*/
- TrustBoundary(String encodedLocations, List locations) {
+ RegionalAccessBoundary(String encodedLocations, List locations, long refreshTime) {
this.encodedLocations = encodedLocations;
this.locations =
locations == null
? Collections.emptyList()
: Collections.unmodifiableList(locations);
+ this.refreshTime = refreshTime;
}
private static EnvironmentProvider environmentProvider = SystemEnvironmentProvider.getInstance();
@@ -95,17 +114,22 @@ public List getLocations() {
return locations;
}
+ /** Returns the time at which the information was last refreshed. */
+ public long getRefreshTime() {
+ return refreshTime;
+ }
+
/**
- * Checks if this TrustBoundary represents a "no-op" (no restrictions).
+ * Checks if the regional access boundary data is expired.
*
- * @return True if the encoded locations indicate no restrictions, false otherwise.
+ * @return True if the data has expired based on the TTL, false otherwise.
*/
- public boolean isNoOp() {
- return NO_OP_VALUE.equals(encodedLocations);
+ public boolean isExpired() {
+ return clock.currentTimeMillis() > refreshTime + TTL_MILLIS;
}
- /** Represents the JSON response from the trust boundary endpoint. */
- public static class TrustBoundaryResponse extends GenericJson {
+ /** Represents the JSON response from the regional access boundary endpoint. */
+ public static class RegionalAccessBoundaryResponse extends GenericJson {
@Key("encodedLocations")
private String encodedLocations;
@@ -123,7 +147,7 @@ public List getLocations() {
}
@Override
- /** Returns a string representation of the TrustBoundaryResponse. */
+ /** Returns a string representation of the RegionalAccessBoundaryResponse. */
public String toString() {
return MoreObjects.toStringHelper(this)
.add("encodedLocations", encodedLocations)
@@ -137,40 +161,52 @@ static void setEnvironmentProviderForTest(@Nullable EnvironmentProvider provider
environmentProvider = provider == null ? SystemEnvironmentProvider.getInstance() : provider;
}
+ @VisibleForTesting
+ static void setClockForTest(Clock testClock) {
+ clock = testClock;
+ }
+
+ @VisibleForTesting
+ static void setMaxRetryElapsedTimeMillisForTest(int millis) {
+ maxRetryElapsedTimeMillis = millis;
+ }
+
/**
- * Checks if the trust boundary feature is enabled based on an environment variable. The feature
- * is enabled if the environment variable is set to "true" or "1" (case-insensitive). Any other
- * value, or if the variable is unset, will result in the feature being disabled.
+ * Checks if the regional access boundary feature is enabled. The feature is enabled if the
+ * environment variable or system property "GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT" is set
+ * to "true" or "1" (case-insensitive).
*
- * @return True if the trust boundary feature is enabled, false otherwise.
+ * @return True if the regional access boundary feature is enabled, false otherwise.
*/
- static boolean isTrustBoundaryEnabled() {
- String trustBoundaryEnabled =
- environmentProvider.getEnv(GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR);
- if (trustBoundaryEnabled == null) {
+ static boolean isEnabled() {
+ String enabled = environmentProvider.getEnv(ENABLE_EXPERIMENT_ENV_VAR);
+ if (enabled == null) {
+ enabled = System.getProperty(ENABLE_EXPERIMENT_ENV_VAR);
+ }
+ if (enabled == null) {
return false;
}
- String lowercasedTrustBoundaryEnabled = trustBoundaryEnabled.toLowerCase();
- return "true".equals(lowercasedTrustBoundaryEnabled) || "1".equals(trustBoundaryEnabled);
+ String lowercased = enabled.toLowerCase();
+ return "true".equals(lowercased) || "1".equals(enabled);
}
/**
- * Refreshes the trust boundary by making a network call to the trust boundary endpoint.
+ * Refreshes the regional access boundary by making a network call to the lookup endpoint.
*
* @param transportFactory The HTTP transport factory to use for the network request.
- * @param url The URL of the trust boundary endpoint.
+ * @param url The URL of the regional access boundary endpoint.
* @param accessToken The access token to authenticate the request.
- * @param cachedTrustBoundary An optional previously cached trust boundary, which may be used in
+ * @param cachedRAB An optional previously cached regional access boundary, which may be used in
* the request headers.
- * @return A new TrustBoundary object containing the refreshed information.
+ * @return A new RegionalAccessBoundary object containing the refreshed information.
* @throws IllegalArgumentException If the provided access token is null or expired.
* @throws IOException If a network error occurs or the response is malformed.
*/
- static TrustBoundary refresh(
+ static RegionalAccessBoundary refresh(
HttpTransportFactory transportFactory,
String url,
AccessToken accessToken,
- @Nullable TrustBoundary cachedTrustBoundary)
+ @Nullable RegionalAccessBoundary cachedRAB)
throws IOException {
Preconditions.checkNotNull(accessToken, "The provided access token is null.");
if (accessToken.getExpirationTime() != null
@@ -182,9 +218,9 @@ static TrustBoundary refresh(
HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url));
request.getHeaders().setAuthorization("Bearer " + accessToken.getTokenValue());
- // Add the cached trust boundary header, if available.
- if (cachedTrustBoundary != null) {
- request.getHeaders().set(TRUST_BOUNDARY_KEY, cachedTrustBoundary.getEncodedLocations());
+ // Add the cached regional access boundary header, if available.
+ if (cachedRAB != null) {
+ request.getHeaders().set(HEADER_KEY, cachedRAB.getEncodedLocations());
}
// Add retry logic
@@ -193,33 +229,40 @@ static TrustBoundary refresh(
.setInitialIntervalMillis(OAuth2Utils.INITIAL_RETRY_INTERVAL_MILLIS)
.setRandomizationFactor(OAuth2Utils.RETRY_RANDOMIZATION_FACTOR)
.setMultiplier(OAuth2Utils.RETRY_MULTIPLIER)
+ .setMaxElapsedTimeMillis(maxRetryElapsedTimeMillis)
.build();
HttpUnsuccessfulResponseHandler unsuccessfulResponseHandler =
- new HttpBackOffUnsuccessfulResponseHandler(backoff);
+ new HttpBackOffUnsuccessfulResponseHandler(backoff)
+ .setBackOffRequired(
+ response -> {
+ int statusCode = response.getStatusCode();
+ return (statusCode >= 500 && statusCode < 600)
+ || statusCode == 403
+ || statusCode == 404;
+ });
request.setUnsuccessfulResponseHandler(unsuccessfulResponseHandler);
HttpIOExceptionHandler ioExceptionHandler = new HttpBackOffIOExceptionHandler(backoff);
request.setIOExceptionHandler(ioExceptionHandler);
- TrustBoundaryResponse json;
+ RegionalAccessBoundaryResponse json;
try {
HttpResponse response = request.execute();
String responseString = response.parseAsString();
JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(responseString);
- json = parser.parseAndClose(TrustBoundaryResponse.class);
+ json = parser.parseAndClose(RegionalAccessBoundaryResponse.class);
} catch (IOException e) {
- throw new IOException("TrustBoundary: Failure while getting trust boundaries:", e);
+ throw new IOException(
+ "RegionalAccessBoundary: Failure while getting regional access boundaries:", e);
}
String encodedLocations = json.getEncodedLocations();
- // The encodedLocations is the value attached to the x-allowed-locations header and
- // it should always have a value. In case of NO_OP the lookup endpoint returns
- // encodedLocations as '0x0' and locations as null. That is why we only check for
- // encodedLocations.
+ // The encodedLocations is the value attached to the x-allowed-locations header, and
+ // it should always have a value.
if (encodedLocations == null) {
throw new IOException(
- "TrustBoundary: Malformed response from lookup endpoint - `encodedLocations` was null.");
+ "RegionalAccessBoundary: Malformed response from lookup endpoint - `encodedLocations` was null.");
}
- return new TrustBoundary(encodedLocations, json.getLocations());
+ return new RegionalAccessBoundary(encodedLocations, json.getLocations());
}
}
diff --git a/oauth2_http/java/com/google/auth/oauth2/TrustBoundaryProvider.java b/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryProvider.java
similarity index 81%
rename from oauth2_http/java/com/google/auth/oauth2/TrustBoundaryProvider.java
rename to oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryProvider.java
index 3f034d22f..520360478 100644
--- a/oauth2_http/java/com/google/auth/oauth2/TrustBoundaryProvider.java
+++ b/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryProvider.java
@@ -35,16 +35,16 @@
import java.io.IOException;
/**
- * An interface for providing trust boundary information. It is used to provide a common interface
- * for credentials that support trust boundary checks.
+ * An interface for providing regional access boundary information. It is used to provide a common
+ * interface for credentials that support regional access boundary checks.
*/
@InternalApi
-interface TrustBoundaryProvider {
+interface RegionalAccessBoundaryProvider {
/**
- * Returns the trust boundary URI.
+ * Returns the regional access boundary URI.
*
- * @return The trust boundary URI.
+ * @return The regional access boundary URI.
*/
- String getTrustBoundaryUrl() throws IOException;
+ String getRegionalAccessBoundaryUrl() throws IOException;
}
diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
index 5307d4d6d..f1aa01bc5 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
@@ -90,7 +90,7 @@
* By default uses a JSON Web Token (JWT) to fetch access tokens.
*/
public class ServiceAccountCredentials extends GoogleCredentials
- implements ServiceAccountSigner, IdTokenProvider, JwtProvider, TrustBoundaryProvider {
+ implements ServiceAccountSigner, IdTokenProvider, JwtProvider, RegionalAccessBoundaryProvider {
private static final long serialVersionUID = 7807543542681217978L;
private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
@@ -581,11 +581,7 @@ public AccessToken refreshAccessToken() throws IOException {
int expiresInSeconds =
OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000L;
- AccessToken newAccessToken = new AccessToken(accessToken, new Date(expiresAtMilliseconds));
-
- refreshTrustBoundary(newAccessToken, transportFactory);
-
- return newAccessToken;
+ return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
}
/**
@@ -830,7 +826,7 @@ public boolean getUseJwtAccessWithScope() {
@InternalApi
@Override
- public String getTrustBoundaryUrl() throws IOException {
+ public String getRegionalAccessBoundaryUrl() throws IOException {
return String.format(
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
getUniverseDomain(),
@@ -842,6 +838,11 @@ JwtCredentials getSelfSignedJwtCredentialsWithScope() {
return selfSignedJwtCredentialsWithScope;
}
+ @Override
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
@Override
public String getAccount() {
return getClientEmail();
@@ -1037,6 +1038,17 @@ JwtCredentials createSelfSignedJwtCredentials(final URI uri, Collection
.build();
}
+ /**
+ * Asynchronously provides the request metadata.
+ *
+ * This method is non-blocking. For Self-signed JWT flows (which are calculated locally), it
+ * may execute the callback immediately on the calling thread. For standard flows, it may use the
+ * provided executor for background tasks.
+ *
+ * @param uri The URI of the request.
+ * @param executor The executor to use for any required background tasks.
+ * @param callback The callback to receive the metadata or any error.
+ */
@Override
public void getRequestMetadata(
final URI uri, Executor executor, final RequestMetadataCallback callback) {
@@ -1059,7 +1071,16 @@ public void getRequestMetadata(
}
}
- /** Provide the request metadata by putting an access JWT directly in the metadata. */
+ /**
+ * Synchronously provides the request metadata.
+ *
+ *
This method is blocking. For standard flows, it will wait for a network call to complete.
+ * For Self-signed JWT flows, it calculates the token locally.
+ *
+ * @param uri The URI of the request.
+ * @return The request metadata containing the authorization header.
+ * @throws IOException If an error occurs while fetching or calculating the token.
+ */
@Override
public Map> getRequestMetadata(URI uri) throws IOException {
if (createScopedRequired() && uri == null) {
@@ -1128,6 +1149,17 @@ private Map> getRequestMetadataWithSelfSignedJwt(URI uri)
}
Map> requestMetadata = jwtCredentials.getRequestMetadata(null);
+ List authHeaders = requestMetadata.get(AuthHttpConstants.AUTHORIZATION);
+ if (authHeaders != null && !authHeaders.isEmpty()) {
+ // Extract the token value to trigger a background Regional Access Boundary refresh.
+ String authHeader = authHeaders.get(0);
+ if (authHeader.startsWith(AuthHttpConstants.BEARER + " ")) {
+ String tokenValue = authHeader.substring((AuthHttpConstants.BEARER + " ").length());
+ // Use a null expiration as JWTs are short-lived anyway.
+ AccessToken wrappedToken = new AccessToken(tokenValue, null);
+ refreshRegionalAccessBoundaryIfExpired(uri, wrappedToken);
+ }
+ }
return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata);
}
diff --git a/oauth2_http/javatests/com/google/auth/TestUtils.java b/oauth2_http/javatests/com/google/auth/TestUtils.java
index b68b49dc5..58ef558a9 100644
--- a/oauth2_http/javatests/com/google/auth/TestUtils.java
+++ b/oauth2_http/javatests/com/google/auth/TestUtils.java
@@ -66,8 +66,8 @@ public class TestUtils {
URI.create("https://auth.cloud.google/authorize");
public static final URI WORKFORCE_IDENTITY_FEDERATION_TOKEN_SERVER_URI =
URI.create("https://sts.googleapis.com/v1/oauthtoken");
- public static final String TRUST_BOUNDARY_ENCODED_LOCATION = "0x800000";
- public static final List TRUST_BOUNDARY_LOCATIONS =
+ public static final String REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION = "0x800000";
+ public static final List REGIONAL_ACCESS_BOUNDARY_LOCATIONS =
ImmutableList.of("us-central1", "us-central2");
private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
index d197bb569..f14c668f3 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
@@ -1401,9 +1401,9 @@ public AwsSecurityCredentials getCredentials(ExternalAccountSupplierContext cont
}
@Test
- public void testRefresh_trustBoundarySuccess() throws IOException {
+ public void testRefresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
MockExternalAccountCredentialsTransportFactory transportFactory =
@@ -1422,11 +1422,29 @@ public void testRefresh_trustBoundarySuccess() throws IOException {
.setSubjectTokenType("subjectTokenType")
.build();
- awsCredential.refresh();
+ // First call: initiates async refresh.
+ Map> headers = awsCredential.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.HEADER_KEY));
- TrustBoundary trustBoundary = awsCredential.getTrustBoundary();
- assertNotNull(trustBoundary);
- assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
- TrustBoundary.setEnvironmentProviderForTest(null);
+ waitForRegionalAccessBoundary(awsCredential);
+
+ // Second call: should have header.
+ headers = awsCredential.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ RegionalAccessBoundary.setEnvironmentProviderForTest(null);
+ }
+
+ private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (credentials.getRegionalAccessBoundary() == null
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (credentials.getRegionalAccessBoundary() == null) {
+ fail("Timed out waiting for regional access boundary refresh");
+ }
}
}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java
index e13c9ce93..83e5b7fc3 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java
@@ -33,7 +33,7 @@
import static com.google.auth.oauth2.ComputeEngineCredentials.METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE;
import static com.google.auth.oauth2.ImpersonatedCredentialsTest.SA_CLIENT_EMAIL;
-import static com.google.auth.oauth2.TrustBoundary.TRUST_BOUNDARY_KEY;
+import static com.google.auth.oauth2.RegionalAccessBoundary.HEADER_KEY;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -132,7 +132,7 @@ public class ComputeEngineCredentialsTest extends BaseSerializationTest {
@After
public void tearDown() {
- TrustBoundary.setEnvironmentProviderForTest(null);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(null);
}
@Test
@@ -1153,45 +1153,45 @@ public void idTokenWithAudience_503StatusCode() {
}
@Test
- public void refresh_trustBoundarySuccess() throws IOException {
+ public void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
String defaultAccountEmail = "default@email.com";
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
- TrustBoundary trustBoundary =
- new TrustBoundary(
- TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, TestUtils.TRUST_BOUNDARY_LOCATIONS);
- transportFactory.transport.setTrustBoundary(trustBoundary);
+ RegionalAccessBoundary regionalAccessBoundary =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS);
+ transportFactory.transport.setRegionalAccessBoundary(regionalAccessBoundary);
transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ // First call: initiates async refresh.
Map> headers = credentials.getRequestMetadata();
- assertEquals(
- headers.get(TRUST_BOUNDARY_KEY), Arrays.asList(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION));
- }
+ assertNull(headers.get(HEADER_KEY));
- @Test
- public void refresh_trustBoundaryFails_throwsIOException() throws IOException {
- TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
+ waitForRegionalAccessBoundary(credentials);
- String defaultAccountEmail = "default@email.com";
- MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
- transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
-
- ComputeEngineCredentials credentials =
- ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
- IOException exception = assertThrows(IOException.class, () -> credentials.refresh());
- assertTrue(
- "The exception message should explain why the refresh failed.",
- exception
- .getMessage()
- .contains("Failed to refresh trust boundary and no cached value is available."));
+ private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (credentials.getRegionalAccessBoundary() == null
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (credentials.getRegionalAccessBoundary() == null) {
+ fail("Timed out waiting for regional access boundary refresh");
+ }
}
static class MockMetadataServerTransportFactory implements HttpTransportFactory {
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
index 525cb20e9..b8930d23b 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
@@ -35,17 +35,14 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
-import com.google.api.client.util.Clock;
import com.google.auth.TestUtils;
import com.google.auth.http.AuthHttpConstants;
import com.google.auth.http.HttpTransportFactory;
@@ -137,7 +134,7 @@ public void setup() {
@After
public void tearDown() {
- TrustBoundary.setEnvironmentProviderForTest(null);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(null);
}
@Test
@@ -1225,76 +1222,45 @@ public void toString_expectedFormat() {
}
@Test
- public void testRefresh_trustBoundarySuccess() throws IOException {
+ public void testRefresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
ExternalAccountAuthorizedUserCredentials credentials =
ExternalAccountAuthorizedUserCredentials.newBuilder()
- .setHttpTransportFactory(transportFactory)
- .setAudience(AUDIENCE)
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setRefreshToken(REFRESH_TOKEN)
.setTokenUrl(TOKEN_URL)
+ .setAudience(
+ "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
+ .setHttpTransportFactory(transportFactory)
.build();
- credentials.refresh();
- TrustBoundary trustBoundary = credentials.getTrustBoundary();
- assertNotNull(trustBoundary);
- assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
- }
-
- @Test
- public void testRefresh_trustBoundaryFails_incorrectAudience() {
- TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.HEADER_KEY));
- ExternalAccountAuthorizedUserCredentials credentials =
- ExternalAccountAuthorizedUserCredentials.newBuilder()
- .setHttpTransportFactory(transportFactory)
- .setAudience("audience")
- .setClientId(CLIENT_ID)
- .setClientSecret(CLIENT_SECRET)
- .setRefreshToken(REFRESH_TOKEN)
- .setTokenUrl(TOKEN_URL)
- .build();
+ waitForRegionalAccessBoundary(credentials);
- IllegalStateException exception =
- assertThrows(
- IllegalStateException.class,
- () -> {
- credentials.refresh();
- });
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
assertEquals(
- "The provided audience is not in the correct format for a workforce pool. "
- + "Refer: https://docs.cloud.google.com/iam/docs/principal-identifiers",
- exception.getMessage());
+ headers.get(RegionalAccessBoundary.HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
}
- @Test
- public void serialize() throws IOException, ClassNotFoundException {
- ExternalAccountAuthorizedUserCredentials credentials =
- ExternalAccountAuthorizedUserCredentials.newBuilder()
- .setAudience(AUDIENCE)
- .setClientId(CLIENT_ID)
- .setClientSecret(CLIENT_SECRET)
- .setRefreshToken(REFRESH_TOKEN)
- .setTokenUrl(TOKEN_URL)
- .setTokenInfoUrl(TOKEN_INFO_URL)
- .setRevokeUrl(REVOKE_URL)
- .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
- .setQuotaProjectId(QUOTA_PROJECT)
- .build();
-
- ExternalAccountAuthorizedUserCredentials deserializedCredentials =
- serializeAndDeserialize(credentials);
- assertEquals(credentials, deserializedCredentials);
- assertEquals(credentials.hashCode(), deserializedCredentials.hashCode());
- assertEquals(credentials.toString(), deserializedCredentials.toString());
- assertSame(deserializedCredentials.clock, Clock.SYSTEM);
+ private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (credentials.getRegionalAccessBoundary() == null
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (credentials.getRegionalAccessBoundary() == null) {
+ fail("Timed out waiting for regional access boundary refresh");
+ }
}
static GenericJson buildJsonCredentials() {
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
index 3245995c9..987753126 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
@@ -32,6 +32,9 @@
package com.google.auth.oauth2;
import static com.google.auth.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL;
+import static com.google.auth.oauth2.OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT;
+import static com.google.auth.oauth2.OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL;
+import static com.google.auth.oauth2.OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKLOAD_POOL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -51,12 +54,7 @@
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URI;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
+import java.util.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -97,7 +95,7 @@ public void setup() {
@After
public void tearDown() {
- TrustBoundary.setEnvironmentProviderForTest(null);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(null);
}
@Test
@@ -1256,7 +1254,7 @@ public void validateServiceAccountImpersonationUrls_invalidUrls() {
}
@Test
- public void getTrustBoundaryUrl_workload() throws IOException {
+ public void getRegionalAccessBoundaryUrl_workload() throws IOException {
String audience =
"//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/my-pool/providers/my-provider";
ExternalAccountCredentials credentials =
@@ -1268,11 +1266,11 @@ public void getTrustBoundaryUrl_workload() throws IOException {
String expectedUrl =
"https://iamcredentials.googleapis.com/v1/projects/12345/locations/global/workloadIdentityPools/my-pool/allowedLocations";
- assertEquals(expectedUrl, credentials.getTrustBoundaryUrl());
+ assertEquals(expectedUrl, credentials.getRegionalAccessBoundaryUrl());
}
@Test
- public void getTrustBoundaryUrl_workforce() throws IOException {
+ public void getRegionalAccessBoundaryUrl_workforce() throws IOException {
String audience =
"//iam.googleapis.com/locations/global/workforcePools/my-pool/providers/my-provider";
ExternalAccountCredentials credentials =
@@ -1285,11 +1283,11 @@ public void getTrustBoundaryUrl_workforce() throws IOException {
String expectedUrl =
"https://iamcredentials.googleapis.com/v1/locations/global/workforcePools/my-pool/allowedLocations";
- assertEquals(expectedUrl, credentials.getTrustBoundaryUrl());
+ assertEquals(expectedUrl, credentials.getRegionalAccessBoundaryUrl());
}
@Test
- public void getTrustBoundaryUrl_invalidAudience_throws() {
+ public void getRegionalAccessBoundaryUrl_invalidAudience_throws() {
ExternalAccountCredentials credentials =
TestExternalAccountCredentials.newBuilder()
.setAudience("invalid-audience")
@@ -1301,7 +1299,7 @@ public void getTrustBoundaryUrl_invalidAudience_throws() {
assertThrows(
IllegalStateException.class,
() -> {
- credentials.getTrustBoundaryUrl();
+ credentials.getRegionalAccessBoundaryUrl();
});
assertEquals(
@@ -1311,12 +1309,13 @@ public void getTrustBoundaryUrl_invalidAudience_throws() {
}
@Test
- public void refresh_workload_trustBoundarySuccess() throws IOException {
+ public void refresh_workload_regionalAccessBoundarySuccess()
+ throws IOException, InterruptedException {
String audience =
"//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/my-pool/providers/my-provider";
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
ExternalAccountCredentials credentials =
@@ -1334,20 +1333,28 @@ public String retrieveSubjectToken() throws IOException {
return "dummy-subject-token";
}
};
- credentials.refresh();
- TrustBoundary trustBoundary = credentials.getTrustBoundary();
- assertNotNull(trustBoundary);
- assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
}
@Test
- public void refresh_workforce_trustBoundarySuccess() throws IOException {
+ public void refresh_workforce_regionalAccessBoundarySuccess()
+ throws IOException, InterruptedException {
String audience =
"//iam.googleapis.com/locations/global/workforcePools/my-pool/providers/my-provider";
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
ExternalAccountCredentials credentials =
@@ -1366,23 +1373,59 @@ public String retrieveSubjectToken() throws IOException {
}
};
- credentials.refresh();
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
- TrustBoundary trustBoundary = credentials.getTrustBoundary();
- assertNotNull(trustBoundary);
- assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
}
@Test
- public void refresh_impersonated_workload_trustBoundarySuccess() throws IOException {
+ public void refresh_impersonated_workload_regionalAccessBoundarySuccess()
+ throws IOException, InterruptedException {
+ String projectNumber = "12345";
+ String poolId = "my-pool";
+ String providerId = "my-provider";
String audience =
- "//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/my-pool/providers/my-provider";
+ String.format(
+ "//iam.googleapis.com/projects/%s/locations/global/workloadIdentityPools/%s/providers/%s",
+ projectNumber, poolId, providerId);
+
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
+ // 1. Setup distinct RABs for workload and impersonated identities.
+ String workloadRabUrl =
+ String.format(
+ IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKLOAD_POOL,
+ GOOGLE_DEFAULT_UNIVERSE,
+ projectNumber,
+ poolId);
+ RegionalAccessBoundary workloadRab =
+ new RegionalAccessBoundary("workload-encoded", Collections.singletonList("workload-loc"));
+ transportFactory.transport.addRegionalAccessBoundary(workloadRabUrl, workloadRab);
+
+ String saEmail =
+ ImpersonatedCredentials.extractTargetPrincipal(SERVICE_ACCOUNT_IMPERSONATION_URL);
+ String impersonatedRabUrl =
+ String.format(
+ IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
+ GOOGLE_DEFAULT_UNIVERSE,
+ saEmail);
+ RegionalAccessBoundary impersonatedRab =
+ new RegionalAccessBoundary(
+ "impersonated-encoded", Collections.singletonList("impersonated-loc"));
+ transportFactory.transport.addRegionalAccessBoundary(impersonatedRabUrl, impersonatedRab);
+
// Use a URL-based source that the mock transport can handle, to avoid file IO.
Map urlCredentialSourceMap = new HashMap<>();
urlCredentialSourceMap.put("url", "https://www.metadata.google.com");
@@ -1401,23 +1444,57 @@ public void refresh_impersonated_workload_trustBoundarySuccess() throws IOExcept
.setEnvironmentProvider(environmentProvider)
.build();
- credentials.refresh();
+ // First call: initiates async refresh.
+ Map> requestHeaders = credentials.getRequestMetadata();
+ assertNull(requestHeaders.get(RegionalAccessBoundary.HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
- TrustBoundary trustBoundary = credentials.getTrustBoundary();
- assertNotNull(trustBoundary);
- assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
+ // Second call: should have the IMPERSONATED header, not the workload one.
+ requestHeaders = credentials.getRequestMetadata();
+ assertEquals(
+ Arrays.asList("impersonated-encoded"),
+ requestHeaders.get(RegionalAccessBoundary.HEADER_KEY));
}
@Test
- public void refresh_impersonated_workforce_trustBoundarySuccess() throws IOException {
+ public void refresh_impersonated_workforce_regionalAccessBoundarySuccess()
+ throws IOException, InterruptedException {
+ String poolId = "my-pool";
+ String providerId = "my-provider";
String audience =
- "//iam.googleapis.com/locations/global/workforcePools/my-pool/providers/my-provider";
+ String.format(
+ "//iam.googleapis.com/locations/global/workforcePools/%s/providers/%s",
+ poolId, providerId);
+
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
+ // 1. Setup distinct RABs for workforce and impersonated identities.
+ String workforceRabUrl =
+ String.format(
+ IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL,
+ GOOGLE_DEFAULT_UNIVERSE,
+ poolId);
+ RegionalAccessBoundary workforceRab =
+ new RegionalAccessBoundary("workforce-encoded", Collections.singletonList("workforce-loc"));
+ transportFactory.transport.addRegionalAccessBoundary(workforceRabUrl, workforceRab);
+
+ String saEmail =
+ ImpersonatedCredentials.extractTargetPrincipal(SERVICE_ACCOUNT_IMPERSONATION_URL);
+ String impersonatedRabUrl =
+ String.format(
+ IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
+ GOOGLE_DEFAULT_UNIVERSE,
+ saEmail);
+ RegionalAccessBoundary impersonatedRab =
+ new RegionalAccessBoundary(
+ "impersonated-encoded", Collections.singletonList("impersonated-loc"));
+ transportFactory.transport.addRegionalAccessBoundary(impersonatedRabUrl, impersonatedRab);
+
// Use a URL-based source that the mock transport can handle, to avoid file IO.
Map urlCredentialSourceMap = new HashMap<>();
urlCredentialSourceMap.put("url", "https://www.metadata.google.com");
@@ -1437,11 +1514,29 @@ public void refresh_impersonated_workforce_trustBoundarySuccess() throws IOExcep
.setEnvironmentProvider(environmentProvider)
.build();
- credentials.refresh();
+ // First call: initiates async refresh.
+ Map> requestHeaders = credentials.getRequestMetadata();
+ assertNull(requestHeaders.get(RegionalAccessBoundary.HEADER_KEY));
- TrustBoundary trustBoundary = credentials.getTrustBoundary();
- assertNotNull(trustBoundary);
- assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have the IMPERSONATED header, not the workforce one.
+ requestHeaders = credentials.getRequestMetadata();
+ assertEquals(
+ Arrays.asList("impersonated-encoded"),
+ requestHeaders.get(RegionalAccessBoundary.HEADER_KEY));
+ }
+
+ private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (credentials.getRegionalAccessBoundary() == null
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (credentials.getRegionalAccessBoundary() == null) {
+ fail("Timed out waiting for regional access boundary refresh");
+ }
}
private GenericJson buildJsonIdentityPoolCredential() {
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
index edf8b104a..ac0ff54be 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
@@ -31,13 +31,12 @@
package com.google.auth.oauth2;
-import static com.google.auth.oauth2.TrustBoundary.TRUST_BOUNDARY_KEY;
+import static com.google.auth.oauth2.RegionalAccessBoundary.HEADER_KEY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -54,11 +53,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Test;
@@ -106,7 +102,9 @@ public class GoogleCredentialsTest extends BaseSerializationTest {
@After
public void tearDown() {
- TrustBoundary.setEnvironmentProviderForTest(null);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(null);
+ RegionalAccessBoundary.setClockForTest(Clock.SYSTEM);
+ RABManager.setClockForTest(Clock.SYSTEM);
}
@Test
@@ -796,6 +794,7 @@ public void serialize() throws IOException, ClassNotFoundException {
assertEquals(testCredentials.hashCode(), deserializedCredentials.hashCode());
assertEquals(testCredentials.toString(), deserializedCredentials.toString());
assertSame(deserializedCredentials.clock, Clock.SYSTEM);
+ assertNotNull(deserializedCredentials.rabManager);
}
@Test
@@ -948,10 +947,10 @@ public void getCredentialInfo_impersonatedServiceAccount() throws IOException {
}
@Test
- public void trustBoundary_shouldNotCallLookupEndpointWhenDisabled() throws IOException {
+ public void regionalAccessBoundary_shouldNotCallLookupEndpointWhenDisabled() throws IOException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "false");
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
+ environmentProvider.setEnv(RegionalAccessBoundary.ENABLE_EXPERIMENT_ENV_VAR, "false");
MockTokenServerTransport transport = new MockTokenServerTransport();
transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
@@ -966,21 +965,23 @@ public void trustBoundary_shouldNotCallLookupEndpointWhenDisabled() throws IOExc
.build();
credentials.getRequestMetadata();
- assertNull(credentials.getTrustBoundary());
+ assertNull(credentials.getRegionalAccessBoundary());
}
@Test
- public void trustBoundary_shouldFetchAndReturnTrustBoundaryDataSuccessfully() throws IOException {
+ public void regionalAccessBoundary_shouldFetchAndReturnRegionalAccessBoundaryDataSuccessfully()
+ throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
+ environmentProvider.setEnv(RegionalAccessBoundary.ENABLE_EXPERIMENT_ENV_VAR, "true");
MockTokenServerTransport transport = new MockTokenServerTransport();
transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
- TrustBoundary trustBoundary =
- new TrustBoundary(
- TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, Collections.singletonList("us-central1"));
- transport.setTrustBoundary(trustBoundary);
+ RegionalAccessBoundary regionalAccessBoundary =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ Collections.singletonList("us-central1"));
+ transport.setRegionalAccessBoundary(regionalAccessBoundary);
ServiceAccountCredentials credentials =
ServiceAccountCredentials.newBuilder()
@@ -991,25 +992,36 @@ public void trustBoundary_shouldFetchAndReturnTrustBoundaryDataSuccessfully() th
.setScopes(SCOPES)
.build();
+ // First call: returns no header, initiates async refresh.
Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
assertEquals(
- headers.get(TRUST_BOUNDARY_KEY), Arrays.asList(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION));
+ headers.get(HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
}
@Test
- public void trustBoundary_shouldRetryTrustBoundaryLookupOnFailure() throws IOException {
+ public void regionalAccessBoundary_shouldRetryRegionalAccessBoundaryLookupOnFailure()
+ throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
+ environmentProvider.setEnv(RegionalAccessBoundary.ENABLE_EXPERIMENT_ENV_VAR, "true");
// This transport will be used for the trust boundary lookup.
// We will configure it to fail on the first attempt.
- MockTokenServerTransport trustBoundaryTransport = new MockTokenServerTransport();
- trustBoundaryTransport.addResponseErrorSequence(new IOException("Service Unavailable"));
- TrustBoundary trustBoundary =
- new TrustBoundary(
- TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, TestUtils.TRUST_BOUNDARY_LOCATIONS);
- trustBoundaryTransport.setTrustBoundary(trustBoundary);
+ MockTokenServerTransport regionalAccessBoundaryTransport = new MockTokenServerTransport();
+ regionalAccessBoundaryTransport.addResponseErrorSequence(
+ new IOException("Service Unavailable"));
+ RegionalAccessBoundary regionalAccessBoundary =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS);
+ regionalAccessBoundaryTransport.setRegionalAccessBoundary(regionalAccessBoundary);
// This transport will be used for the access token refresh.
// It will succeed.
@@ -1029,7 +1041,7 @@ public void trustBoundary_shouldRetryTrustBoundaryLookupOnFailure() throws IOExc
public com.google.api.client.http.LowLevelHttpRequest buildRequest(
String method, String url) throws IOException {
if (url.endsWith("/allowedLocations")) {
- return trustBoundaryTransport.buildRequest(method, url);
+ return regionalAccessBoundaryTransport.buildRequest(method, url);
}
return accessTokenTransport.buildRequest(method, url);
}
@@ -1037,40 +1049,21 @@ public com.google.api.client.http.LowLevelHttpRequest buildRequest(
.setScopes(SCOPES)
.build();
+ credentials.getRequestMetadata();
+ waitForRegionalAccessBoundary(credentials);
+
Map> headers = credentials.getRequestMetadata();
assertEquals(
- Arrays.asList(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION), headers.get(TRUST_BOUNDARY_KEY));
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION),
+ headers.get(HEADER_KEY));
}
@Test
- public void trustBoundary_refreshShouldReturnNullWhenDefaultDomainIsNotGoogleApis()
+ public void regionalAccessBoundary_refreshShouldNotThrowWhenNoValidAccessTokenIsPassed()
throws IOException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
-
- MockTokenServerTransport transport = new MockTokenServerTransport();
- transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
-
- ServiceAccountCredentials credentials =
- ServiceAccountCredentials.newBuilder()
- .setClientEmail(SA_CLIENT_EMAIL)
- .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
- .setPrivateKeyId(SA_PRIVATE_KEY_ID)
- .setHttpTransportFactory(() -> transport)
- .setScopes(SCOPES)
- .setUniverseDomain("other.universe")
- .build();
-
- credentials.refreshAccessToken();
- assertNull(credentials.getTrustBoundary());
- }
-
- @Test
- public void trustBoundary_refreshShouldThrowWhenNoValidAccessTokenIsPassed() throws IOException {
- TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
+ environmentProvider.setEnv(RegionalAccessBoundary.ENABLE_EXPERIMENT_ENV_VAR, "true");
MockTokenServerTransport transport = new MockTokenServerTransport();
// Return an expired access token.
@@ -1086,20 +1079,22 @@ public void trustBoundary_refreshShouldThrowWhenNoValidAccessTokenIsPassed() thr
.setScopes(SCOPES)
.build();
- IllegalArgumentException exception =
- assertThrows(IllegalArgumentException.class, () -> credentials.getRequestMetadata());
- assertEquals("The provided access token is expired.", exception.getMessage());
+ // Should not throw, but just fail-open (no header).
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(HEADER_KEY));
}
@Test
- public void trustBoundary_refreshShouldReturnNoOpIfResponseFromLookupIsNoOp() throws IOException {
+ public void regionalAccessBoundary_cooldownDoublingAndRefresh()
+ throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
+ environmentProvider.setEnv(RegionalAccessBoundary.ENABLE_EXPERIMENT_ENV_VAR, "true");
MockTokenServerTransport transport = new MockTokenServerTransport();
transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
- transport.setTrustBoundary(new TrustBoundary("0x0", Collections.emptyList()));
+ // Always fail lookup for now.
+ transport.addResponseErrorSequence(new IOException("Persistent Failure"));
ServiceAccountCredentials credentials =
ServiceAccountCredentials.newBuilder()
@@ -1110,209 +1105,218 @@ public void trustBoundary_refreshShouldReturnNoOpIfResponseFromLookupIsNoOp() th
.setScopes(SCOPES)
.build();
- credentials.refresh();
+ TestClock testClock = new TestClock();
+ RABManager.setClockForTest(testClock);
+ RegionalAccessBoundary.setMaxRetryElapsedTimeMillisForTest(100);
- assertTrue(credentials.getTrustBoundary().isNoOp());
- }
+ // First attempt: triggers lookup, fails, enters 15m cooldown.
+ credentials.getRequestMetadata();
+ waitForCooldownActive(credentials);
+ assertTrue(credentials.rabManager.isCooldownActive());
+ assertEquals(15 * 60 * 1000L, credentials.rabManager.getCurrentCooldownMillis());
- @Test
- public void trustBoundary_refreshShouldReturnNoOpAndNotCallLookupEndpointWhenCachedIsNoOp()
- throws IOException {
- TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
+ // Second attempt (during cooldown): does not trigger lookup.
+ credentials.getRequestMetadata();
+ assertTrue(credentials.rabManager.isCooldownActive());
- MockTokenServerTransport transport = new MockTokenServerTransport();
- transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
- transport.setTrustBoundary(new TrustBoundary("0x0", Collections.emptyList()));
+ // Fast-forward past 15m cooldown.
+ testClock.advanceTime(16 * 60 * 1000L);
+ assertFalse(credentials.rabManager.isCooldownActive());
- ServiceAccountCredentials credentials =
- ServiceAccountCredentials.newBuilder()
- .setClientEmail(SA_CLIENT_EMAIL)
- .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
- .setPrivateKeyId(SA_PRIVATE_KEY_ID)
- .setHttpTransportFactory(() -> transport)
- .setScopes(SCOPES)
- .build();
+ // Third attempt (cooldown expired): triggers lookup, fails again, cooldown should double.
+ credentials.getRequestMetadata();
+ waitForCooldownActive(credentials);
+ assertTrue(credentials.rabManager.isCooldownActive());
+ assertEquals(30 * 60 * 1000L, credentials.rabManager.getCurrentCooldownMillis());
+
+ // Fast-forward past 30m cooldown.
+ testClock.advanceTime(31 * 60 * 1000L);
+ assertFalse(credentials.rabManager.isCooldownActive());
- // First refresh to cache the no-op trust boundary.
- credentials.refresh();
+ // Set successful response.
+ transport.setRegionalAccessBoundary(
+ new RegionalAccessBoundary("0x123", Collections.emptyList()));
- // Set trust boundary to a valid non No-Op value.
- transport.setTrustBoundary(
- new TrustBoundary(
- TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, TestUtils.TRUST_BOUNDARY_LOCATIONS));
+ // Fourth attempt: triggers lookup, succeeds, resets cooldown.
+ credentials.getRequestMetadata();
+ waitForRegionalAccessBoundary(credentials);
+ assertFalse(credentials.rabManager.isCooldownActive());
+ assertEquals("0x123", credentials.getRegionalAccessBoundary().getEncodedLocations());
+ assertEquals(15 * 60 * 1000L, credentials.rabManager.getCurrentCooldownMillis());
+ }
- // Refresh trust boundaries
- credentials.refresh();
+ @Test
+ public void regionalAccessBoundary_manualOverride() throws IOException {
+ RegionalAccessBoundary manualRAB =
+ new RegionalAccessBoundary("0x999", Collections.singletonList("us-east1"));
+ GoogleCredentials credentials =
+ GoogleCredentials.newBuilder()
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, null))
+ .setRegionalAccessBoundary(manualRAB)
+ .build();
- // Check whether the trust boundaries are still no_op.
- assertTrue(credentials.getTrustBoundary().isNoOp());
+ Map> headers = credentials.getRequestMetadata();
+ assertEquals(Collections.singletonList("0x999"), headers.get(HEADER_KEY));
}
@Test
- public void trustBoundary_refreshShouldReturnCachedTbIfCallToLookupFails() throws IOException {
+ public void regionalAccessBoundary_staleRabErrorInitiatesBackgroundLookup()
+ throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
+ environmentProvider.setEnv(RegionalAccessBoundary.ENABLE_EXPERIMENT_ENV_VAR, "true");
MockTokenServerTransport transport = new MockTokenServerTransport();
- transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
- TrustBoundary trustBoundary =
- new TrustBoundary(
- TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, TestUtils.TRUST_BOUNDARY_LOCATIONS);
- transport.setTrustBoundary(trustBoundary);
-
- ServiceAccountCredentials credentials =
- ServiceAccountCredentials.newBuilder()
- .setClientEmail(SA_CLIENT_EMAIL)
- .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
- .setPrivateKeyId(SA_PRIVATE_KEY_ID)
- .setHttpTransportFactory(() -> transport)
- .setScopes(SCOPES)
- .build();
+ // Ensure success response.
+ transport.setRegionalAccessBoundary(
+ new RegionalAccessBoundary("new-rab", Collections.singletonList("us-central1")));
- // First refresh to cache the trust boundary.
- credentials.refresh();
+ GoogleCredentials credentials = createTestCredentials(transport);
+ // Seed with an "old" RAB.
+ credentials.setRegionalAccessBoundary(
+ new RegionalAccessBoundary("old-rab", Collections.singletonList("us-central1")));
- // Set the trust boundary to be returned to null so we get an exception.
- transport.setTrustBoundary(null);
+ // Reactive refresh should clear cache and start a background lookup.
+ // We pass the token explicitly to avoid triggering a refresh inside getRequestMetadata.
+ credentials.reactiveRefreshRegionalAccessBoundary(
+ CALL_URI, new AccessToken(ACCESS_TOKEN, null));
- credentials.refresh();
+ // Current cache should be null (cleared).
+ assertNull(credentials.getRegionalAccessBoundary());
- assertEquals(
- TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION,
- credentials.getTrustBoundary().getEncodedLocations());
+ waitForRegionalAccessBoundary(credentials);
+ assertEquals("new-rab", credentials.getRegionalAccessBoundary().getEncodedLocations());
+ // Verify one lookup was made.
+ assertEquals(1, transport.getRegionalAccessBoundaryRequestCount());
}
@Test
- public void trustBoundary_refreshShouldThrowIfCallToLookupFailsAndNoCachedTb()
- throws IOException {
- TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
-
- MockTokenServerTransport transport = new MockTokenServerTransport();
- transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
- transport.addResponseErrorSequence(new IOException("Service Unavailable"));
+ public void regionalAccessBoundary_shouldFailOpenWhenRefreshCannotBeStarted() throws IOException {
+ // Use a simple AccessToken-based credential that won't try to refresh.
+ GoogleCredentials credentials = GoogleCredentials.create(new AccessToken("some-token", null));
- ServiceAccountCredentials credentials =
- ServiceAccountCredentials.newBuilder()
- .setClientEmail(SA_CLIENT_EMAIL)
- .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
- .setPrivateKeyId(SA_PRIVATE_KEY_ID)
- .setHttpTransportFactory(() -> transport)
- .setScopes(SCOPES)
- .build();
- IOException exception = assertThrows(IOException.class, () -> credentials.refresh());
- assertTrue(
- exception
- .getMessage()
- .contains("Failed to refresh trust boundary and no cached value is available."));
+ // Should not throw, but just fail-open (no header).
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(HEADER_KEY));
}
@Test
- public void trustBoundary_refreshShouldThrowInCaseOfMalformedResponse() throws IOException {
+ public void regionalAccessBoundary_deduplicationOfConcurrentRefreshes()
+ throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
+ environmentProvider.setEnv(RegionalAccessBoundary.ENABLE_EXPERIMENT_ENV_VAR, "true");
MockTokenServerTransport transport = new MockTokenServerTransport();
- transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
- // The transport will return a response with no encodedLocations field.
- transport.setTrustBoundary(new TrustBoundary(null, Collections.emptyList()));
+ transport.setRegionalAccessBoundary(
+ new RegionalAccessBoundary("valid", Collections.singletonList("us-central1")));
+ // Add delay to lookup to ensure threads overlap.
+ transport.setResponseDelayMillis(500);
+
+ GoogleCredentials credentials = createTestCredentials(transport);
+
+ // Fire multiple concurrent requests.
+ for (int i = 0; i < 10; i++) {
+ new Thread(
+ () -> {
+ try {
+ credentials.getRequestMetadata();
+ } catch (IOException e) {
+ }
+ })
+ .start();
+ }
- ServiceAccountCredentials credentials =
- ServiceAccountCredentials.newBuilder()
- .setClientEmail(SA_CLIENT_EMAIL)
- .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
- .setPrivateKeyId(SA_PRIVATE_KEY_ID)
- .setHttpTransportFactory(() -> transport)
- .setScopes(SCOPES)
- .build();
+ waitForRegionalAccessBoundary(credentials);
- IOException exception = assertThrows(IOException.class, () -> credentials.refresh());
- assertTrue(
- exception
- .getMessage()
- .contains("Failed to refresh trust boundary and no cached value is available."));
+ // Only ONE request should have been made to the lookup endpoint.
+ assertEquals(1, transport.getRegionalAccessBoundaryRequestCount());
}
@Test
- public void trustBoundary_getRequestHeadersShouldAttachTrustBoundaryHeader() throws IOException {
+ public void regionalAccessBoundary_shouldSkipRefreshForRegionalEndpoints() throws IOException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
+ environmentProvider.setEnv(RegionalAccessBoundary.ENABLE_EXPERIMENT_ENV_VAR, "true");
MockTokenServerTransport transport = new MockTokenServerTransport();
- transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
- TrustBoundary trustBoundary =
- new TrustBoundary(
- TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, Collections.singletonList("us-central1"));
- transport.setTrustBoundary(trustBoundary);
-
- ServiceAccountCredentials credentials =
- ServiceAccountCredentials.newBuilder()
- .setClientEmail(SA_CLIENT_EMAIL)
- .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
- .setPrivateKeyId(SA_PRIVATE_KEY_ID)
- .setHttpTransportFactory(() -> transport)
- .setScopes(SCOPES)
- .build();
+ GoogleCredentials credentials = createTestCredentials(transport);
- Map> headers = credentials.getRequestMetadata();
+ URI regionalUri = URI.create("https://storage.us-central1.rep.googleapis.com/v1/b/foo");
+ credentials.getRequestMetadata(regionalUri);
- assertEquals(
- Arrays.asList(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION), headers.get(TRUST_BOUNDARY_KEY));
+ // Should not have triggered any lookup.
+ assertEquals(0, transport.getRegionalAccessBoundaryRequestCount());
}
@Test
- public void trustBoundary_getRequestHeadersShouldAttachEmptyStringTbHeaderInCaseOfNoOp()
- throws IOException {
+ public void regionalAccessBoundary_staleHeaderPrevention()
+ throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
+ environmentProvider.setEnv(RegionalAccessBoundary.ENABLE_EXPERIMENT_ENV_VAR, "true");
MockTokenServerTransport transport = new MockTokenServerTransport();
- transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
- transport.setTrustBoundary(new TrustBoundary("0x0", Collections.emptyList()));
+ // Start with an expired RAB (using 0 as expiration timestamp).
+ RegionalAccessBoundary expiredRAB =
+ new RegionalAccessBoundary("expired-rab", Collections.singletonList("us-central1"), 0L);
+ transport.setRegionalAccessBoundary(expiredRAB);
- ServiceAccountCredentials credentials =
- ServiceAccountCredentials.newBuilder()
- .setClientEmail(SA_CLIENT_EMAIL)
- .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
- .setPrivateKeyId(SA_PRIVATE_KEY_ID)
- .setHttpTransportFactory(() -> transport)
- .setScopes(SCOPES)
- .build();
+ GoogleCredentials credentials = createTestCredentials(transport);
+ credentials.setRegionalAccessBoundary(expiredRAB);
- Map> headers = credentials.getRequestMetadata();
+ // Verify it is considered expired.
+ assertNull(credentials.getRegionalAccessBoundary());
- assertEquals(Arrays.asList(""), headers.get(TRUST_BOUNDARY_KEY));
+ // Call should NOT have the stale header.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(HEADER_KEY));
}
- @Test
- public void trustBoundary_getRequestHeadersShouldNotAttachTbHeaderInCaseOfNonGduUniverse()
+ private GoogleCredentials createTestCredentials(MockTokenServerTransport transport)
throws IOException {
- TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv(TrustBoundary.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED_ENV_VAR, "true");
-
- MockTokenServerTransport transport = new MockTokenServerTransport();
transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
+ return new ServiceAccountCredentials.Builder()
+ .setClientEmail(SA_CLIENT_EMAIL)
+ .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
+ .setPrivateKeyId(SA_PRIVATE_KEY_ID)
+ .setHttpTransportFactory(() -> transport)
+ .setScopes(SCOPES)
+ .build();
+ }
+
+ private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (credentials.getRegionalAccessBoundary() == null
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (credentials.getRegionalAccessBoundary() == null) {
+ fail("Timed out waiting for regional access boundary refresh");
+ }
+ }
- ServiceAccountCredentials credentials =
- ServiceAccountCredentials.newBuilder()
- .setClientEmail(SA_CLIENT_EMAIL)
- .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
- .setPrivateKeyId(SA_PRIVATE_KEY_ID)
- .setHttpTransportFactory(() -> transport)
- .setScopes(SCOPES)
- .setUniverseDomain("other.universe")
- .build();
+ private void waitForCooldownActive(GoogleCredentials credentials) throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (!credentials.rabManager.isCooldownActive() && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (!credentials.rabManager.isCooldownActive()) {
+ fail("Timed out waiting for cooldown to become active");
+ }
+ }
- Map> headers = credentials.getRequestMetadata();
+ private static class TestClock implements Clock {
+ private final AtomicLong currentTime = new AtomicLong(System.currentTimeMillis());
- assertNull(headers.get(TRUST_BOUNDARY_KEY));
+ @Override
+ public long currentTimeMillis() {
+ return currentTime.get();
+ }
+
+ public void advanceTime(long millis) {
+ currentTime.addAndGet(millis);
+ }
}
}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
index a48e56c7e..9a2fc6193 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
@@ -1306,9 +1306,9 @@ void setShouldThrowOnGetCertificatePath(boolean shouldThrow) {
}
@Test
- public void testRefresh_trustBoundarySuccess() throws IOException {
+ public void testRefresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
MockExternalAccountCredentialsTransportFactory transportFactory =
@@ -1325,11 +1325,29 @@ public void testRefresh_trustBoundarySuccess() throws IOException {
.setTokenUrl(STS_URL)
.build();
- credentials.refresh();
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.HEADER_KEY));
- TrustBoundary trustBoundary = credentials.getTrustBoundary();
- assertNotNull(trustBoundary);
- assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
- TrustBoundary.setEnvironmentProviderForTest(null);
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ RegionalAccessBoundary.setEnvironmentProviderForTest(null);
+ }
+
+ private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (credentials.getRegionalAccessBoundary() == null
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (credentials.getRegionalAccessBoundary() == null) {
+ fail("Timed out waiting for regional access boundary refresh");
+ }
}
}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
index abe5d4ed7..d8c3fef2b 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
@@ -31,7 +31,7 @@
package com.google.auth.oauth2;
-import static com.google.auth.oauth2.TrustBoundary.TRUST_BOUNDARY_KEY;
+import static com.google.auth.oauth2.RegionalAccessBoundary.HEADER_KEY;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -156,9 +156,10 @@ public class ImpersonatedCredentialsTest extends BaseSerializationTest {
private static final String REFRESH_TOKEN = "dasdfasdffa4ffdfadgyjirasdfadsft";
public static final List DELEGATES =
Arrays.asList("sa1@developer.gserviceaccount.com", "sa2@developer.gserviceaccount.com");
- public static final TrustBoundary TRUST_BOUNDARY =
- new TrustBoundary(
- TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, TestUtils.TRUST_BOUNDARY_LOCATIONS);
+ public static final RegionalAccessBoundary REGIONAL_ACCESS_BOUNDARY =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS);
private GoogleCredentials sourceCredentials;
private MockIAMCredentialsServiceTransportFactory mockTransportFactory;
@@ -171,7 +172,7 @@ public void setup() throws IOException {
@After
public void tearDown() {
- TrustBoundary.setEnvironmentProviderForTest(null);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(null);
}
static GoogleCredentials getSourceCredentials() throws IOException {
@@ -187,7 +188,7 @@ static GoogleCredentials getSourceCredentials() throws IOException {
.setHttpTransportFactory(transportFactory)
.build();
transportFactory.transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
- transportFactory.transport.setTrustBoundary(TRUST_BOUNDARY);
+ transportFactory.transport.setRegionalAccessBoundary(REGIONAL_ACCESS_BOUNDARY);
return sourceCredentials;
}
@@ -1315,15 +1316,15 @@ public void serialize() throws IOException, ClassNotFoundException {
}
@Test
- public void refresh_trustBoundarySuccess() throws IOException {
+ public void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
- // Mock trust boundary response
- TrustBoundary trustBoundary = TRUST_BOUNDARY;
+ // Mock regional access boundary response
+ RegionalAccessBoundary regionalAccessBoundary = REGIONAL_ACCESS_BOUNDARY;
- mockTransportFactory.getTransport().setTrustBoundary(trustBoundary);
+ mockTransportFactory.getTransport().setRegionalAccessBoundary(regionalAccessBoundary);
mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN);
mockTransportFactory.getTransport().setExpireTime(getDefaultExpireTime());
@@ -1340,40 +1341,29 @@ public void refresh_trustBoundarySuccess() throws IOException {
VALID_LIFETIME,
mockTransportFactory);
+ // First call: initiates async refresh.
Map> headers = targetCredentials.getRequestMetadata();
- assertEquals(
- headers.get(TRUST_BOUNDARY_KEY),
- Collections.singletonList(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION));
- }
+ assertNull(headers.get(HEADER_KEY));
- @Test
- public void refresh_trustBoundaryFails_throwsIOException() throws IOException {
- TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
- environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
+ waitForRegionalAccessBoundary(targetCredentials);
- mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
- mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN);
- mockTransportFactory.getTransport().setExpireTime(getDefaultExpireTime());
- mockTransportFactory
- .getTransport()
- .addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "", true);
-
- ImpersonatedCredentials targetCredentials =
- ImpersonatedCredentials.create(
- sourceCredentials,
- IMPERSONATED_CLIENT_EMAIL,
- null,
- IMMUTABLE_SCOPES_LIST,
- VALID_LIFETIME,
- mockTransportFactory);
+ // Second call: should have header.
+ headers = targetCredentials.getRequestMetadata();
+ assertEquals(
+ headers.get(HEADER_KEY),
+ Collections.singletonList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
- IOException exception = assertThrows(IOException.class, () -> targetCredentials.refresh());
- assertTrue(
- "The exception message should explain why the refresh failed.",
- exception
- .getMessage()
- .contains("Failed to refresh trust boundary and no cached value is available."));
+ private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (credentials.getRegionalAccessBoundary() == null
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (credentials.getRegionalAccessBoundary() == null) {
+ fail("Timed out waiting for regional access boundary refresh");
+ }
}
public static String getDefaultExpireTime() {
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
index 7be65adf3..33e1998df 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
@@ -50,6 +50,7 @@
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
@@ -68,7 +69,7 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
private static final String AWS_IMDSV2_SESSION_TOKEN_URL = "https://169.254.169.254/imdsv2";
private static final String METADATA_SERVER_URL = "https://www.metadata.google.com";
private static final String STS_URL = "https://sts.googleapis.com/v1/token";
- private static final String TRUST_BOUNDARY_URL_END = "/allowedLocations";
+ private static final String REGIONAL_ACCESS_BOUNDARY_URL_END = "/allowedLocations";
private static final String SUBJECT_TOKEN = "subjectToken";
private static final String TOKEN_TYPE = "Bearer";
@@ -93,6 +94,11 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
private String expireTime;
private String metadataServerContentType;
private String stsContent;
+ private final Map regionalAccessBoundaries = new HashMap<>();
+
+ public void addRegionalAccessBoundary(String url, RegionalAccessBoundary regionalAccessBoundary) {
+ this.regionalAccessBoundaries.put(url, regionalAccessBoundary);
+ }
public void addResponseErrorSequence(IOException... errors) {
Collections.addAll(responseErrorSequence, errors);
@@ -198,11 +204,18 @@ public LowLevelHttpResponse execute() throws IOException {
if (url.contains(IAM_ENDPOINT)) {
- if (url.endsWith(TRUST_BOUNDARY_URL_END)) {
+ if (url.endsWith(REGIONAL_ACCESS_BOUNDARY_URL_END)) {
+ RegionalAccessBoundary rab = regionalAccessBoundaries.get(url);
+ if (rab == null) {
+ rab =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS);
+ }
GenericJson responseJson = new GenericJson();
responseJson.setFactory(OAuth2Utils.JSON_FACTORY);
- responseJson.put("encodedLocations", TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION);
- responseJson.put("locations", TestUtils.TRUST_BOUNDARY_LOCATIONS);
+ responseJson.put("encodedLocations", rab.getEncodedLocations());
+ responseJson.put("locations", rab.getLocations());
String content = responseJson.toPrettyString();
return new MockLowLevelHttpResponse()
.setContentType(Json.MEDIA_TYPE)
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java
index 9994f3040..5346f4fdb 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java
@@ -80,7 +80,7 @@ public ServerResponse(int statusCode, String response, boolean repeatServerRespo
private String universeDomain;
- private TrustBoundary trustBoundary;
+ private RegionalAccessBoundary regionalAccessBoundary;
private MockLowLevelHttpRequest request;
@@ -134,8 +134,8 @@ public void setAccessTokenEndpoint(String accessTokenEndpoint) {
this.iamAccessTokenEndpoint = accessTokenEndpoint;
}
- public void setTrustBoundary(TrustBoundary trustBoundary) {
- this.trustBoundary = trustBoundary;
+ public void setRegionalAccessBoundary(RegionalAccessBoundary regionalAccessBoundary) {
+ this.regionalAccessBoundary = regionalAccessBoundary;
}
public MockLowLevelHttpRequest getRequest() {
@@ -232,13 +232,13 @@ public LowLevelHttpResponse execute() throws IOException {
new MockLowLevelHttpRequest(url) {
@Override
public LowLevelHttpResponse execute() throws IOException {
- if (trustBoundary == null) {
+ if (regionalAccessBoundary == null) {
return new MockLowLevelHttpResponse().setStatusCode(404);
}
GenericJson responseJson = new GenericJson();
responseJson.setFactory(OAuth2Utils.JSON_FACTORY);
- responseJson.put("encodedLocations", trustBoundary.getEncodedLocations());
- responseJson.put("locations", trustBoundary.getLocations());
+ responseJson.put("encodedLocations", regionalAccessBoundary.getEncodedLocations());
+ responseJson.put("locations", regionalAccessBoundary.getLocations());
String content = responseJson.toPrettyString();
return new MockLowLevelHttpResponse()
.setContentType(Json.MEDIA_TYPE)
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java
index 3e90d0faa..70012330b 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java
@@ -73,7 +73,8 @@ public class MockMetadataServerTransport extends MockHttpTransport {
private boolean emptyContent;
private MockLowLevelHttpRequest request;
- private TrustBoundary trustBoundary;
+ private RegionalAccessBoundary regionalAccessBoundary;
+ private IOException lookupError;
public MockMetadataServerTransport() {}
@@ -122,8 +123,12 @@ public void setEmptyContent(boolean emptyContent) {
this.emptyContent = emptyContent;
}
- public void setTrustBoundary(TrustBoundary trustBoundary) {
- this.trustBoundary = trustBoundary;
+ public void setRegionalAccessBoundary(RegionalAccessBoundary regionalAccessBoundary) {
+ this.regionalAccessBoundary = regionalAccessBoundary;
+ }
+
+ public void setLookupError(IOException lookupError) {
+ this.lookupError = lookupError;
}
public MockLowLevelHttpRequest getRequest() {
@@ -147,7 +152,7 @@ public LowLevelHttpRequest buildRequest(String method, String url) throws IOExce
} else if (isMtlsConfigRequestUrl(url)) {
return getMockRequestForMtlsConfig(url);
} else if (isIamLookupUrl(url)) {
- return getMockRequestForTrustBoundaryLookup(url);
+ return getMockRequestForRegionalAccessBoundaryLookup(url);
}
this.request =
new MockLowLevelHttpRequest(url) {
@@ -370,17 +375,20 @@ protected boolean isMtlsConfigRequestUrl(String url) {
+ SecureSessionAgent.S2A_CONFIG_ENDPOINT_POSTFIX);
}
- private MockLowLevelHttpRequest getMockRequestForTrustBoundaryLookup(String url) {
+ private MockLowLevelHttpRequest getMockRequestForRegionalAccessBoundaryLookup(String url) {
return new MockLowLevelHttpRequest(url) {
@Override
public LowLevelHttpResponse execute() throws IOException {
- if (trustBoundary == null) {
+ if (lookupError != null) {
+ throw lookupError;
+ }
+ if (regionalAccessBoundary == null) {
return new MockLowLevelHttpResponse().setStatusCode(404);
}
GenericJson responseJson = new GenericJson();
responseJson.setFactory(OAuth2Utils.JSON_FACTORY);
- responseJson.put("encodedLocations", trustBoundary.getEncodedLocations());
- responseJson.put("locations", trustBoundary.getLocations());
+ responseJson.put("encodedLocations", regionalAccessBoundary.getEncodedLocations());
+ responseJson.put("locations", regionalAccessBoundary.getLocations());
String content = responseJson.toPrettyString();
return new MockLowLevelHttpResponse().setContentType(Json.MEDIA_TYPE).setContent(content);
}
@@ -388,7 +396,7 @@ public LowLevelHttpResponse execute() throws IOException {
}
protected boolean isIamLookupUrl(String url) {
- // Mocking call to the /allowedLocations endpoint for trust boundary refresh.
+ // Mocking call to the /allowedLocations endpoint for regional access boundary refresh.
// For testing convenience, this mock transport handles
// the /allowedLocations endpoint. The actual server for this endpoint
// will be the IAM Credentials API.
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java
index e3a50d027..8d98d430c 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java
@@ -62,7 +62,7 @@ public final class MockStsTransport extends MockHttpTransport {
private static final String ISSUED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
private static final String VALID_STS_PATTERN =
"https:\\/\\/sts.[a-z-_\\.]+\\/v1\\/(token|oauthtoken)";
- private static final String VALID_TRUST_BOUNDARY_PATTERN =
+ private static final String VALID_REGIONAL_ACCESS_BOUNDARY_PATTERN =
"https:\\/\\/iam.[a-z-_\\.]+\\/v1\\/.*\\/allowedLocations";
private static final String ACCESS_TOKEN = "accessToken";
private static final String TOKEN_TYPE = "Bearer";
@@ -103,15 +103,16 @@ public LowLevelHttpRequest buildRequest(final String method, final String url) {
public LowLevelHttpResponse execute() throws IOException {
// Mocking call to refresh trust boundaries.
// The lookup endpoint is located in the IAM server.
- Matcher trustBoundaryMatcher =
- Pattern.compile(VALID_TRUST_BOUNDARY_PATTERN).matcher(url);
- if (trustBoundaryMatcher.matches()) {
- // Mocking call to the /allowedLocations endpoint for trust boundary refresh.
+ Matcher regionalAccessBoundaryMatcher =
+ Pattern.compile(VALID_REGIONAL_ACCESS_BOUNDARY_PATTERN).matcher(url);
+ if (regionalAccessBoundaryMatcher.matches()) {
+ // Mocking call to the /allowedLocations endpoint for regional access boundary
+ // refresh.
// For testing convenience, this mock transport handles
// the /allowedLocations endpoint.
GenericJson response = new GenericJson();
- response.put("locations", TestUtils.TRUST_BOUNDARY_LOCATIONS);
- response.put("encodedLocations", TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION);
+ response.put("locations", TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS);
+ response.put("encodedLocations", TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION);
return new MockLowLevelHttpResponse()
.setContentType(Json.MEDIA_TYPE)
.setContent(OAuth2Utils.JSON_FACTORY.toString(response));
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java
index 76ef3f807..42b4396fd 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java
@@ -77,10 +77,25 @@ public class MockTokenServerTransport extends MockHttpTransport {
private MockLowLevelHttpRequest request;
private ClientAuthenticationType clientAuthenticationType;
private PKCEProvider pkceProvider;
- private TrustBoundary trustBoundary;
+ private RegionalAccessBoundary regionalAccessBoundary;
+ private final Map regionalAccessBoundaries = new HashMap<>();
+ private int regionalAccessBoundaryRequestCount = 0;
+ private int responseDelayMillis = 0;
- public void setTrustBoundary(TrustBoundary trustBoundary) {
- this.trustBoundary = trustBoundary;
+ public void setRegionalAccessBoundary(RegionalAccessBoundary regionalAccessBoundary) {
+ this.regionalAccessBoundary = regionalAccessBoundary;
+ }
+
+ public void addRegionalAccessBoundary(String url, RegionalAccessBoundary regionalAccessBoundary) {
+ this.regionalAccessBoundaries.put(url, regionalAccessBoundary);
+ }
+
+ public int getRegionalAccessBoundaryRequestCount() {
+ return regionalAccessBoundaryRequestCount;
+ }
+
+ public void setResponseDelayMillis(int responseDelayMillis) {
+ this.responseDelayMillis = responseDelayMillis;
}
public MockTokenServerTransport() {}
@@ -327,7 +342,7 @@ public LowLevelHttpResponse execute() throws IOException {
};
return request;
} else if (urlWithoutQuery.endsWith("/allowedLocations")) {
- // Mocking call to the /allowedLocations endpoint for trust boundary refresh.
+ // Mocking call to the /allowedLocations endpoint for regional access boundary refresh.
// For testing convenience, this mock transport handles
// the /allowedLocations endpoint. The actual server for this endpoint
// will be the IAM Credentials API.
@@ -335,13 +350,25 @@ public LowLevelHttpResponse execute() throws IOException {
new MockLowLevelHttpRequest(url) {
@Override
public LowLevelHttpResponse execute() throws IOException {
- if (trustBoundary == null) {
+ regionalAccessBoundaryRequestCount++;
+ if (responseDelayMillis > 0) {
+ try {
+ Thread.sleep(responseDelayMillis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ RegionalAccessBoundary rab = regionalAccessBoundaries.get(url);
+ if (rab == null) {
+ rab = regionalAccessBoundary;
+ }
+ if (rab == null) {
return new MockLowLevelHttpResponse().setStatusCode(404);
}
GenericJson responseJson = new GenericJson();
responseJson.setFactory(JSON_FACTORY);
- responseJson.put("encodedLocations", trustBoundary.getEncodedLocations());
- responseJson.put("locations", trustBoundary.getLocations());
+ responseJson.put("encodedLocations", rab.getEncodedLocations());
+ responseJson.put("locations", rab.getLocations());
String content = responseJson.toPrettyString();
return new MockLowLevelHttpResponse()
.setContentType(Json.MEDIA_TYPE)
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java
index fa21acc0e..81fc4879a 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java
@@ -604,9 +604,9 @@ public void serialize() throws IOException, ClassNotFoundException {
}
@Test
- public void testRefresh_trustBoundarySuccess() throws IOException {
+ public void testRefresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
MockExternalAccountCredentialsTransportFactory transportFactory =
@@ -624,11 +624,30 @@ public void testRefresh_trustBoundarySuccess() throws IOException {
.setExecutableHandler(options -> "pluggableAuthToken")
.build();
- credentials.refresh();
- TrustBoundary trustBoundary = credentials.getTrustBoundary();
- assertNotNull(trustBoundary);
- assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
- TrustBoundary.setEnvironmentProviderForTest(null);
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ RegionalAccessBoundary.setEnvironmentProviderForTest(null);
+ }
+
+ private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (credentials.getRegionalAccessBoundary() == null
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (credentials.getRegionalAccessBoundary() == null) {
+ fail("Timed out waiting for regional access boundary refresh");
+ }
}
private static PluggableAuthCredentialSource buildCredentialSource() {
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java
index ca275f1e3..62f7d4012 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java
@@ -31,7 +31,7 @@
package com.google.auth.oauth2;
-import static com.google.auth.oauth2.TrustBoundary.TRUST_BOUNDARY_KEY;
+import static com.google.auth.oauth2.RegionalAccessBoundary.HEADER_KEY;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -164,7 +164,7 @@ static ServiceAccountCredentials.Builder createDefaultBuilder() throws IOExcepti
@After
public void tearDown() {
- TrustBoundary.setEnvironmentProviderForTest(null);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(null);
}
@Test
@@ -1810,19 +1810,20 @@ public void createScopes_existingAccessTokenInvalidated() throws IOException {
}
@Test
- public void refresh_trustBoundarySuccess() throws IOException {
+ public void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
- // Mock trust boundary response
- TrustBoundary trustBoundary =
- new TrustBoundary(
- TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, TestUtils.TRUST_BOUNDARY_LOCATIONS);
+ // Mock regional access boundary response
+ RegionalAccessBoundary regionalAccessBoundary =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS);
MockTokenServerTransport transport = new MockTokenServerTransport();
transport.addServiceAccount(CLIENT_EMAIL, "test-access-token");
- transport.setTrustBoundary(trustBoundary);
+ transport.setRegionalAccessBoundary(regionalAccessBoundary);
ServiceAccountCredentials credentials =
ServiceAccountCredentials.newBuilder()
@@ -1834,19 +1835,33 @@ public void refresh_trustBoundarySuccess() throws IOException {
.setScopes(SCOPES)
.build();
+ // First call: initiates async refresh.
Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
assertEquals(
- headers.get(TRUST_BOUNDARY_KEY), Arrays.asList(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION));
+ headers.get(HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
}
@Test
- public void refresh_trustBoundaryFails_throwsIOException() throws IOException {
+ public void refresh_regionalAccessBoundary_selfSignedJWT()
+ throws IOException, InterruptedException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
- TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
+ RegionalAccessBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
+ RegionalAccessBoundary regionalAccessBoundary =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS);
+
MockTokenServerTransport transport = new MockTokenServerTransport();
- transport.addServiceAccount(CLIENT_EMAIL, "test-access-token");
+ transport.setRegionalAccessBoundary(regionalAccessBoundary);
ServiceAccountCredentials credentials =
ServiceAccountCredentials.newBuilder()
@@ -1855,14 +1870,30 @@ public void refresh_trustBoundaryFails_throwsIOException() throws IOException {
OAuth2Utils.privateKeyFromPkcs8(ServiceAccountCredentialsTest.PRIVATE_KEY_PKCS8))
.setPrivateKeyId("test-key-id")
.setHttpTransportFactory(() -> transport)
+ .setUseJwtAccessWithScope(true)
.setScopes(SCOPES)
.build();
- IOException exception = assertThrows(IOException.class, () -> credentials.refresh());
- assertTrue(
- "The exception message should explain why the refresh failed.",
- exception
- .getMessage()
- .contains("Failed to refresh trust boundary and no cached value is available."));
+
+ // First call: initiates async refresh using the SSJWT as the token.
+ credentials.getRequestMetadata();
+
+ waitForRegionalAccessBoundary(credentials);
+
+ assertEquals(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ credentials.getRegionalAccessBoundary().getEncodedLocations());
+ }
+
+ private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (credentials.getRegionalAccessBoundary() == null
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (credentials.getRegionalAccessBoundary() == null) {
+ fail("Timed out waiting for regional access boundary refresh");
+ }
}
private void verifyJwtAccess(Map> metadata, String expectedScopeClaim)