diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java
index ad5fb8e7dcf3..c5b8e0d3adbf 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java
@@ -41,6 +41,7 @@
import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
+import com.google.api.core.InternalApi;
import com.google.auth.CredentialTypeForMetrics;
import com.google.auth.Credentials;
import com.google.auth.Retryable;
@@ -71,6 +72,7 @@
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.regex.Pattern;
/**
* OAuth2 credentials representing the built-in service account for a Google Compute Engine VM.
@@ -80,7 +82,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 {
+ implements ServiceAccountSigner, IdTokenProvider, RegionalAccessBoundaryProvider {
static final String METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE =
"Empty content from metadata token server request.";
@@ -116,6 +118,7 @@ public class ComputeEngineCredentials extends GoogleCredentials
private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
private static final String PARSE_ERROR_ACCOUNT = "Error parsing service account response. ";
+ private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@[^@]+\\.[^@]+$");
private static final long serialVersionUID = -4113476462526554235L;
private final String transportFactoryClassName;
@@ -454,7 +457,6 @@ public AccessToken refreshAccessToken() throws IOException {
int expiresInSeconds =
OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000;
-
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
}
@@ -779,6 +781,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() {
@@ -792,6 +799,24 @@ public String getAccount() {
return principal;
}
+ @InternalApi
+ @Override
+ public String getRegionalAccessBoundaryUrl() throws IOException {
+ String account = getAccount();
+ // The MDS may return a non-email value for the account and we should skip RAB refresh in that
+ // scenario.
+ if (account == null || !EMAIL_PATTERN.matcher(account).matches()) {
+ LoggingUtils.log(
+ LOGGER_PROVIDER,
+ Level.INFO,
+ Collections.emptyMap(),
+ "Unable to retrieve this instance's email and will skip the regional request routing. Proceeding with request");
+ return null;
+ }
+ return String.format(
+ OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, account);
+ }
+
/**
* Signs the provided bytes using the private key associated with the service account.
*
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java
index b274fec76c65..81f95b6de3cb 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java
@@ -31,7 +31,9 @@
package com.google.auth.oauth2;
+import static com.google.auth.oauth2.OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL;
import static com.google.auth.oauth2.OAuth2Utils.JSON_FACTORY;
+import static com.google.auth.oauth2.OAuth2Utils.WORKFORCE_AUDIENCE_PATTERN;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
@@ -43,6 +45,7 @@
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Preconditions;
+import com.google.api.core.InternalApi;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.base.MoreObjects;
import com.google.common.io.BaseEncoding;
@@ -54,6 +57,7 @@
import java.util.Date;
import java.util.Map;
import java.util.Objects;
+import java.util.regex.Matcher;
import javax.annotation.Nullable;
/**
@@ -74,7 +78,8 @@
* }
*
*/
-public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials {
+public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials
+ implements RegionalAccessBoundaryProvider {
private static final LoggerProvider LOGGER_PROVIDER =
LoggerProvider.forClazz(ExternalAccountAuthorizedUserCredentials.class);
@@ -229,6 +234,24 @@ public AccessToken refreshAccessToken() throws IOException {
.build();
}
+ @InternalApi
+ @Override
+ public String getRegionalAccessBoundaryUrl() throws IOException {
+ Matcher matcher = WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience());
+ if (!matcher.matches()) {
+ throw new IllegalStateException(
+ "The provided audience is not in the correct format for a workforce pool. "
+ + "Refer: https://docs.cloud.google.com/iam/docs/principal-identifiers");
+ }
+ String poolId = matcher.group("pool");
+ return String.format(IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL, poolId);
+ }
+
+ @Override
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
@Nullable
public String getAudience() {
return audience;
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
index 47cb398d26bf..7e762b3b60b2 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
@@ -31,11 +31,14 @@
package com.google.auth.oauth2;
+import static com.google.auth.oauth2.OAuth2Utils.WORKFORCE_AUDIENCE_PATTERN;
+import static com.google.auth.oauth2.OAuth2Utils.WORKLOAD_AUDIENCE_PATTERN;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.json.GenericJson;
import com.google.api.client.util.Data;
+import com.google.api.core.InternalApi;
import com.google.auth.RequestMetadataCallback;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.base.MoreObjects;
@@ -54,6 +57,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
@@ -63,7 +67,8 @@
*
Handles initializing external credentials, calls to the Security Token Service, and service
* account impersonation.
*/
-public abstract class ExternalAccountCredentials extends GoogleCredentials {
+public abstract class ExternalAccountCredentials extends GoogleCredentials
+ implements RegionalAccessBoundaryProvider {
private static final long serialVersionUID = 8049126194174465023L;
@@ -577,6 +582,11 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
*/
public abstract String retrieveSubjectToken() throws IOException;
+ @Override
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
public String getAudience() {
return audience;
}
@@ -620,6 +630,37 @@ public String getServiceAccountEmail() {
return ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
}
+ @InternalApi
+ @Override
+ public String getRegionalAccessBoundaryUrl() throws IOException {
+ if (getServiceAccountEmail() != null) {
+ return String.format(
+ OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
+ getServiceAccountEmail());
+ }
+
+ Matcher workforceMatcher = WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience());
+ if (workforceMatcher.matches()) {
+ String poolId = workforceMatcher.group("pool");
+ return String.format(
+ OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL, poolId);
+ }
+
+ Matcher workloadMatcher = WORKLOAD_AUDIENCE_PATTERN.matcher(getAudience());
+ if (workloadMatcher.matches()) {
+ String projectNumber = workloadMatcher.group("project");
+ String poolId = workloadMatcher.group("pool");
+ return String.format(
+ OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKLOAD_POOL,
+ projectNumber,
+ poolId);
+ }
+
+ throw new IllegalStateException(
+ "The provided audience is not in a valid format for either a workload identity pool or a workforce pool."
+ + " Refer: https://docs.cloud.google.com/iam/docs/principal-identifiers");
+ }
+
@Nullable
public String getClientId() {
return clientId;
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
index 7395274c4786..e423a68ac18b 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
@@ -36,6 +36,8 @@
import com.google.api.client.util.Preconditions;
import com.google.api.core.ObsoleteApi;
import com.google.auth.Credentials;
+import com.google.auth.RequestMetadataCallback;
+import com.google.auth.http.AuthHttpConstants;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
@@ -46,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;
@@ -106,6 +110,9 @@ String getFileType() {
private final String universeDomain;
private final boolean isExplicitUniverseDomain;
+ transient RegionalAccessBoundaryManager regionalAccessBoundaryManager =
+ new RegionalAccessBoundaryManager(clock);
+
protected final String quotaProjectId;
private static final DefaultCredentialsProvider defaultCredentialsProvider =
@@ -347,6 +354,139 @@ public GoogleCredentials createWithQuotaProject(String quotaProject) {
return this.toBuilder().setQuotaProjectId(quotaProject).build();
}
+ /**
+ * 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.
+ */
+ final RegionalAccessBoundary getRegionalAccessBoundary() {
+ return regionalAccessBoundaryManager.getCachedRAB();
+ }
+
+ /**
+ * 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) || !isDefaultUniverseDomain()) {
+ return;
+ }
+
+ // 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.getExpirationTimeMillis() != null
+ && token.getExpirationTimeMillis() < clock.currentTimeMillis())) {
+ return;
+ }
+
+ HttpTransportFactory transportFactory = getTransportFactory();
+ if (transportFactory == null) {
+ return;
+ }
+
+ regionalAccessBoundaryManager.triggerAsyncRefresh(
+ transportFactory, (RegionalAccessBoundaryProvider) this, token);
+ }
+
+ /**
+ * Extracts the self-signed JWT from the request metadata and triggers a Regional Access Boundary
+ * refresh if expired.
+ *
+ * @param uri The URI of the outbound request.
+ * @param requestMetadata The request metadata containing the authorization header.
+ */
+ void refreshRegionalAccessBoundaryWithSelfSignedJwtIfExpired(
+ @Nullable URI uri, Map> requestMetadata) {
+ List authHeaders = requestMetadata.get(AuthHttpConstants.AUTHORIZATION);
+ if (authHeaders != null && !authHeaders.isEmpty()) {
+ 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);
+ try {
+ refreshRegionalAccessBoundaryIfExpired(uri, wrappedToken);
+ } catch (IOException e) {
+ // Ignore failure in async refresh trigger.
+ }
+ }
+ }
+ }
+
+ /**
+ * 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);
+ metadata = addRegionalAccessBoundaryToRequestMetadata(uri, metadata);
+ try {
+ // Sets off an async refresh for request-metadata.
+ refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken());
+ } catch (IOException e) {
+ // Ignore failure in async refresh trigger.
+ }
+ 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,
+ final java.util.concurrent.Executor executor,
+ final RequestMetadataCallback callback) {
+ super.getRequestMetadata(
+ uri,
+ executor,
+ new RequestMetadataCallback() {
+ @Override
+ public void onSuccess(Map> metadata) {
+ metadata = addRegionalAccessBoundaryToRequestMetadata(uri, metadata);
+ 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);
+ }
+ });
+ }
+
/**
* Gets the universe domain for the credential.
*
@@ -390,22 +530,59 @@ boolean isDefaultUniverseDomain() throws IOException {
static Map> addQuotaProjectIdToRequestMetadata(
String quotaProjectId, Map> requestMetadata) {
Preconditions.checkNotNull(requestMetadata);
- Map> newRequestMetadata = new HashMap<>(requestMetadata);
if (quotaProjectId != null && !requestMetadata.containsKey(QUOTA_PROJECT_ID_HEADER_KEY)) {
- newRequestMetadata.put(
- QUOTA_PROJECT_ID_HEADER_KEY, Collections.singletonList(quotaProjectId));
+ return ImmutableMap.>builder()
+ .putAll(requestMetadata)
+ .put(QUOTA_PROJECT_ID_HEADER_KEY, Collections.singletonList(quotaProjectId))
+ .build();
+ }
+ return requestMetadata;
+ }
+
+ /**
+ * Adds Regional Access Boundary header to requestMetadata if available. Overwrites if present. If
+ * the current RAB is null, it removes any stale header that might have survived serialization.
+ *
+ * @param uri The URI of the request.
+ * @param requestMetadata The request metadata.
+ * @return a new map with Regional Access Boundary header added, updated, or removed
+ */
+ Map> addRegionalAccessBoundaryToRequestMetadata(
+ URI uri, Map> requestMetadata) {
+ Preconditions.checkNotNull(requestMetadata);
+
+ if (uri != null && uri.getHost() != null) {
+ String host = uri.getHost();
+ if (host.endsWith(".rep.googleapis.com") || host.endsWith(".rep.sandbox.googleapis.com")) {
+ return requestMetadata;
+ }
}
- return Collections.unmodifiableMap(newRequestMetadata);
+
+ RegionalAccessBoundary rab = getRegionalAccessBoundary();
+ if (rab != null) {
+ // Overwrite the header to ensure the most recent async update is used,
+ // preventing staleness if the token itself hasn't expired yet.
+ Map> newMetadata = new HashMap<>(requestMetadata);
+ newMetadata.put(
+ RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY,
+ Collections.singletonList(rab.getEncodedLocations()));
+ return ImmutableMap.copyOf(newMetadata);
+ } else if (requestMetadata.containsKey(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY)) {
+ // If RAB is null but the header exists (e.g., from a serialized cache), we must strip it
+ // to prevent sending stale data to the server.
+ Map> newMetadata = new HashMap<>(requestMetadata);
+ newMetadata.remove(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY);
+ return ImmutableMap.copyOf(newMetadata);
+ }
+ return requestMetadata;
}
@Override
protected Map> getAdditionalHeaders() {
- Map> headers = super.getAdditionalHeaders();
+ Map> headers = new HashMap<>(super.getAdditionalHeaders());
+
String quotaProjectId = this.getQuotaProjectId();
- if (quotaProjectId != null) {
- return addQuotaProjectIdToRequestMetadata(quotaProjectId, headers);
- }
- return headers;
+ return addQuotaProjectIdToRequestMetadata(quotaProjectId, headers);
}
/** Default constructor. */
@@ -516,6 +693,11 @@ public int hashCode() {
return Objects.hash(this.quotaProjectId, this.universeDomain, this.isExplicitUniverseDomain);
}
+ private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
+ input.defaultReadObject();
+ regionalAccessBoundaryManager = new RegionalAccessBoundaryManager(clock);
+ }
+
public static Builder newBuilder() {
return new Builder();
}
@@ -651,6 +833,16 @@ 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;
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java
index 34716d92b552..ede86c646d95 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java
@@ -44,6 +44,7 @@
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
+import com.google.api.core.InternalApi;
import com.google.api.core.ObsoleteApi;
import com.google.auth.CredentialTypeForMetrics;
import com.google.auth.ServiceAccountSigner;
@@ -98,7 +99,7 @@
*
*/
public class ImpersonatedCredentials extends GoogleCredentials
- implements ServiceAccountSigner, IdTokenProvider {
+ implements ServiceAccountSigner, IdTokenProvider, RegionalAccessBoundaryProvider {
private static final long serialVersionUID = -2133257318957488431L;
private static final int TWELVE_HOURS_IN_SECONDS = 43200;
@@ -327,10 +328,22 @@ public GoogleCredentials getSourceCredentials() {
return sourceCredentials;
}
+ @InternalApi
+ @Override
+ public String getRegionalAccessBoundaryUrl() throws IOException {
+ return String.format(
+ OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, getAccount());
+ }
+
int getLifetime() {
return this.lifetime;
}
+ @Override
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
public void setTransportFactory(HttpTransportFactory httpTransportFactory) {
this.transportFactory = httpTransportFactory;
}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java
index e17714c3eee8..ef1225d19a73 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java
@@ -62,7 +62,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
@@ -167,6 +166,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) {
@@ -178,8 +187,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 {
@@ -267,11 +282,8 @@ private AsyncRefreshResult getOrCreateRefreshTask() {
final ListenableFutureTask task =
ListenableFutureTask.create(
- new Callable() {
- @Override
- public OAuthValue call() throws Exception {
- return OAuthValue.create(refreshAccessToken(), getAdditionalHeaders());
- }
+ () -> {
+ return OAuthValue.create(refreshAccessToken(), getAdditionalHeaders());
});
refreshTask = new RefreshTask(task, new RefreshTaskListener(task));
@@ -376,7 +388,7 @@ public AccessToken refreshAccessToken() throws IOException {
/**
* Provide additional headers to return as request metadata.
*
- * @return additional headers
+ * @return additional headers.
*/
protected Map> getAdditionalHeaders() {
return EMPTY_EXTRA_HEADERS;
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
index 643c3dc7dc65..84cb62390fe7 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
@@ -68,6 +68,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Pattern;
/**
* Internal utilities for the com.google.auth.oauth2 namespace.
@@ -123,6 +124,22 @@ enum Pkcs8Algorithm {
static final double RETRY_MULTIPLIER = 2;
static final int DEFAULT_NUMBER_OF_RETRIES = 3;
+ static final Pattern WORKFORCE_AUDIENCE_PATTERN =
+ Pattern.compile(
+ "^//iam.googleapis.com/locations/(?[^/]+)/workforcePools/(?[^/]+)/providers/(?[^/]+)$");
+ static final Pattern WORKLOAD_AUDIENCE_PATTERN =
+ Pattern.compile(
+ "^//iam.googleapis.com/projects/(?[^/]+)/locations/(?[^/]+)/workloadIdentityPools/(?[^/]+)/providers/(?[^/]+)$");
+
+ static final String IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT =
+ "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s/allowedLocations";
+
+ static final String IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL =
+ "https://iamcredentials.googleapis.com/v1/locations/global/workforcePools/%s/allowedLocations";
+
+ static final String IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKLOAD_POOL =
+ "https://iamcredentials.googleapis.com/v1/projects/%s/locations/global/workloadIdentityPools/%s/allowedLocations";
+
// Includes expected server errors from Google token endpoint
// Other 5xx codes are either not used or retries are unlikely to succeed
public static final Set TOKEN_ENDPOINT_RETRYABLE_STATUS_CODES =
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java
new file mode 100644
index 000000000000..dfcbe8491cd5
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java
@@ -0,0 +1,255 @@
+/*
+ * 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.http.GenericUrl;
+import com.google.api.client.http.HttpBackOffIOExceptionHandler;
+import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler;
+import com.google.api.client.http.HttpIOExceptionHandler;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonObjectParser;
+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;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 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 RegionalAccessBoundary implements Serializable {
+
+ static final String X_ALLOWED_LOCATIONS_HEADER_KEY = "x-allowed-locations";
+ private static final long serialVersionUID = -2428522338274020302L;
+
+ static final long TTL_MILLIS = 6 * 60 * 60 * 1000L; // 6 hours
+ static final long REFRESH_THRESHOLD_MILLIS = 1 * 60 * 60 * 1000L; // 1 hour
+
+ private final String encodedLocations;
+ private final List locations;
+ private final long refreshTime;
+ private transient Clock clock;
+
+ /**
+ * Creates a new RegionalAccessBoundary instance.
+ *
+ * @param encodedLocations The encoded string representation of the allowed locations.
+ * @param locations A list of human-readable location strings.
+ * @param clock The clock used to set the creation time.
+ */
+ RegionalAccessBoundary(String encodedLocations, List locations, Clock clock) {
+ this(
+ encodedLocations,
+ locations,
+ clock != null ? clock.currentTimeMillis() : Clock.SYSTEM.currentTimeMillis(),
+ clock);
+ }
+
+ /**
+ * 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.
+ * @param clock The clock to use for expiration checks.
+ */
+ RegionalAccessBoundary(
+ String encodedLocations, List locations, long refreshTime, Clock clock) {
+ this.encodedLocations = encodedLocations;
+ this.locations =
+ locations == null
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(locations);
+ this.refreshTime = refreshTime;
+ this.clock = clock != null ? clock : Clock.SYSTEM;
+ }
+
+ /** Returns the encoded string representation of the allowed locations. */
+ public String getEncodedLocations() {
+ return encodedLocations;
+ }
+
+ /** Returns a list of human-readable location strings. */
+ public List getLocations() {
+ return locations;
+ }
+
+ /**
+ * Checks if the regional access boundary data is expired.
+ *
+ * @return True if the data has expired based on the TTL, false otherwise.
+ */
+ public boolean isExpired() {
+ return clock.currentTimeMillis() > refreshTime + TTL_MILLIS;
+ }
+
+ /**
+ * Checks if the regional access boundary data should be refreshed. This is a "soft-expiry" check
+ * that allows for background refreshes before the data actually expires.
+ *
+ * @return True if the data is within the refresh threshold, false otherwise.
+ */
+ public boolean shouldRefresh() {
+ return clock.currentTimeMillis() > refreshTime + (TTL_MILLIS - REFRESH_THRESHOLD_MILLIS);
+ }
+
+ /** Represents the JSON response from the regional access boundary endpoint. */
+ public static class RegionalAccessBoundaryResponse extends GenericJson {
+ @Key("encodedLocations")
+ private String encodedLocations;
+
+ @Key("locations")
+ private List locations;
+
+ /** Returns the encoded string representation of the allowed locations from the API response. */
+ public String getEncodedLocations() {
+ return encodedLocations;
+ }
+
+ /** Returns a list of human-readable location strings from the API response. */
+ public List getLocations() {
+ return locations;
+ }
+
+ @Override
+ /** Returns a string representation of the RegionalAccessBoundaryResponse. */
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("encodedLocations", encodedLocations)
+ .add("locations", locations)
+ .toString();
+ }
+ }
+
+ /**
+ * 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 regional access boundary endpoint.
+ * @param accessToken The access token to authenticate the request.
+ * @param clock The clock to use for expiration checks.
+ * @param maxRetryElapsedTimeMillis The max duration to wait for retries.
+ * @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 RegionalAccessBoundary refresh(
+ HttpTransportFactory transportFactory,
+ String url,
+ AccessToken accessToken,
+ Clock clock,
+ int maxRetryElapsedTimeMillis)
+ throws IOException {
+ Preconditions.checkNotNull(accessToken, "The provided access token is null.");
+ if (accessToken.getExpirationTimeMillis() != null
+ && accessToken.getExpirationTimeMillis() < clock.currentTimeMillis()) {
+ throw new IllegalArgumentException("The provided access token is expired.");
+ }
+
+ HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
+ HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url));
+ // Disable automatic logging by google-http-java-client to prevent leakage of sensitive tokens.
+ request.setLoggingEnabled(false);
+ request.getHeaders().setAuthorization("Bearer " + accessToken.getTokenValue());
+
+ // Add retry logic
+ ExponentialBackOff backoff =
+ new ExponentialBackOff.Builder()
+ .setInitialIntervalMillis(OAuth2Utils.INITIAL_RETRY_INTERVAL_MILLIS)
+ .setRandomizationFactor(OAuth2Utils.RETRY_RANDOMIZATION_FACTOR)
+ .setMultiplier(OAuth2Utils.RETRY_MULTIPLIER)
+ .setMaxElapsedTimeMillis(maxRetryElapsedTimeMillis)
+ .build();
+
+ HttpUnsuccessfulResponseHandler unsuccessfulResponseHandler =
+ new HttpBackOffUnsuccessfulResponseHandler(backoff)
+ .setBackOffRequired(
+ response -> {
+ int statusCode = response.getStatusCode();
+ return statusCode == 500
+ || statusCode == 502
+ || statusCode == 503
+ || statusCode == 504;
+ });
+ request.setUnsuccessfulResponseHandler(unsuccessfulResponseHandler);
+
+ HttpIOExceptionHandler ioExceptionHandler = new HttpBackOffIOExceptionHandler(backoff);
+ request.setIOExceptionHandler(ioExceptionHandler);
+
+ request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY));
+
+ RegionalAccessBoundaryResponse json;
+ HttpResponse response = null;
+ try {
+ response = request.execute();
+ json = response.parseAs(RegionalAccessBoundaryResponse.class);
+ } catch (IOException e) {
+ throw new IOException(
+ "RegionalAccessBoundary: Failure while getting regional access boundaries:", e);
+ } finally {
+ if (response != null) {
+ response.disconnect();
+ }
+ }
+ String encodedLocations = json.getEncodedLocations();
+ // 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(
+ "RegionalAccessBoundary: Malformed response from lookup endpoint - `encodedLocations` was null.");
+ }
+ return new RegionalAccessBoundary(encodedLocations, json.getLocations(), clock);
+ }
+
+ /**
+ * Initializes the transient clock to Clock.SYSTEM upon deserialization to prevent
+ * NullPointerException when evaluating expiration on deserialized objects.
+ */
+ private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
+ input.defaultReadObject();
+ clock = Clock.SYSTEM;
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java
new file mode 100644
index 000000000000..b2237fae6d13
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java
@@ -0,0 +1,298 @@
+/*
+ * 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 static com.google.auth.oauth2.LoggingUtils.log;
+
+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.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+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 RegionalAccessBoundaryManager {
+
+ private static final LoggerProvider LOGGER_PROVIDER =
+ LoggerProvider.forClazz(RegionalAccessBoundaryManager.class);
+
+ static final long INITIAL_COOLDOWN_MILLIS = 15 * 60 * 1000L; // 15 minutes
+ static final long MAX_COOLDOWN_MILLIS = 6 * 60 * 60 * 1000L; // 6 hours
+
+ /**
+ * The default maximum elapsed time in milliseconds for retrying Regional Access Boundary lookup
+ * requests.
+ */
+ static final int DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS = 60000;
+
+ /**
+ * 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<>();
+
+ /**
+ * isRefreshing acts as an atomic gate for request de-duplication. If true, it indicates a
+ * background refresh is already in progress.
+ */
+ private final AtomicBoolean isRefreshing = new AtomicBoolean(false);
+
+ private final AtomicReference cooldownState =
+ new AtomicReference<>(new CooldownState(0, INITIAL_COOLDOWN_MILLIS));
+
+ private final AtomicBoolean skipRAB = new AtomicBoolean(false);
+
+ // Unbounded thread creation is discouraged in library code to avoid resource
+ // exhaustion. A shared, bounded executor service ensures a hard limit (5)
+ // on concurrent refresh tasks, while threadCount provides unique names
+ // for easier debugging.
+ private static final AtomicInteger threadCount = new AtomicInteger(0);
+
+ // Bounded executor service ensures hard limits on concurrent refresh tasks and queued tasks
+ // to avoid resource exhaustion.
+ private static final int EXECUTOR_POOL_SIZE = 5;
+ private static final int EXECUTOR_QUEUE_CAPACITY = 100;
+
+ private static final ExecutorService DEFAULT_SHARED_EXECUTOR;
+
+ static {
+ ThreadPoolExecutor executor =
+ new ThreadPoolExecutor(
+ EXECUTOR_POOL_SIZE, // corePoolSize: threads to keep alive
+ EXECUTOR_POOL_SIZE, // maximumPoolSize: max threads allowed
+ 1, // keepAliveTime: time to wait before terminating idle threads
+ TimeUnit.HOURS, // unit for keepAliveTime
+ new LinkedBlockingQueue<>(EXECUTOR_QUEUE_CAPACITY), // work queue with bound
+ r -> {
+ Thread t = new Thread(r, "RAB-refresh-" + threadCount.getAndIncrement());
+ t.setDaemon(true);
+ return t;
+ });
+ // Allow core threads to time out so the executor can shrink to 0 when idle.
+ // Ensures threads are released when idle to avoid unnecessary resource usage.
+ executor.allowCoreThreadTimeOut(true);
+ DEFAULT_SHARED_EXECUTOR = executor;
+ }
+
+ private final transient Clock clock;
+ private final int maxRetryElapsedTimeMillis;
+ private final ExecutorService executor;
+
+ /**
+ * Creates a new RegionalAccessBoundaryManager with the default retry timeout of 60 seconds.
+ *
+ * @param clock The clock to use for cooldown and expiration checks.
+ */
+ RegionalAccessBoundaryManager(Clock clock) {
+ this(clock, DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS, DEFAULT_SHARED_EXECUTOR);
+ }
+
+ @VisibleForTesting
+ RegionalAccessBoundaryManager(Clock clock, int maxRetryElapsedTimeMillis) {
+ this(clock, maxRetryElapsedTimeMillis, DEFAULT_SHARED_EXECUTOR);
+ }
+
+ @VisibleForTesting
+ RegionalAccessBoundaryManager(
+ Clock clock, int maxRetryElapsedTimeMillis, ExecutorService executor) {
+ this.clock = clock != null ? clock : Clock.SYSTEM;
+ this.maxRetryElapsedTimeMillis = maxRetryElapsedTimeMillis;
+ this.executor = executor;
+ }
+
+ /**
+ * 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;
+ }
+
+ @VisibleForTesting
+ void setCachedRAB(RegionalAccessBoundary rab) {
+ this.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 provider The provider used to retrieve the lookup endpoint URL.
+ * @param accessToken The access token for authentication.
+ */
+ void triggerAsyncRefresh(
+ final HttpTransportFactory transportFactory,
+ final RegionalAccessBoundaryProvider provider,
+ final AccessToken accessToken) {
+ if (skipRAB.get() || isCooldownActive()) {
+ return;
+ }
+
+ RegionalAccessBoundary currentRab = cachedRAB.get();
+ if (currentRab != null && !currentRab.shouldRefresh()) {
+ return;
+ }
+
+ // 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 (isRefreshing.compareAndSet(false, true)) {
+ Runnable refreshTask =
+ () -> {
+ try {
+ String url = provider.getRegionalAccessBoundaryUrl();
+ if (url == null) {
+ skipRAB.set(true);
+ return;
+ }
+ RegionalAccessBoundary newRAB =
+ RegionalAccessBoundary.refresh(
+ transportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis);
+ cachedRAB.set(newRAB);
+ resetCooldown();
+ } catch (Exception e) {
+ handleRefreshFailure(e);
+ } finally {
+ // Open the gate again for future refresh requests.
+ isRefreshing.set(false);
+ }
+ };
+
+ try {
+ this.executor.submit(refreshTask);
+ } catch (Exception | Error e) {
+ // If scheduling fails (e.g., RejectedExecutionException, OutOfMemoryError for threads),
+ // the task's finally block will never execute. We must release the lock here.
+ log(
+ LOGGER_PROVIDER,
+ Level.FINE,
+ null,
+ "Could not submit background refresh task for Regional Access Boundary. "
+ + "This is non-blocking and the library will attempt to refresh on the next access. Error: "
+ + e.getMessage());
+ isRefreshing.set(false);
+ }
+ }
+ }
+
+ private void handleRefreshFailure(Exception e) {
+ CooldownState currentCooldownState = cooldownState.get();
+ CooldownState next;
+ if (currentCooldownState.expiryTime == 0) {
+ // In the first non-retryable failure, we set cooldown to currentTime + 15 mins.
+ next =
+ new CooldownState(
+ clock.currentTimeMillis() + INITIAL_COOLDOWN_MILLIS, INITIAL_COOLDOWN_MILLIS);
+ } else {
+ // We attempted to exit cool-down but failed.
+ // For each failed cooldown exit attempt, we double the cooldown time (till max 6 hrs).
+ // This avoids overwhelming RAB lookup endpoint.
+ long nextDuration = Math.min(currentCooldownState.durationMillis * 2, MAX_COOLDOWN_MILLIS);
+ next = new CooldownState(clock.currentTimeMillis() + nextDuration, nextDuration);
+ }
+
+ // Atomically update the cooldown state. compareAndSet returns true only if the state
+ // hasn't been changed by another thread in the meantime. This prevents multiple
+ // concurrent failures from logging redundant messages or incorrectly calculating
+ // the exponential backoff.
+ if (cooldownState.compareAndSet(currentCooldownState, next)) {
+ log(
+ LOGGER_PROVIDER,
+ Level.FINE,
+ null,
+ "Regional Access Boundary lookup was not successful; will retry after a cooldown of "
+ + (next.durationMillis / 60000)
+ + "m. This is handled automatically. Details: "
+ + e.getMessage());
+ }
+ }
+
+ private void resetCooldown() {
+ cooldownState.set(new CooldownState(0, INITIAL_COOLDOWN_MILLIS));
+ }
+
+ boolean isCooldownActive() {
+ CooldownState state = cooldownState.get();
+ if (state.expiryTime == 0) {
+ return false;
+ }
+ return clock.currentTimeMillis() < state.expiryTime;
+ }
+
+ @VisibleForTesting
+ long getCurrentCooldownMillis() {
+ return cooldownState.get().durationMillis;
+ }
+
+ @VisibleForTesting
+ boolean isSkipRAB() {
+ return skipRAB.get();
+ }
+
+ private static class CooldownState {
+ /** The time (in milliseconds from epoch) when the current cooldown period expires. */
+ final long expiryTime;
+
+ /** The duration (in milliseconds) of the current cooldown period. */
+ final long durationMillis;
+
+ CooldownState(long expiryTime, long durationMillis) {
+ this.expiryTime = expiryTime;
+ this.durationMillis = durationMillis;
+ }
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryProvider.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryProvider.java
new file mode 100644
index 000000000000..e34bbafea0dc
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryProvider.java
@@ -0,0 +1,50 @@
+/*
+ * 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.core.InternalApi;
+import java.io.IOException;
+
+/**
+ * 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 RegionalAccessBoundaryProvider {
+
+ /**
+ * Returns the regional access boundary URI.
+ *
+ * @return The regional access boundary URI.
+ */
+ String getRegionalAccessBoundaryUrl() throws IOException;
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
index a65ddbe8d26e..ca6e330762cd 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java
@@ -52,6 +52,7 @@
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Joiner;
import com.google.api.client.util.Preconditions;
+import com.google.api.core.InternalApi;
import com.google.auth.CredentialTypeForMetrics;
import com.google.auth.Credentials;
import com.google.auth.RequestMetadataCallback;
@@ -90,7 +91,7 @@
*
By default uses a JSON Web Token (JWT) to fetch access tokens.
*/
public class ServiceAccountCredentials extends GoogleCredentials
- implements ServiceAccountSigner, IdTokenProvider, JwtProvider {
+ 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";
@@ -834,11 +835,23 @@ public boolean getUseJwtAccessWithScope() {
return useJwtAccessWithScope;
}
+ @InternalApi
+ @Override
+ public String getRegionalAccessBoundaryUrl() throws IOException {
+ return String.format(
+ OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, getAccount());
+ }
+
@VisibleForTesting
JwtCredentials getSelfSignedJwtCredentialsWithScope() {
return selfSignedJwtCredentialsWithScope;
}
+ @Override
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
@Override
public String getAccount() {
return getClientEmail();
@@ -1034,6 +1047,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) {
@@ -1056,7 +1080,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) {
@@ -1125,6 +1158,8 @@ private Map> getRequestMetadataWithSelfSignedJwt(URI uri)
}
Map> requestMetadata = jwtCredentials.getRequestMetadata(null);
+ requestMetadata = addRegionalAccessBoundaryToRequestMetadata(uri, requestMetadata);
+ refreshRegionalAccessBoundaryWithSelfSignedJwtIfExpired(uri, requestMetadata);
return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata);
}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/TestUtils.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/TestUtils.java
index 91b648992848..9315c631985e 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/TestUtils.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/TestUtils.java
@@ -42,6 +42,7 @@
import com.google.api.client.json.gson.GsonFactory;
import com.google.auth.http.AuthHttpConstants;
import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -55,6 +56,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.TimeZone;
import javax.annotation.Nullable;
/** Utilities for test code under com.google.auth. */
@@ -64,6 +66,9 @@ 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 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();
@@ -167,7 +172,9 @@ public static String getDefaultExpireTime() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.SECOND, 300);
- return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(calendar.getTime());
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return dateFormat.format(calendar.getTime());
}
private TestUtils() {}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
index e401ae853771..26fe9151955b 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java
@@ -32,6 +32,7 @@
package com.google.auth.oauth2;
import static com.google.auth.Credentials.GOOGLE_DEFAULT_UNIVERSE;
+import static com.google.auth.oauth2.TestUtils.createDummyRab;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -56,11 +57,15 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/** Tests for {@link AwsCredentials}. */
class AwsCredentialsTest extends BaseSerializationTest {
+ @org.junit.jupiter.api.BeforeEach
+ void setUp() {}
+
private static final String STS_URL = "https://sts.googleapis.com/v1/token";
private static final String AWS_CREDENTIALS_URL = "https://169.254.169.254";
private static final String AWS_CREDENTIALS_URL_WITH_ROLE = "https://169.254.169.254/roleName";
@@ -130,6 +135,7 @@ void refreshAccessToken_withoutServiceAccountImpersonation() throws IOException
.setHttpTransportFactory(transportFactory)
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
AccessToken accessToken = awsCredential.refreshAccessToken();
@@ -159,6 +165,7 @@ void refreshAccessToken_withServiceAccountImpersonation() throws IOException {
.setServiceAccountImpersonationUrl(
transportFactory.transport.getServiceAccountImpersonationUrl())
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
AccessToken accessToken = awsCredential.refreshAccessToken();
@@ -191,6 +198,7 @@ void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOExcept
.setServiceAccountImpersonationOptions(
ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions())
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
AccessToken accessToken = awsCredential.refreshAccessToken();
@@ -228,6 +236,7 @@ void refreshAccessTokenProgrammaticRefresh_withoutServiceAccountImpersonation()
.setTokenUrl(STS_URL)
.setSubjectTokenType("subjectTokenType")
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
AccessToken accessToken = awsCredential.refreshAccessToken();
@@ -259,6 +268,7 @@ void refreshAccessTokenProgrammaticRefresh_withServiceAccountImpersonation() thr
.setServiceAccountImpersonationUrl(
transportFactory.transport.getServiceAccountImpersonationUrl())
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
AccessToken accessToken = awsCredential.refreshAccessToken();
@@ -282,6 +292,7 @@ void retrieveSubjectToken() throws IOException {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8");
@@ -326,6 +337,7 @@ void retrieveSubjectTokenWithSessionTokenUrl() throws IOException {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory))
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8");
@@ -399,6 +411,7 @@ void retrieveSubjectToken_imdsv1EnvVariablesSet_metadataServerNotCalled() throws
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.setEnvironmentProvider(environmentProvider)
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8");
@@ -444,6 +457,7 @@ void retrieveSubjectToken_imdsv2EnvVariablesSet_metadataServerNotCalled() throws
.setCredentialSource(buildAwsImdsv2CredentialSource(transportFactory))
.setEnvironmentProvider(environmentProvider)
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8");
@@ -483,6 +497,7 @@ void retrieveSubjectToken_noRegion_expectThrows() {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
IOException exception = assertThrows(IOException.class, awsCredential::retrieveSubjectToken);
assertEquals("Failed to retrieve AWS region.", exception.getMessage());
@@ -508,6 +523,7 @@ void retrieveSubjectToken_noRole_expectThrows() {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
IOException exception = assertThrows(IOException.class, awsCredential::retrieveSubjectToken);
assertEquals("Failed to retrieve AWS IAM role.", exception.getMessage());
@@ -536,6 +552,7 @@ void retrieveSubjectToken_noCredentials_expectThrows() {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
IOException exception = assertThrows(IOException.class, awsCredential::retrieveSubjectToken);
assertEquals("Failed to retrieve AWS credentials.", exception.getMessage());
@@ -567,6 +584,7 @@ void retrieveSubjectToken_noRegionUrlProvided() {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(new AwsCredentialSource(credentialSource))
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
IOException exception = assertThrows(IOException.class, awsCredential::retrieveSubjectToken);
assertEquals(
@@ -595,6 +613,7 @@ void retrieveSubjectToken_withProgrammaticRefresh() throws IOException {
.setTokenUrl(STS_URL)
.setSubjectTokenType("subjectTokenType")
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8");
@@ -637,6 +656,7 @@ void retrieveSubjectToken_withProgrammaticRefreshSessionToken() throws IOExcepti
.setTokenUrl(STS_URL)
.setSubjectTokenType("subjectTokenType")
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
String subjectToken = URLDecoder.decode(awsCredential.retrieveSubjectToken(), "UTF-8");
@@ -687,6 +707,7 @@ void retrieveSubjectToken_passesContext() {
.setTokenUrl(STS_URL)
.setSubjectTokenType("subjectTokenType")
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
assertDoesNotThrow(awsCredential::retrieveSubjectToken);
}
@@ -709,6 +730,7 @@ void retrieveSubjectToken_withProgrammaticRefreshThrowsError() {
.setTokenUrl(STS_URL)
.setSubjectTokenType("subjectTokenType")
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
IOException exception = assertThrows(IOException.class, awsCredential::retrieveSubjectToken);
assertEquals("test", exception.getMessage());
@@ -725,6 +747,8 @@ void getAwsSecurityCredentials_fromEnvironmentVariablesNoToken() throws IOExcept
AwsCredentials.newBuilder(AWS_CREDENTIAL)
.setEnvironmentProvider(environmentProvider)
.build();
+ testAwsCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(testAwsCredentials.clock));
AwsSecurityCredentials credentials =
testAwsCredentials.getAwsSecurityCredentialsSupplier().getCredentials(emptyContext);
@@ -758,6 +782,8 @@ void getAwsSecurityCredentials_fromEnvironmentVariablesWithToken() throws IOExce
.setEnvironmentProvider(environmentProvider)
.setCredentialSource(credSource)
.build();
+ testAwsCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(testAwsCredentials.clock));
AwsSecurityCredentials credentials =
testAwsCredentials.getAwsSecurityCredentialsSupplier().getCredentials(emptyContext);
@@ -780,6 +806,8 @@ void getAwsSecurityCredentials_fromEnvironmentVariables_noMetadataServerCall()
AwsCredentials.newBuilder(AWS_CREDENTIAL)
.setEnvironmentProvider(environmentProvider)
.build();
+ testAwsCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(testAwsCredentials.clock));
AwsSecurityCredentials credentials =
testAwsCredentials.getAwsSecurityCredentialsSupplier().getCredentials(emptyContext);
@@ -799,6 +827,7 @@ void getAwsSecurityCredentials_fromMetadataServer() throws IOException {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
AwsSecurityCredentials credentials =
awsCredential.getAwsSecurityCredentialsSupplier().getCredentials(emptyContext);
@@ -831,6 +860,7 @@ void getAwsSecurityCredentials_fromMetadataServer_noUrlProvided() {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(new AwsCredentialSource(credentialSource))
.build();
+ awsCredential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredential.clock));
IOException exception =
assertThrows(
@@ -859,6 +889,7 @@ void getAwsRegion_awsRegionEnvironmentVariable() throws IOException {
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.setEnvironmentProvider(environmentProvider)
.build();
+ awsCredentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredentials.clock));
String region = awsCredentials.getAwsSecurityCredentialsSupplier().getRegion(emptyContext);
@@ -884,6 +915,7 @@ void getAwsRegion_awsDefaultRegionEnvironmentVariable() throws IOException {
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.setEnvironmentProvider(environmentProvider)
.build();
+ awsCredentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredentials.clock));
String region = awsCredentials.getAwsSecurityCredentialsSupplier().getRegion(emptyContext);
@@ -905,6 +937,7 @@ void getAwsRegion_metadataServer() throws IOException {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(buildAwsCredentialSource(transportFactory))
.build();
+ awsCredentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(awsCredentials.clock));
String region = awsCredentials.getAwsSecurityCredentialsSupplier().getRegion(emptyContext);
@@ -933,10 +966,12 @@ void createdScoped_clonedCredentialWithAddedScopes() {
.setClientSecret("clientSecret")
.setUniverseDomain("universeDomain")
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
List newScopes = Arrays.asList("scope1", "scope2");
AwsCredentials newCredentials = (AwsCredentials) credentials.createScoped(newScopes);
+ newCredentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(newCredentials.clock));
assertEquals(credentials.getAudience(), newCredentials.getAudience());
assertEquals(credentials.getSubjectTokenType(), newCredentials.getSubjectTokenType());
@@ -1012,6 +1047,7 @@ void builder_allFields() {
.setScopes(scopes)
.setUniverseDomain("universeDomain")
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertEquals("audience", credentials.getAudience());
assertEquals("subjectTokenType", credentials.getSubjectTokenType());
@@ -1048,6 +1084,7 @@ void builder_missingUniverseDomain_defaults() {
.setClientSecret("clientSecret")
.setScopes(scopes)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertEquals("https://test.com", credentials.getRegionalCredentialVerificationUrlOverride());
assertEquals("audience", credentials.getAudience());
@@ -1085,8 +1122,11 @@ void newBuilder_allFields() {
.setScopes(scopes)
.setUniverseDomain("universeDomain")
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
AwsCredentials newBuilderCreds = AwsCredentials.newBuilder(credentials).build();
+ newBuilderCreds.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(newBuilderCreds.clock));
assertEquals(credentials.getAudience(), newBuilderCreds.getAudience());
assertEquals(credentials.getSubjectTokenType(), newBuilderCreds.getSubjectTokenType());
assertEquals(credentials.getTokenUrl(), newBuilderCreds.getTokenUrl());
@@ -1122,8 +1162,11 @@ void newBuilder_noUniverseDomain_defaults() {
.setClientSecret("clientSecret")
.setScopes(scopes)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
AwsCredentials newBuilderCreds = AwsCredentials.newBuilder(credentials).build();
+ newBuilderCreds.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(newBuilderCreds.clock));
assertEquals(credentials.getAudience(), newBuilderCreds.getAudience());
assertEquals(credentials.getSubjectTokenType(), newBuilderCreds.getSubjectTokenType());
assertEquals(credentials.getTokenUrl(), newBuilderCreds.getTokenUrl());
@@ -1161,6 +1204,7 @@ void builder_defaultRegionalCredentialVerificationUrlOverride() {
.setClientSecret("clientSecret")
.setScopes(scopes)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertNull(credentials.getRegionalCredentialVerificationUrlOverride());
assertEquals(
@@ -1240,6 +1284,8 @@ void serialize() throws IOException, ClassNotFoundException {
.setUniverseDomain("universeDomain")
.setScopes(scopes)
.build();
+ testCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(testCredentials.clock));
AwsCredentials deserializedCredentials = serializeAndDeserialize(testCredentials);
assertEquals(testCredentials, deserializedCredentials);
@@ -1357,4 +1403,48 @@ public AwsSecurityCredentials getCredentials(ExternalAccountSupplierContext cont
return credentials;
}
}
+
+ @Test
+ public void testRefresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
+
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ AwsSecurityCredentialsSupplier supplier =
+ new TestAwsSecurityCredentialsSupplier("test", programmaticAwsCreds, null, null);
+
+ AwsCredentials awsCredential =
+ AwsCredentials.newBuilder()
+ .setAwsSecurityCredentialsSupplier(supplier)
+ .setHttpTransportFactory(transportFactory)
+ .setAudience(
+ "//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/pool/providers/provider")
+ .setTokenUrl(STS_URL)
+ .setSubjectTokenType("subjectTokenType")
+ .build();
+
+ // First call: initiates async refresh.
+ Map> headers = awsCredential.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(awsCredential);
+
+ // Second call: should have header.
+ headers = awsCredential.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
+
+ 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) {
+ Assertions.fail("Timed out waiting for regional access boundary refresh");
+ }
+ }
}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java
index 82240171d9af..b6de475ae510 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java
@@ -33,6 +33,8 @@
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.RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY;
+import static com.google.auth.oauth2.TestUtils.createDummyRab;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -43,6 +45,7 @@
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.http.HttpTransport;
@@ -75,6 +78,9 @@
/** Test case for {@link ComputeEngineCredentials}. */
class ComputeEngineCredentialsTest extends BaseSerializationTest {
+ @org.junit.jupiter.api.BeforeEach
+ void setUp() {}
+
private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
private static final String TOKEN_URL =
@@ -388,12 +394,12 @@ void getRequestMetadata_hasAccessToken() throws IOException {
transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN);
// verify metrics header added and other header intact
Map> requestHeaders = transportFactory.transport.getRequest().getHeaders();
- com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "at", "mds");
assertTrue(requestHeaders.containsKey("metadata-flavor"));
assertTrue(requestHeaders.get("metadata-flavor").contains("Google"));
}
@@ -405,6 +411,7 @@ void getRequestMetadata_shouldInvalidateAccessTokenWhenScoped_newAccessTokenFrom
transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL");
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN);
@@ -412,6 +419,8 @@ void getRequestMetadata_shouldInvalidateAccessTokenWhenScoped_newAccessTokenFrom
assertNotNull(credentials.getAccessToken());
ComputeEngineCredentials scopedCredentialCopy =
(ComputeEngineCredentials) credentials.createScoped(SCOPES);
+ scopedCredentialCopy.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(scopedCredentialCopy.clock));
assertNull(scopedCredentialCopy.getAccessToken());
Map> metadataForCopiedCredentials =
scopedCredentialCopy.getRequestMetadata(CALL_URI);
@@ -426,6 +435,7 @@ void getRequestMetadata_missingServiceAccount_throws() {
transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL");
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
IOException exception =
assertThrows(IOException.class, () -> credentials.getRequestMetadata(CALL_URI));
String message = exception.getMessage();
@@ -441,6 +451,7 @@ void getRequestMetadata_serverError_throws() {
transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL");
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
IOException exception =
assertThrows(IOException.class, () -> credentials.getRequestMetadata(CALL_URI));
String message = exception.getMessage();
@@ -564,6 +575,7 @@ void getAccount_sameAs() {
transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertEquals(defaultAccountEmail, credentials.getAccount());
@@ -597,6 +609,7 @@ public LowLevelHttpResponse execute() throws IOException {
transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
RuntimeException exception = assertThrows(RuntimeException.class, credentials::getAccount);
assertEquals("Failed to get service account", exception.getMessage());
@@ -628,6 +641,7 @@ public LowLevelHttpResponse execute() throws IOException {
transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
RuntimeException exception = assertThrows(RuntimeException.class, credentials::getAccount);
assertEquals("Failed to get service account", exception.getMessage());
@@ -645,6 +659,7 @@ void sign_sameAs() {
transportFactory.transport.setSignature(expectedSignature);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertArrayEquals(expectedSignature, credentials.sign(expectedSignature));
}
@@ -657,6 +672,7 @@ void sign_getUniverseException() {
transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transportFactory.transport.setStatusCode(501);
assertThrows(IOException.class, credentials::getUniverseDomain);
@@ -675,6 +691,7 @@ void sign_getAccountFails() {
transportFactory.transport.setSignature(expectedSignature);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
SigningException exception =
assertThrows(SigningException.class, () -> credentials.sign(expectedSignature));
@@ -710,6 +727,7 @@ public LowLevelHttpResponse execute() throws IOException {
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
byte[] bytes = {0xD, 0xE, 0xA, 0xD};
@@ -748,6 +766,7 @@ public LowLevelHttpResponse execute() throws IOException {
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
byte[] bytes = {0xD, 0xE, 0xA, 0xD};
@@ -779,6 +798,7 @@ public LowLevelHttpResponse execute() throws IOException {
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
IOException exception = assertThrows(IOException.class, credentials::refreshAccessToken);
assertTrue(exception.getCause().getMessage().contains("503"));
@@ -842,6 +862,7 @@ public LowLevelHttpResponse execute() throws IOException {
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String universeDomain = credentials.getUniverseDomain();
assertEquals("some-universe.xyz", universeDomain);
@@ -869,6 +890,7 @@ public LowLevelHttpResponse execute() throws IOException {
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String universeDomain = credentials.getUniverseDomain();
assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain);
@@ -896,6 +918,7 @@ public LowLevelHttpResponse execute() throws IOException {
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String universeDomain = credentials.getUniverseDomain();
assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain);
@@ -942,6 +965,7 @@ void getUniverseDomain_fromMetadata_non404error_throws() {
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
for (int status = 400; status < 600; status++) {
// 404 should not throw and tested separately
@@ -982,6 +1006,7 @@ public LowLevelHttpResponse execute() {
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
byte[] bytes = {0xD, 0xE, 0xA, 0xD};
@@ -998,6 +1023,7 @@ void idTokenWithAudience_sameAs() throws IOException {
transportFactory.transport.setIdToken(STANDARD_ID_TOKEN);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://foo.bar";
IdTokenCredentials tokenCredential =
@@ -1018,6 +1044,7 @@ void idTokenWithAudience_standard() throws IOException {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://foo.bar";
IdTokenCredentials tokenCredential =
@@ -1037,6 +1064,7 @@ void idTokenWithAudience_full() throws IOException {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://foo.bar";
IdTokenCredentials tokenCredential =
@@ -1063,6 +1091,7 @@ void idTokenWithAudience_licenses() throws IOException {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://foo.bar";
IdTokenCredentials tokenCredential =
@@ -1091,6 +1120,7 @@ void idTokenWithAudience_404StatusCode() {
transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
IOException exception =
assertThrows(IOException.class, () -> credentials.idTokenWithAudience("Audience", null));
assertEquals(
@@ -1108,6 +1138,7 @@ void idTokenWithAudience_emptyContent() {
transportFactory.transport.setEmptyContent(true);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
IOException exception =
assertThrows(IOException.class, () -> credentials.idTokenWithAudience("Audience", null));
assertEquals(METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE, exception.getMessage());
@@ -1119,6 +1150,7 @@ void idTokenWithAudience_503StatusCode() {
transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertThrows(
GoogleAuthException.class, () -> credentials.idTokenWithAudience("Audience", null));
}
@@ -1143,6 +1175,7 @@ public LowLevelHttpResponse execute() throws IOException {
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String projectId = credentials.getProjectId();
assertEquals("some-project-id", projectId);
}
@@ -1153,6 +1186,7 @@ void getProjectId_metadataServerFailure_404StatusCode() {
transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertNull(credentials.getProjectId());
}
@@ -1162,6 +1196,7 @@ void getProjectId_metadataServerFailure_otherStatusCode() {
transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertNull(credentials.getProjectId());
}
@@ -1171,12 +1206,124 @@ void getProjectId_explicitSet_noMDsCall() {
new MockRequestCountingTransportFactory();
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
credentials.setProjectId("explicit.project_id");
assertEquals("explicit.project_id", credentials.getProjectId());
assertEquals(0, transportFactory.transport.getRequestCount());
}
+ @Test
+ void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
+
+ String defaultAccountEmail = "default@email.com";
+ MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
+ RegionalAccessBoundary regionalAccessBoundary =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS,
+ null);
+ transportFactory.transport.setRegionalAccessBoundary(regionalAccessBoundary);
+ transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
+
+ ComputeEngineCredentials credentials =
+ ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
+
+ @Test
+ void refresh_regionalAccessBoundaryNonEmail_skipsRABLookup()
+ throws IOException, InterruptedException {
+ String nonEmailAccount = "non-email-account-value";
+ MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
+ RegionalAccessBoundary regionalAccessBoundary =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS,
+ null);
+ transportFactory.transport.setRegionalAccessBoundary(regionalAccessBoundary);
+ transportFactory.transport.setServiceAccountEmail(nonEmailAccount);
+
+ ComputeEngineCredentials credentials =
+ ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+
+ // Before any call, skipRAB flag should be false
+ assertFalse(credentials.regionalAccessBoundaryManager.isSkipRAB());
+
+ // First call: triggers lookup which determines non-email, returns null, and sets skipRAB to
+ // true
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ // Since the task is scheduled asynchronously on the shared executor, wait for it to complete
+ long deadline = System.currentTimeMillis() + 5000;
+ while (!credentials.regionalAccessBoundaryManager.isSkipRAB()
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(50);
+ }
+
+ // Verify skipRAB flag has been set to true
+ assertTrue(credentials.regionalAccessBoundaryManager.isSkipRAB());
+
+ // Verify RAB is still null
+ assertNull(credentials.getRegionalAccessBoundary());
+
+ // Second call: should bypass triggerAsyncRefresh completely and remain null
+ headers = credentials.getRequestMetadata();
+ assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
+ }
+
+ @Test
+ void getRegionalAccessBoundaryUrl_validEmail_returnsUrl() throws IOException {
+ MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
+ String defaultAccountEmail = "mail@mail.com";
+
+ transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
+ ComputeEngineCredentials credentials =
+ ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+
+ String expectedUrl =
+ String.format(
+ OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
+ defaultAccountEmail);
+ assertEquals(expectedUrl, credentials.getRegionalAccessBoundaryUrl());
+ }
+
+ @Test
+ void getRegionalAccessBoundaryUrl_invalidEmail_returnsNull() throws IOException {
+ MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
+ String defaultAccountEmail = "default"; // non-email account format
+
+ transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
+ ComputeEngineCredentials credentials =
+ ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+
+ assertNull(credentials.getRegionalAccessBoundaryUrl());
+ }
+
+ private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ 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 {
MockMetadataServerTransport transport =
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
index 78bb6811953e..b374e08111ff 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
@@ -32,6 +32,7 @@
package com.google.auth.oauth2;
import static com.google.auth.Credentials.GOOGLE_DEFAULT_UNIVERSE;
+import static com.google.auth.oauth2.TestUtils.createDummyRab;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -43,7 +44,6 @@
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;
@@ -62,6 +62,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -129,6 +130,9 @@ void setup() {
transportFactory = new MockExternalAccountAuthorizedUserCredentialsTransportFactory();
}
+ @org.junit.jupiter.api.AfterEach
+ void tearDown() {}
+
@Test
void builder_allFields() throws IOException {
ExternalAccountAuthorizedUserCredentials credentials =
@@ -703,6 +707,7 @@ void createScopedRequired_false() {
void getRequestMetadata() throws IOException {
GoogleCredentials credentials =
ExternalAccountAuthorizedUserCredentials.fromJson(buildJsonCredentials(), transportFactory);
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
@@ -714,6 +719,7 @@ void getRequestMetadata() throws IOException {
void getRequestMetadata_withQuotaProjectId() throws IOException {
GoogleCredentials credentials =
ExternalAccountAuthorizedUserCredentials.fromJson(buildJsonCredentials(), transportFactory);
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
@@ -732,6 +738,7 @@ void getRequestMetadata_withAccessToken() throws IOException {
.setHttpTransportFactory(transportFactory)
.setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
@@ -1233,7 +1240,46 @@ void serialize() throws IOException, ClassNotFoundException {
assertEquals(credentials, deserializedCredentials);
assertEquals(credentials.hashCode(), deserializedCredentials.hashCode());
assertEquals(credentials.toString(), deserializedCredentials.toString());
- assertSame(Clock.SYSTEM, deserializedCredentials.clock);
+ assertSame(com.google.api.client.util.Clock.SYSTEM, deserializedCredentials.clock);
+ }
+
+ @org.junit.jupiter.api.Test
+ void testRefresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
+
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .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();
+
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
+
+ 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) {
+ Assertions.fail("Timed out waiting for regional access boundary refresh");
+ }
}
static GenericJson buildJsonCredentials() {
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
index 1338c0d68fe9..ae4fbb1aac8b 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
@@ -32,6 +32,10 @@
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 com.google.auth.oauth2.TestUtils.createDummyRab;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@@ -53,12 +57,8 @@
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.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -1109,6 +1109,8 @@ void getRequestMetadata_withQuotaProjectId() throws IOException {
.setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
.setQuotaProjectId("quotaProjectId")
.build();
+ testCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(testCredentials.clock));
Map> requestMetadata =
testCredentials.getRequestMetadata(URI.create("http://googleapis.com/foo/bar"));
@@ -1144,7 +1146,7 @@ void serialize() throws IOException, ClassNotFoundException {
assertEquals(
testCredentials.getServiceAccountImpersonationOptions().getLifetime(),
deserializedCredentials.getServiceAccountImpersonationOptions().getLifetime());
- assertSame(Clock.SYSTEM, deserializedCredentials.clock);
+ assertSame(deserializedCredentials.clock, Clock.SYSTEM);
assertEquals(
MockExternalAccountCredentialsTransportFactory.class,
deserializedCredentials.toBuilder().getHttpTransportFactory().getClass());
@@ -1240,6 +1242,266 @@ void validateServiceAccountImpersonationUrls_invalidUrls() {
}
}
+ @Test
+ public void getRegionalAccessBoundaryUrl_workload() throws IOException {
+ String audience =
+ "//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/my-pool/providers/my-provider";
+ ExternalAccountCredentials credentials =
+ TestExternalAccountCredentials.newBuilder()
+ .setAudience(audience)
+ .setSubjectTokenType("subject_token_type")
+ .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
+ .build();
+
+ String expectedUrl =
+ "https://iamcredentials.googleapis.com/v1/projects/12345/locations/global/workloadIdentityPools/my-pool/allowedLocations";
+ assertEquals(expectedUrl, credentials.getRegionalAccessBoundaryUrl());
+ }
+
+ @Test
+ public void getRegionalAccessBoundaryUrl_workforce() throws IOException {
+ String audience =
+ "//iam.googleapis.com/locations/global/workforcePools/my-pool/providers/my-provider";
+ ExternalAccountCredentials credentials =
+ TestExternalAccountCredentials.newBuilder()
+ .setAudience(audience)
+ .setWorkforcePoolUserProject("12345")
+ .setSubjectTokenType("subject_token_type")
+ .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
+ .build();
+
+ String expectedUrl =
+ "https://iamcredentials.googleapis.com/v1/locations/global/workforcePools/my-pool/allowedLocations";
+ assertEquals(expectedUrl, credentials.getRegionalAccessBoundaryUrl());
+ }
+
+ @Test
+ public void getRegionalAccessBoundaryUrl_invalidAudience_throws() {
+ ExternalAccountCredentials credentials =
+ TestExternalAccountCredentials.newBuilder()
+ .setAudience("invalid-audience")
+ .setSubjectTokenType("subject_token_type")
+ .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
+ .build();
+
+ IllegalStateException exception =
+ assertThrows(
+ IllegalStateException.class,
+ () -> {
+ credentials.getRegionalAccessBoundaryUrl();
+ });
+
+ assertEquals(
+ "The provided audience is not in a valid format for either a workload identity pool or a workforce pool. "
+ + "Refer: https://docs.cloud.google.com/iam/docs/principal-identifiers",
+ exception.getMessage());
+ }
+
+ @Test
+ public void refresh_workload_regionalAccessBoundarySuccess()
+ throws IOException, InterruptedException {
+
+ String audience =
+ "//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/my-pool/providers/my-provider";
+
+ ExternalAccountCredentials credentials =
+ new IdentityPoolCredentials(
+ IdentityPoolCredentials.newBuilder()
+ .setHttpTransportFactory(transportFactory)
+ .setAudience(audience)
+ .setSubjectTokenType("subject_token_type")
+ .setTokenUrl(STS_URL)
+ .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))) {
+ @Override
+ public String retrieveSubjectToken() throws IOException {
+ // This override isolates the test from the filesystem.
+ return "dummy-subject-token";
+ }
+ };
+
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
+
+ @Test
+ public void refresh_workforce_regionalAccessBoundarySuccess()
+ throws IOException, InterruptedException {
+
+ String audience =
+ "//iam.googleapis.com/locations/global/workforcePools/my-pool/providers/my-provider";
+
+ ExternalAccountCredentials credentials =
+ new IdentityPoolCredentials(
+ IdentityPoolCredentials.newBuilder()
+ .setHttpTransportFactory(transportFactory)
+ .setAudience(audience)
+ .setWorkforcePoolUserProject("12345")
+ .setSubjectTokenType("subject_token_type")
+ .setTokenUrl(STS_URL)
+ .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))) {
+ @Override
+ public String retrieveSubjectToken() throws IOException {
+ return "dummy-subject-token";
+ }
+ };
+
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
+
+ @Test
+ public void refresh_impersonated_workload_regionalAccessBoundarySuccess()
+ throws IOException, InterruptedException {
+
+ String projectNumber = "12345";
+ String poolId = "my-pool";
+ String providerId = "my-provider";
+ String audience =
+ String.format(
+ "//iam.googleapis.com/projects/%s/locations/global/workloadIdentityPools/%s/providers/%s",
+ projectNumber, poolId, providerId);
+
+ 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, projectNumber, poolId);
+ RegionalAccessBoundary workloadRab =
+ new RegionalAccessBoundary(
+ "workload-encoded", Collections.singletonList("workload-loc"), null);
+ 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, saEmail);
+ RegionalAccessBoundary impersonatedRab =
+ new RegionalAccessBoundary(
+ "impersonated-encoded", Collections.singletonList("impersonated-loc"), null);
+ 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");
+ Map headers = new HashMap<>();
+ headers.put("Metadata-Flavor", "Google");
+ urlCredentialSourceMap.put("headers", headers);
+
+ ExternalAccountCredentials credentials =
+ IdentityPoolCredentials.newBuilder()
+ .setHttpTransportFactory(transportFactory)
+ .setAudience(audience)
+ .setSubjectTokenType("subject_token_type")
+ .setTokenUrl(STS_URL)
+ .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
+ .setCredentialSource(new IdentityPoolCredentialSource(urlCredentialSourceMap))
+ .build();
+
+ // First call: initiates async refresh.
+ Map> requestHeaders = credentials.getRequestMetadata();
+ assertNull(requestHeaders.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have the IMPERSONATED header, not the workload one.
+ requestHeaders = credentials.getRequestMetadata();
+ assertEquals(
+ Arrays.asList("impersonated-encoded"),
+ requestHeaders.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
+ }
+
+ @Test
+ public void refresh_impersonated_workforce_regionalAccessBoundarySuccess()
+ throws IOException, InterruptedException {
+
+ String poolId = "my-pool";
+ String providerId = "my-provider";
+ String audience =
+ String.format(
+ "//iam.googleapis.com/locations/global/workforcePools/%s/providers/%s",
+ poolId, providerId);
+
+ 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, poolId);
+ RegionalAccessBoundary workforceRab =
+ new RegionalAccessBoundary(
+ "workforce-encoded", Collections.singletonList("workforce-loc"), null);
+ 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, saEmail);
+ RegionalAccessBoundary impersonatedRab =
+ new RegionalAccessBoundary(
+ "impersonated-encoded", Collections.singletonList("impersonated-loc"), null);
+ 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");
+ Map headers = new HashMap<>();
+ headers.put("Metadata-Flavor", "Google");
+ urlCredentialSourceMap.put("headers", headers);
+
+ ExternalAccountCredentials credentials =
+ IdentityPoolCredentials.newBuilder()
+ .setHttpTransportFactory(transportFactory)
+ .setAudience(audience)
+ .setWorkforcePoolUserProject("12345")
+ .setSubjectTokenType("subject_token_type")
+ .setTokenUrl(STS_URL)
+ .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
+ .setCredentialSource(new IdentityPoolCredentialSource(urlCredentialSourceMap))
+ .build();
+
+ // First call: initiates async refresh.
+ Map> requestHeaders = credentials.getRequestMetadata();
+ assertNull(requestHeaders.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have the IMPERSONATED header, not the workforce one.
+ requestHeaders = credentials.getRequestMetadata();
+ assertEquals(
+ Arrays.asList("impersonated-encoded"),
+ requestHeaders.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_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) {
+ Assertions.fail("Timed out waiting for regional access boundary refresh");
+ }
+ }
+
private GenericJson buildJsonIdentityPoolCredential() {
GenericJson json = new GenericJson();
json.put(
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
index 74aa9fae9ccd..18e5c4585eef 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
@@ -31,6 +31,8 @@
package com.google.auth.oauth2;
+import static com.google.auth.oauth2.RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY;
+import static org.junit.Assert.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@@ -44,6 +46,7 @@
import com.google.api.client.json.GenericJson;
import com.google.api.client.util.Clock;
import com.google.auth.Credentials;
+import com.google.auth.RequestMetadataCallback;
import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.ExternalAccountAuthorizedUserCredentialsTest.MockExternalAccountAuthorizedUserCredentialsTransportFactory;
@@ -58,7 +61,10 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/** Test case for {@link GoogleCredentials}. */
@@ -99,6 +105,12 @@ class GoogleCredentialsTest extends BaseSerializationTest {
private static final String GOOGLE_DEFAULT_UNIVERSE = "googleapis.com";
private static final String TPC_UNIVERSE = "foo.bar";
+ @org.junit.jupiter.api.BeforeEach
+ void setUp() {}
+
+ @org.junit.jupiter.api.AfterEach
+ void tearDown() {}
+
@Test
void getApplicationDefault_nullTransport_throws() {
assertThrows(NullPointerException.class, () -> GoogleCredentials.getApplicationDefault(null));
@@ -838,6 +850,54 @@ void serialize() throws IOException, ClassNotFoundException {
assertEquals(testCredentials.hashCode(), deserializedCredentials.hashCode());
assertEquals(testCredentials.toString(), deserializedCredentials.toString());
assertSame(Clock.SYSTEM, deserializedCredentials.clock);
+ assertSame(deserializedCredentials.clock, Clock.SYSTEM);
+ assertNotNull(deserializedCredentials.regionalAccessBoundaryManager);
+ }
+
+ @Test
+ public void serialize_removesStaleRabHeaders() throws Exception {
+
+ MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
+ RegionalAccessBoundary rab =
+ new RegionalAccessBoundary(
+ "test-encoded",
+ Collections.singletonList("test-loc"),
+ System.currentTimeMillis(),
+ null);
+ transportFactory.transport.setRegionalAccessBoundary(rab);
+ transportFactory.transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
+
+ GoogleCredentials credentials =
+ new ServiceAccountCredentials.Builder()
+ .setClientEmail(SA_CLIENT_EMAIL)
+ .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
+ .setPrivateKeyId(SA_PRIVATE_KEY_ID)
+ .setHttpTransportFactory(transportFactory)
+ .setScopes(SCOPES)
+ .build();
+
+ // 1. Trigger request metadata to start async RAB refresh
+ credentials.getRequestMetadata(URI.create("https://foo.com"));
+
+ // Wait for the RAB to be fetched and cached
+ waitForRegionalAccessBoundary(credentials);
+
+ // 2. Verify the live credential has the RAB header
+ Map> metadata = credentials.getRequestMetadata();
+ assertEquals(
+ Collections.singletonList("test-encoded"),
+ metadata.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ // 3. Serialize and deserialize.
+ GoogleCredentials deserialized = serializeAndDeserialize(credentials);
+
+ // 4. Verify.
+ // The manager is transient, so it should be empty.
+ assertNull(deserialized.getRegionalAccessBoundary());
+
+ // The metadata should NOT contain the RAB header anymore, preventing stale headers.
+ Map> deserializedMetadata = deserialized.getRequestMetadata();
+ assertNull(deserializedMetadata.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
}
@Test
@@ -977,4 +1037,334 @@ void getCredentialInfo_impersonatedServiceAccount() throws IOException {
assertEquals(
ImpersonatedCredentialsTest.IMPERSONATED_CLIENT_EMAIL, credentialInfo.get("Principal"));
}
+
+ @Test
+ public void regionalAccessBoundary_shouldFetchAndReturnRegionalAccessBoundaryDataSuccessfully()
+ throws IOException, InterruptedException {
+
+ MockTokenServerTransport transport = new MockTokenServerTransport();
+ transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
+ RegionalAccessBoundary regionalAccessBoundary =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ Collections.singletonList("us-central1"),
+ null);
+ transport.setRegionalAccessBoundary(regionalAccessBoundary);
+
+ 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();
+
+ // First call: returns no header, initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
+
+ @Test
+ public void regionalAccessBoundary_shouldRetryRegionalAccessBoundaryLookupOnFailure()
+ throws IOException, InterruptedException {
+
+ // This transport will be used for the regional access boundary lookup.
+ // We will configure it to fail on the first attempt.
+ 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,
+ null);
+ regionalAccessBoundaryTransport.setRegionalAccessBoundary(regionalAccessBoundary);
+
+ // This transport will be used for the access token refresh.
+ // It will succeed.
+ MockTokenServerTransport accessTokenTransport = new MockTokenServerTransport();
+ accessTokenTransport.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)
+ // Use a custom transport factory that returns the correct transport for each endpoint.
+ .setHttpTransportFactory(
+ () ->
+ new com.google.api.client.testing.http.MockHttpTransport() {
+ @Override
+ public com.google.api.client.http.LowLevelHttpRequest buildRequest(
+ String method, String url) throws IOException {
+ if (url.endsWith("/allowedLocations")) {
+ return regionalAccessBoundaryTransport.buildRequest(method, url);
+ }
+ return accessTokenTransport.buildRequest(method, url);
+ }
+ })
+ .setScopes(SCOPES)
+ .build();
+
+ credentials.getRequestMetadata();
+ waitForRegionalAccessBoundary(credentials);
+
+ Map> headers = credentials.getRequestMetadata();
+ assertEquals(
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION),
+ headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
+ }
+
+ @Test
+ public void regionalAccessBoundary_refreshShouldNotThrowWhenNoValidAccessTokenIsPassed()
+ throws IOException {
+
+ MockTokenServerTransport transport = new MockTokenServerTransport();
+ // Return an expired access token.
+ transport.addServiceAccount(SA_CLIENT_EMAIL, "expired-token");
+ transport.setExpiresInSeconds(-1);
+
+ 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();
+
+ // Should not throw, but just fail-open (no header).
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
+ }
+
+ @Test
+ public void regionalAccessBoundary_cooldownDoublingAndRefresh()
+ throws IOException, InterruptedException {
+
+ MockTokenServerTransport transport = new MockTokenServerTransport();
+ transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
+ // Always fail lookup for now.
+ transport.addResponseErrorSequence(new IOException("Persistent Failure"));
+
+ 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();
+
+ TestClock testClock = new TestClock();
+ credentials.clock = testClock;
+ credentials.regionalAccessBoundaryManager = new RegionalAccessBoundaryManager(testClock, 100);
+
+ // First attempt: triggers lookup, fails, enters 15m cooldown.
+ credentials.getRequestMetadata();
+ waitForCooldownActive(credentials);
+ assertTrue(credentials.regionalAccessBoundaryManager.isCooldownActive());
+ assertEquals(
+ 15 * 60 * 1000L, credentials.regionalAccessBoundaryManager.getCurrentCooldownMillis());
+
+ // Second attempt (during cooldown): does not trigger lookup.
+ credentials.getRequestMetadata();
+ assertTrue(credentials.regionalAccessBoundaryManager.isCooldownActive());
+
+ // Fast-forward past 15m cooldown.
+ testClock.advanceTime(16 * 60 * 1000L);
+ assertFalse(credentials.regionalAccessBoundaryManager.isCooldownActive());
+
+ // Third attempt (cooldown expired): triggers lookup, fails again, cooldown should double.
+ credentials.getRequestMetadata();
+ waitForCooldownActive(credentials);
+ assertTrue(credentials.regionalAccessBoundaryManager.isCooldownActive());
+ assertEquals(
+ 30 * 60 * 1000L, credentials.regionalAccessBoundaryManager.getCurrentCooldownMillis());
+
+ // Fast-forward past 30m cooldown.
+ testClock.advanceTime(31 * 60 * 1000L);
+ assertFalse(credentials.regionalAccessBoundaryManager.isCooldownActive());
+
+ // Set successful response.
+ transport.setRegionalAccessBoundary(
+ new RegionalAccessBoundary("0x123", Collections.emptyList(), null));
+
+ // Fourth attempt: triggers lookup, succeeds, resets cooldown.
+ credentials.getRequestMetadata();
+ waitForRegionalAccessBoundary(credentials);
+ assertFalse(credentials.regionalAccessBoundaryManager.isCooldownActive());
+ assertEquals("0x123", credentials.getRegionalAccessBoundary().getEncodedLocations());
+ assertEquals(
+ 15 * 60 * 1000L, credentials.regionalAccessBoundaryManager.getCurrentCooldownMillis());
+ }
+
+ @Test
+ 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));
+
+ // Should not throw, but just fail-open (no header).
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
+ }
+
+ @Test
+ public void regionalAccessBoundary_deduplicationOfConcurrentRefreshes()
+ throws IOException, InterruptedException {
+
+ MockTokenServerTransport transport = new MockTokenServerTransport();
+ transport.setRegionalAccessBoundary(
+ new RegionalAccessBoundary("valid", Collections.singletonList("us-central1"), null));
+ // 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();
+ }
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Only ONE request should have been made to the lookup endpoint.
+ assertEquals(1, transport.getRegionalAccessBoundaryRequestCount());
+ }
+
+ @Test
+ public void regionalAccessBoundary_shouldSkipRefreshForRegionalEndpoints() throws IOException {
+
+ MockTokenServerTransport transport = new MockTokenServerTransport();
+ GoogleCredentials credentials = createTestCredentials(transport);
+
+ URI regionalUri = URI.create("https://storage.us-central1.rep.googleapis.com/v1/b/foo");
+ credentials.getRequestMetadata(regionalUri);
+
+ // Should not have triggered any lookup.
+ assertEquals(0, transport.getRegionalAccessBoundaryRequestCount());
+ }
+
+ @Test
+ public void getRequestMetadata_ignoresRabRefreshException() throws IOException {
+ GoogleCredentials credentials =
+ new GoogleCredentials() {
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ return new AccessToken("token", null);
+ }
+
+ @Override
+ void refreshRegionalAccessBoundaryIfExpired(
+ @Nullable URI uri, @Nullable AccessToken token) throws IOException {
+ throw new IOException("Simulated RAB failure");
+ }
+ };
+
+ // This should not throw the IOException from refreshRegionalAccessBoundaryIfExpired
+ Map> metadata =
+ credentials.getRequestMetadata(URI.create("https://foo.com"));
+ assertTrue(metadata.containsKey("Authorization"));
+ }
+
+ @Test
+ public void getRequestMetadataAsync_ignoresRabRefreshException() throws IOException {
+ GoogleCredentials credentials =
+ new GoogleCredentials() {
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ return new AccessToken("token", null);
+ }
+
+ @Override
+ void refreshRegionalAccessBoundaryIfExpired(
+ @Nullable URI uri, @Nullable AccessToken token) throws IOException {
+ throw new IOException("Simulated RAB failure");
+ }
+ };
+
+ java.util.concurrent.atomic.AtomicBoolean success =
+ new java.util.concurrent.atomic.AtomicBoolean(false);
+ credentials.getRequestMetadata(
+ URI.create("https://foo.com"),
+ Runnable::run,
+ new RequestMetadataCallback() {
+ @Override
+ public void onSuccess(Map> metadata) {
+ success.set(true);
+ }
+
+ @Override
+ public void onFailure(Throwable exception) {
+ fail("Should not have failed");
+ }
+ });
+
+ assertTrue(success.get());
+ }
+
+ private GoogleCredentials createTestCredentials(MockTokenServerTransport transport)
+ throws IOException {
+ 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) {
+ Assertions.fail("Timed out waiting for regional access boundary refresh");
+ }
+ }
+
+ private void waitForCooldownActive(GoogleCredentials credentials) throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 5000;
+ while (!credentials.regionalAccessBoundaryManager.isCooldownActive()
+ && System.currentTimeMillis() < deadline) {
+ Thread.sleep(100);
+ }
+ if (!credentials.regionalAccessBoundaryManager.isCooldownActive()) {
+ Assertions.fail("Timed out waiting for cooldown to become active");
+ }
+ }
+
+ private static class TestClock implements Clock {
+ private final AtomicLong currentTime = new AtomicLong(System.currentTimeMillis());
+
+ @Override
+ public long currentTimeMillis() {
+ return currentTime.get();
+ }
+
+ public void advanceTime(long millis) {
+ currentTime.addAndGet(millis);
+ }
+ }
}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdTokenCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdTokenCredentialsTest.java
index e3dcec4b520c..56ebcc3f273e 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdTokenCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdTokenCredentialsTest.java
@@ -31,6 +31,7 @@
package com.google.auth.oauth2;
+import static com.google.auth.oauth2.TestUtils.createDummyRab;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
@@ -46,6 +47,7 @@ void hashCode_equals() throws IOException {
transportFactory.transport.setIdToken(ComputeEngineCredentialsTest.STANDARD_ID_TOKEN);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://foo.bar";
IdTokenCredentials tokenCredential =
@@ -72,6 +74,7 @@ void toString_equals() throws IOException {
transportFactory.transport.setIdToken(ComputeEngineCredentialsTest.STANDARD_ID_TOKEN);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://foo.bar";
IdTokenCredentials tokenCredential =
@@ -99,6 +102,7 @@ void serialize() throws IOException, ClassNotFoundException {
transportFactory.transport.setIdToken(ComputeEngineCredentialsTest.STANDARD_ID_TOKEN);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://foo.bar";
IdTokenCredentials tokenCredential =
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
index 674d523e5090..3a5dcd8720e7 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
@@ -34,12 +34,15 @@
import static com.google.auth.Credentials.GOOGLE_DEFAULT_UNIVERSE;
import static com.google.auth.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL;
import static com.google.auth.oauth2.OAuth2Utils.JSON_FACTORY;
+import static com.google.auth.oauth2.TestUtils.createDummyRab;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
@@ -75,6 +78,12 @@ class IdentityPoolCredentialsTest extends BaseSerializationTest {
private static final IdentityPoolSubjectTokenSupplier testProvider =
(ExternalAccountSupplierContext context) -> "testSubjectToken";
+ @org.junit.jupiter.api.BeforeEach
+ void setUp() {}
+
+ @org.junit.jupiter.api.AfterEach
+ void tearDown() {}
+
@Test
void createdScoped_clonedCredentialWithAddedScopes() {
IdentityPoolCredentials credentials =
@@ -85,10 +94,12 @@ void createdScoped_clonedCredentialWithAddedScopes() {
.setClientSecret("clientSecret")
.setUniverseDomain("universeDomain")
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
List newScopes = Arrays.asList("scope1", "scope2");
IdentityPoolCredentials newCredentials = credentials.createScoped(newScopes);
+ newCredentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(newCredentials.clock));
assertEquals(credentials.getAudience(), newCredentials.getAudience());
assertEquals(credentials.getSubjectTokenType(), newCredentials.getSubjectTokenType());
@@ -126,6 +137,7 @@ void retrieveSubjectToken_fileSourced() throws IOException {
IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setCredentialSource(credentialSource)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String subjectToken = credentials.retrieveSubjectToken();
@@ -167,6 +179,7 @@ void retrieveSubjectToken_fileSourcedWithJsonFormat() throws IOException {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(credentialSource)
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
String subjectToken = credential.retrieveSubjectToken();
@@ -205,6 +218,7 @@ void retrieveSubjectToken_noFile_throws() {
IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setCredentialSource(credentialSource)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
IOException e = assertThrows(IOException.class, credentials::retrieveSubjectToken);
assertEquals(
@@ -223,6 +237,7 @@ void retrieveSubjectToken_urlSourced() throws IOException {
.setCredentialSource(
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
String subjectToken = credential.retrieveSubjectToken();
@@ -248,6 +263,7 @@ void retrieveSubjectToken_urlSourcedWithJsonFormat() throws IOException {
.setHttpTransportFactory(transportFactory)
.setCredentialSource(credentialSource)
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
String subjectToken = credential.retrieveSubjectToken();
@@ -268,6 +284,7 @@ void retrieveSubjectToken_urlSourcedCredential_throws() {
.setCredentialSource(
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
IOException e = assertThrows(IOException.class, credential::retrieveSubjectToken);
assertEquals(
@@ -285,6 +302,7 @@ void retrieveSubjectToken_provider() throws IOException {
.setCredentialSource(null)
.setSubjectTokenSupplier(testProvider)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String subjectToken = credentials.retrieveSubjectToken();
@@ -304,6 +322,7 @@ void retrieveSubjectToken_providerThrowsError() {
.setCredentialSource(null)
.setSubjectTokenSupplier(errorProvider)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
IOException e = assertThrows(IOException.class, credentials::retrieveSubjectToken);
assertEquals("test", e.getMessage());
@@ -328,6 +347,7 @@ void retrieveSubjectToken_supplierPassesContext() throws IOException {
.setCredentialSource(null)
.setSubjectTokenSupplier(testSupplier)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
credentials.retrieveSubjectToken();
}
@@ -349,6 +369,7 @@ void refreshAccessToken_withoutServiceAccountImpersonation() throws IOException
.setCredentialSource(
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
AccessToken accessToken = credential.refreshAccessToken();
@@ -375,6 +396,7 @@ void refreshAccessToken_internalOptionsSet() throws IOException {
.setCredentialSource(
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
AccessToken accessToken = credential.refreshAccessToken();
@@ -412,6 +434,7 @@ void refreshAccessToken_withServiceAccountImpersonation() throws IOException {
.setCredentialSource(
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
AccessToken accessToken = credential.refreshAccessToken();
@@ -445,6 +468,7 @@ void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOExcept
.setServiceAccountImpersonationOptions(
ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions())
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
AccessToken accessToken = credential.refreshAccessToken();
@@ -481,6 +505,7 @@ void refreshAccessToken_Provider() throws IOException {
.setTokenUrl(transportFactory.transport.getStsUrl())
.setHttpTransportFactory(transportFactory)
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
AccessToken accessToken = credential.refreshAccessToken();
@@ -510,6 +535,7 @@ void refreshAccessToken_providerWithServiceAccountImpersonation() throws IOExcep
.setTokenUrl(transportFactory.transport.getStsUrl())
.setHttpTransportFactory(transportFactory)
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
AccessToken accessToken = credential.refreshAccessToken();
@@ -540,6 +566,7 @@ void refreshAccessToken_workforceWithServiceAccountImpersonation() throws IOExce
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
.setWorkforcePoolUserProject("userProject")
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
AccessToken accessToken = credential.refreshAccessToken();
@@ -577,6 +604,7 @@ void refreshAccessToken_workforceWithServiceAccountImpersonationOptions() throws
.setServiceAccountImpersonationOptions(
ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions())
.build();
+ credential.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credential.clock));
AccessToken accessToken = credential.refreshAccessToken();
@@ -760,6 +788,7 @@ void builder_allFields() {
.setScopes(scopes)
.setUniverseDomain("universeDomain")
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertEquals("audience", credentials.getAudience());
assertEquals("subjectTokenType", credentials.getSubjectTokenType());
@@ -794,6 +823,7 @@ void builder_subjectTokenSupplier() {
.setClientSecret("clientSecret")
.setScopes(scopes)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertEquals(testProvider, credentials.getIdentityPoolSubjectTokenSupplier());
}
@@ -845,6 +875,7 @@ void builder_emptyWorkforceUserProjectWithWorkforceAudience() {
.setCredentialSource(createFileCredentialSource())
.setQuotaProjectId("quotaProjectId")
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertTrue(credentials.isWorkforcePoolConfiguration());
}
@@ -899,6 +930,7 @@ void builder_missingUniverseDomain_defaults() {
.setClientSecret("clientSecret")
.setScopes(scopes)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertEquals("audience", credentials.getAudience());
assertEquals("subjectTokenType", credentials.getSubjectTokenType());
@@ -936,9 +968,13 @@ void newBuilder_allFields() {
.setWorkforcePoolUserProject("workforcePoolUserProject")
.setUniverseDomain("universeDomain")
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
IdentityPoolCredentials newBuilderCreds =
IdentityPoolCredentials.newBuilder(credentials).build();
+ newBuilderCreds.regionalAccessBoundaryManager.setCachedRAB(
+ new RegionalAccessBoundary(
+ "dummy-locations", Arrays.asList("dummy-loc"), newBuilderCreds.clock));
assertEquals(credentials.getAudience(), newBuilderCreds.getAudience());
assertEquals(credentials.getSubjectTokenType(), newBuilderCreds.getSubjectTokenType());
assertEquals(credentials.getTokenUrl(), newBuilderCreds.getTokenUrl());
@@ -977,9 +1013,13 @@ void newBuilder_noUniverseDomain_defaults() {
.setScopes(scopes)
.setWorkforcePoolUserProject("workforcePoolUserProject")
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
IdentityPoolCredentials newBuilderCreds =
IdentityPoolCredentials.newBuilder(credentials).build();
+ newBuilderCreds.regionalAccessBoundaryManager.setCachedRAB(
+ new RegionalAccessBoundary(
+ "dummy-locations", Arrays.asList("dummy-loc"), newBuilderCreds.clock));
assertEquals(credentials.getAudience(), newBuilderCreds.getAudience());
assertEquals(credentials.getSubjectTokenType(), newBuilderCreds.getSubjectTokenType());
assertEquals(credentials.getTokenUrl(), newBuilderCreds.getTokenUrl());
@@ -1008,6 +1048,9 @@ void serialize() throws IOException, ClassNotFoundException {
.setClientSecret("clientSecret")
.setUniverseDomain("universeDomain")
.build();
+ testCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ new RegionalAccessBoundary(
+ "dummy-locations", Arrays.asList("dummy-loc"), testCredentials.clock));
IdentityPoolCredentials deserializedCredentials = serializeAndDeserialize(testCredentials);
assertEquals(testCredentials, deserializedCredentials);
@@ -1037,6 +1080,7 @@ void build_withCertificateSource_succeeds() throws Exception {
.setSubjectTokenType("test-token-type")
.setCredentialSource(credentialSource)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
// Verify successful creation and correct internal setup.
assertNotNull(credentials, "Credentials should be successfully created");
@@ -1079,6 +1123,7 @@ void build_withDefaultCertificateConfig_success()
.setSubjectTokenType("test-token-type")
.setCredentialSource(credentialSource)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
// Verify successful creation and correct internal setup.
assertNotNull(credentials, "Credentials should be successfully created");
@@ -1248,15 +1293,18 @@ private IdentityPoolCredentials createBaseFileSourcedCredentials() {
IdentityPoolCredentialSource identityPoolCredentialSource =
new IdentityPoolCredentialSource(fileCredentialSourceMap);
- return IdentityPoolCredentials.newBuilder()
- .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
- .setAudience(
- "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
- .setSubjectTokenType("subjectTokenType")
- .setTokenUrl(STS_URL)
- .setTokenInfoUrl("tokenInfoUrl")
- .setCredentialSource(identityPoolCredentialSource)
- .build();
+ IdentityPoolCredentials credentials =
+ IdentityPoolCredentials.newBuilder()
+ .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
+ .setAudience(
+ "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl(STS_URL)
+ .setTokenInfoUrl("tokenInfoUrl")
+ .setCredentialSource(identityPoolCredentialSource)
+ .build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
+ return credentials;
}
private IdentityPoolCredentialSource createFileCredentialSource() {
@@ -1299,4 +1347,46 @@ void setShouldThrowOnGetKeyStore(boolean shouldThrow) {
this.shouldThrowOnGetKeyStore = shouldThrow;
}
}
+
+ @Test
+ public void testRefresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
+
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+ HttpTransportFactory testingHttpTransportFactory = transportFactory;
+
+ IdentityPoolCredentials credentials =
+ IdentityPoolCredentials.newBuilder()
+ .setSubjectTokenSupplier(testProvider)
+ .setHttpTransportFactory(testingHttpTransportFactory)
+ .setAudience(
+ "//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/pool/providers/provider")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl(STS_URL)
+ .build();
+
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
+
+ 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/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
index 044aa0ce6755..3664fb22c2ff 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
@@ -31,6 +31,8 @@
package com.google.auth.oauth2;
+import static com.google.auth.oauth2.RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY;
+import static com.google.auth.oauth2.TestUtils.createDummyRab;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -40,6 +42,7 @@
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -69,6 +72,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
+import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -145,6 +149,11 @@ 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 RegionalAccessBoundary REGIONAL_ACCESS_BOUNDARY =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS,
+ null);
private GoogleCredentials sourceCredentials;
private MockIAMCredentialsServiceTransportFactory mockTransportFactory;
@@ -167,7 +176,10 @@ static GoogleCredentials getSourceCredentials() throws IOException {
.setProjectId(PROJECT_ID)
.setHttpTransportFactory(transportFactory)
.build();
+ sourceCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(sourceCredentials.clock));
transportFactory.transport.addServiceAccount(SA_CLIENT_EMAIL, ACCESS_TOKEN);
+ transportFactory.transport.setRegionalAccessBoundary(REGIONAL_ACCESS_BOUNDARY);
return sourceCredentials;
}
@@ -583,6 +595,8 @@ void getRequestMetadata_withQuotaProjectId() throws IOException, IllegalStateExc
VALID_LIFETIME,
mockTransportFactory,
QUOTA_PROJECT_ID);
+ targetCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(targetCredentials.clock));
Map> metadata = targetCredentials.getRequestMetadata();
assertTrue(metadata.containsKey("x-goog-user-project"));
@@ -605,6 +619,8 @@ void getRequestMetadata_withoutQuotaProjectId() throws IOException, IllegalState
IMMUTABLE_SCOPES_LIST,
VALID_LIFETIME,
mockTransportFactory);
+ targetCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(targetCredentials.clock));
Map> metadata = targetCredentials.getRequestMetadata();
assertFalse(metadata.containsKey("x-goog-user-project"));
@@ -1260,6 +1276,54 @@ void refreshAccessToken_afterSerialization_success() throws IOException, ClassNo
assertEquals(ACCESS_TOKEN, token.getTokenValue());
}
+ @Test
+ void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
+
+ // Mock regional access boundary response
+ RegionalAccessBoundary regionalAccessBoundary = REGIONAL_ACCESS_BOUNDARY;
+
+ mockTransportFactory.getTransport().setRegionalAccessBoundary(regionalAccessBoundary);
+ 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);
+
+ // First call: initiates async refresh.
+ Map> headers = targetCredentials.getRequestMetadata();
+ assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(targetCredentials);
+
+ // Second call: should have header.
+ headers = targetCredentials.getRequestMetadata();
+ assertEquals(
+ headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Collections.singletonList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
+
+ 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() {
return Instant.now().plusSeconds(VALID_LIFETIME).truncatedTo(ChronoUnit.SECONDS).toString();
}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java
index 524a312ce0c1..92ea38cbcf7d 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java
@@ -43,6 +43,7 @@
import static com.google.auth.oauth2.ServiceAccountCredentialsTest.DEFAULT_ID_TOKEN;
import static com.google.auth.oauth2.ServiceAccountCredentialsTest.SCOPES;
import static com.google.auth.oauth2.ServiceAccountCredentialsTest.createDefaultBuilder;
+import static com.google.auth.oauth2.TestUtils.createDummyRab;
import static com.google.auth.oauth2.UserCredentialsTest.CLIENT_ID;
import static com.google.auth.oauth2.UserCredentialsTest.CLIENT_SECRET;
import static com.google.auth.oauth2.UserCredentialsTest.REFRESH_TOKEN;
@@ -94,12 +95,16 @@ static void setup() {
LoggingUtils.setEnvironmentProvider(testEnvironmentProvider);
}
+ @org.junit.jupiter.api.BeforeEach
+ void setUp() {}
+
@Test
void userCredentials_getRequestMetadata_fromRefreshToken_hasAccessToken() throws IOException {
TestAppender testAppender = setupTestLogger(UserCredentials.class);
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
transportFactory.transport.addClient(CLIENT_ID, CLIENT_SECRET);
transportFactory.transport.addRefreshToken(REFRESH_TOKEN, ACCESS_TOKEN);
+
UserCredentials userCredentials =
UserCredentials.newBuilder()
.setClientId(CLIENT_ID)
@@ -163,6 +168,7 @@ void serviceAccountCredentials_getRequestMetadata_hasAccessToken() throws IOExce
ServiceAccountCredentialsTest.createDefaultBuilderWithToken(ACCESS_TOKEN)
.setScopes(SCOPES)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN);
@@ -212,12 +218,14 @@ void serviceAccountCredentials_idTokenWithAudience_iamFlow_targetAudienceMatches
transportFactory.getTransport().setTargetPrincipal(CLIENT_EMAIL);
transportFactory.getTransport().setIdToken(DEFAULT_ID_TOKEN);
transportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
+
ServiceAccountCredentials credentials =
createDefaultBuilder()
.setScopes(SCOPES)
.setHttpTransportFactory(transportFactory)
.setUniverseDomain(nonGDU)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://foo.bar";
IdTokenCredentials tokenCredential =
@@ -439,11 +447,12 @@ void getRequestMetadata_hasAccessToken() throws IOException {
transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL");
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN);
- assertEquals(3, testAppender.events.size());
+ assertEquals(5, testAppender.events.size());
ILoggingEvent accessTokenRequest = testAppender.events.get(0);
assertEquals("Sending request to refresh access token", accessTokenRequest.getMessage());
@@ -480,6 +489,7 @@ void idTokenWithAudience_full() throws IOException {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://foo.bar";
IdTokenCredentials tokenCredential =
@@ -534,6 +544,7 @@ void serviceAccountCredentials_exchangeToken_masksSensitiveTokens() throws IOExc
ServiceAccountCredentialsTest.createDefaultBuilderWithToken(ACCESS_TOKEN)
.setScopes(SCOPES)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN);
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
index 7719b08d2e7b..c53eda5b2bd5 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
+++ b/google-auth-library-java/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,6 +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 REGIONAL_ACCESS_BOUNDARY_URL_END = "/allowedLocations";
private static final String SUBJECT_TOKEN = "subjectToken";
private static final String TOKEN_TYPE = "Bearer";
@@ -92,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);
@@ -196,6 +203,26 @@ public LowLevelHttpResponse execute() throws IOException {
}
if (url.contains(IAM_ENDPOINT)) {
+
+ 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,
+ null);
+ }
+ GenericJson responseJson = new GenericJson();
+ responseJson.setFactory(OAuth2Utils.JSON_FACTORY);
+ responseJson.put("encodedLocations", rab.getEncodedLocations());
+ responseJson.put("locations", rab.getLocations());
+ String content = responseJson.toPrettyString();
+ return new MockLowLevelHttpResponse()
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(content);
+ }
+
GenericJson query =
OAuth2Utils.JSON_FACTORY
.createJsonParser(getContentAsString())
@@ -220,7 +247,9 @@ public LowLevelHttpResponse execute() throws IOException {
}
};
- this.requests.add(request);
+ if (url == null || !url.contains("allowedLocations")) {
+ this.requests.add(request);
+ }
return request;
}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java
index cbd57d115afe..5346f4fdba3d 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockIAMCredentialsServiceTransport.java
@@ -80,6 +80,8 @@ public ServerResponse(int statusCode, String response, boolean repeatServerRespo
private String universeDomain;
+ private RegionalAccessBoundary regionalAccessBoundary;
+
private MockLowLevelHttpRequest request;
MockIAMCredentialsServiceTransport(String universeDomain) {
@@ -132,6 +134,10 @@ public void setAccessTokenEndpoint(String accessTokenEndpoint) {
this.iamAccessTokenEndpoint = accessTokenEndpoint;
}
+ public void setRegionalAccessBoundary(RegionalAccessBoundary regionalAccessBoundary) {
+ this.regionalAccessBoundary = regionalAccessBoundary;
+ }
+
public MockLowLevelHttpRequest getRequest() {
return request;
}
@@ -221,6 +227,25 @@ public LowLevelHttpResponse execute() throws IOException {
.setContent(tokenContent);
}
};
+ } else if (url.endsWith("/allowedLocations")) {
+ request =
+ new MockLowLevelHttpRequest(url) {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ if (regionalAccessBoundary == null) {
+ return new MockLowLevelHttpResponse().setStatusCode(404);
+ }
+ GenericJson responseJson = new GenericJson();
+ responseJson.setFactory(OAuth2Utils.JSON_FACTORY);
+ responseJson.put("encodedLocations", regionalAccessBoundary.getEncodedLocations());
+ responseJson.put("locations", regionalAccessBoundary.getLocations());
+ String content = responseJson.toPrettyString();
+ return new MockLowLevelHttpResponse()
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(content);
+ }
+ };
+ return request;
} else {
return super.buildRequest(method, url);
}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java
index 1b218b73ef45..92b24d60fd53 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java
@@ -72,6 +72,9 @@ public class MockMetadataServerTransport extends MockHttpTransport {
private boolean emptyContent;
private MockLowLevelHttpRequest request;
+ private RegionalAccessBoundary regionalAccessBoundary;
+ private IOException lookupError;
+
public MockMetadataServerTransport() {}
public MockMetadataServerTransport(String accessToken) {
@@ -119,6 +122,14 @@ public void setEmptyContent(boolean emptyContent) {
this.emptyContent = emptyContent;
}
+ public void setRegionalAccessBoundary(RegionalAccessBoundary regionalAccessBoundary) {
+ this.regionalAccessBoundary = regionalAccessBoundary;
+ }
+
+ public void setLookupError(IOException lookupError) {
+ this.lookupError = lookupError;
+ }
+
public MockLowLevelHttpRequest getRequest() {
return request;
}
@@ -139,6 +150,8 @@ public LowLevelHttpRequest buildRequest(String method, String url) throws IOExce
return this.request;
} else if (isMtlsConfigRequestUrl(url)) {
return getMockRequestForMtlsConfig(url);
+ } else if (isIamLookupUrl(url)) {
+ return getMockRequestForRegionalAccessBoundaryLookup(url);
}
this.request =
new MockLowLevelHttpRequest(url) {
@@ -213,7 +226,7 @@ public LowLevelHttpResponse execute() throws IOException {
refreshContents.put(
"access_token", scopesToAccessToken.get("[" + urlParsed.get(1) + "]"));
}
- refreshContents.put("expires_in", 3600000);
+ refreshContents.put("expires_in", 3600);
refreshContents.put("token_type", "Bearer");
String refreshText = refreshContents.toPrettyString();
@@ -346,4 +359,32 @@ protected boolean isMtlsConfigRequestUrl(String url) {
ComputeEngineCredentials.getMetadataServerUrl()
+ SecureSessionAgent.S2A_CONFIG_ENDPOINT_POSTFIX);
}
+
+ private MockLowLevelHttpRequest getMockRequestForRegionalAccessBoundaryLookup(String url) {
+ return new MockLowLevelHttpRequest(url) {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ 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", regionalAccessBoundary.getEncodedLocations());
+ responseJson.put("locations", regionalAccessBoundary.getLocations());
+ String content = responseJson.toPrettyString();
+ return new MockLowLevelHttpResponse().setContentType(Json.MEDIA_TYPE).setContent(content);
+ }
+ };
+ }
+
+ protected boolean isIamLookupUrl(String url) {
+ // 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.
+ return url.endsWith("/allowedLocations");
+ }
}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java
index cdb0a068e2d0..24566a0e5ca3 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java
@@ -62,6 +62,8 @@ 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_REGIONAL_ACCESS_BOUNDARY_PATTERN =
+ "https:\\/\\/iam.[a-z-_\\.]+\\/v1\\/.*\\/allowedLocations";
private static final String ACCESS_TOKEN = "accessToken";
private static final String TOKEN_TYPE = "Bearer";
private static final Long EXPIRES_IN = 3600L;
@@ -99,6 +101,23 @@ public LowLevelHttpRequest buildRequest(final String method, final String url) {
new MockLowLevelHttpRequest(url) {
@Override
public LowLevelHttpResponse execute() throws IOException {
+ // Mocking call to refresh regional access boundaries.
+ // The lookup endpoint is located in the IAM server.
+ 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.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));
+ }
+
// Environment version is prefixed by "aws". e.g. "aws1".
Matcher matcher = Pattern.compile(VALID_STS_PATTERN).matcher(url);
if (!matcher.matches()) {
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java
index 5a6cd2e5d1a8..62f31e256d24 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockTokenServerTransport.java
@@ -76,6 +76,21 @@ public class MockTokenServerTransport extends MockHttpTransport {
private int expiresInSeconds = 3600;
private MockLowLevelHttpRequest request;
private PKCEProvider pkceProvider;
+ private RegionalAccessBoundary regionalAccessBoundary;
+ private int regionalAccessBoundaryRequestCount = 0;
+ private int responseDelayMillis = 0;
+
+ public void setRegionalAccessBoundary(RegionalAccessBoundary regionalAccessBoundary) {
+ this.regionalAccessBoundary = regionalAccessBoundary;
+ }
+
+ public int getRegionalAccessBoundaryRequestCount() {
+ return regionalAccessBoundaryRequestCount;
+ }
+
+ public void setResponseDelayMillis(int responseDelayMillis) {
+ this.responseDelayMillis = responseDelayMillis;
+ }
public MockTokenServerTransport() {}
@@ -171,6 +186,40 @@ public LowLevelHttpRequest buildRequest(String method, String url) throws IOExce
int questionMarkPos = url.indexOf('?');
final String urlWithoutQuery = (questionMarkPos > 0) ? url.substring(0, questionMarkPos) : url;
+ if (urlWithoutQuery.endsWith("/allowedLocations")) {
+ // 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.
+ request =
+ new MockLowLevelHttpRequest(url) {
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ regionalAccessBoundaryRequestCount++;
+ if (responseDelayMillis > 0) {
+ try {
+ Thread.sleep(responseDelayMillis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ RegionalAccessBoundary rab = regionalAccessBoundary;
+ if (rab == null) {
+ return new MockLowLevelHttpResponse().setStatusCode(404);
+ }
+ GenericJson responseJson = new GenericJson();
+ responseJson.setFactory(JSON_FACTORY);
+ responseJson.put("encodedLocations", rab.getEncodedLocations());
+ responseJson.put("locations", rab.getLocations());
+ String content = responseJson.toPrettyString();
+ return new MockLowLevelHttpResponse()
+ .setContentType(Json.MEDIA_TYPE)
+ .setContent(content);
+ }
+ };
+ return request;
+ }
+
if (!responseSequence.isEmpty()) {
request =
new MockLowLevelHttpRequest(url) {
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java
index 094b21f9dbb2..8576ffe38e3a 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java
@@ -36,6 +36,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
@@ -56,6 +57,10 @@
/** Tests for {@link PluggableAuthCredentials}. */
class PluggableAuthCredentialsTest extends BaseSerializationTest {
+
+ @org.junit.jupiter.api.AfterEach
+ void tearDown() {}
+
// The default timeout for waiting for the executable to finish (30 seconds).
private static final int DEFAULT_EXECUTABLE_TIMEOUT_MS = 30 * 1000;
// The minimum timeout for waiting for the executable to finish (5 seconds).
@@ -601,6 +606,49 @@ void serialize() {
assertThrows(NotSerializableException.class, () -> serializeAndDeserialize(testCredentials));
}
+ @Test
+ public void testRefresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
+
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+ transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
+
+ PluggableAuthCredentials credentials =
+ PluggableAuthCredentials.newBuilder()
+ .setHttpTransportFactory(transportFactory)
+ .setAudience(
+ "//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/pool/providers/provider")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl(transportFactory.transport.getStsUrl())
+ .setCredentialSource(buildCredentialSource())
+ .setExecutableHandler(options -> "pluggableAuthToken")
+ .build();
+
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
+
+ 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() {
return buildCredentialSource("command", null, null);
}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/RegionalAccessBoundaryTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/RegionalAccessBoundaryTest.java
new file mode 100644
index 000000000000..5664582ef059
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/RegionalAccessBoundaryTest.java
@@ -0,0 +1,337 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.api.client.testing.http.MockHttpTransport;
+import com.google.api.client.testing.http.MockLowLevelHttpResponse;
+import com.google.api.client.util.Clock;
+import com.google.auth.http.HttpTransportFactory;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicLong;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class RegionalAccessBoundaryTest {
+
+ private static final long TTL = RegionalAccessBoundary.TTL_MILLIS;
+ private static final long REFRESH_THRESHOLD = RegionalAccessBoundary.REFRESH_THRESHOLD_MILLIS;
+
+ private TestClock testClock;
+
+ @BeforeEach
+ public void setUp() {
+ testClock = new TestClock();
+ }
+
+ @AfterEach
+ public void tearDown() {}
+
+ @Test
+ public void testIsExpired() {
+ long now = testClock.currentTimeMillis();
+ RegionalAccessBoundary rab =
+ new RegionalAccessBoundary("encoded", Collections.singletonList("loc"), now, testClock);
+
+ assertFalse(rab.isExpired());
+
+ testClock.set(now + TTL - 1);
+ assertFalse(rab.isExpired());
+
+ testClock.set(now + TTL + 1);
+ assertTrue(rab.isExpired());
+ }
+
+ @Test
+ public void testShouldRefresh() {
+ long now = testClock.currentTimeMillis();
+ RegionalAccessBoundary rab =
+ new RegionalAccessBoundary("encoded", Collections.singletonList("loc"), now, testClock);
+
+ // Initial state: fresh
+ assertFalse(rab.shouldRefresh());
+
+ // Just before threshold
+ testClock.set(now + TTL - REFRESH_THRESHOLD - 1);
+ assertFalse(rab.shouldRefresh());
+
+ // At threshold
+ testClock.set(now + TTL - REFRESH_THRESHOLD + 1);
+ assertTrue(rab.shouldRefresh());
+
+ // Still not expired
+ assertFalse(rab.isExpired());
+ }
+
+ @Test
+ public void testSerialization() throws Exception {
+ long now = testClock.currentTimeMillis();
+ RegionalAccessBoundary rab =
+ new RegionalAccessBoundary("encoded", Collections.singletonList("loc"), now, testClock);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(rab);
+ oos.close();
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ ObjectInputStream ois = new ObjectInputStream(bais);
+ RegionalAccessBoundary deserializedRab = (RegionalAccessBoundary) ois.readObject();
+ ois.close();
+
+ assertEquals("encoded", deserializedRab.getEncodedLocations());
+ assertEquals(1, deserializedRab.getLocations().size());
+ assertEquals("loc", deserializedRab.getLocations().get(0));
+ // The transient clock field should be restored to Clock.SYSTEM upon deserialization,
+ // thereby avoiding a NullPointerException when checking expiration.
+ assertFalse(deserializedRab.isExpired());
+ }
+
+ @Test
+ public void testRefreshClosesResponse() throws Exception {
+ final String url = "https://example.com/rab";
+ final AccessToken token =
+ new AccessToken("token", new java.util.Date(System.currentTimeMillis() + 3600000L));
+
+ TrackingMockLowLevelHttpResponse mockResponse = new TrackingMockLowLevelHttpResponse();
+ mockResponse.setContentType("application/json");
+ mockResponse.setContent("{\"encodedLocations\": \"encoded\", \"locations\": [\"loc\"]}");
+
+ MockHttpTransport transport =
+ new MockHttpTransport.Builder().setLowLevelHttpResponse(mockResponse).build();
+ HttpTransportFactory transportFactory = () -> transport;
+
+ RegionalAccessBoundary rab =
+ RegionalAccessBoundary.refresh(transportFactory, url, token, testClock, 1000);
+
+ assertEquals("encoded", rab.getEncodedLocations());
+ assertTrue(mockResponse.isDisconnected(), "Response should have been disconnected");
+ }
+
+ @Test
+ public void testManagerTriggersRefreshInGracePeriod() throws InterruptedException {
+ final String url =
+ "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/default:allowedLocations";
+ final AccessToken token =
+ new AccessToken(
+ "token", new java.util.Date(System.currentTimeMillis() + 10 * 3600000L)); //
+
+ // Mock transport to return a new RAB
+ final String newEncoded = "new-encoded";
+ MockHttpTransport transport =
+ new MockHttpTransport.Builder()
+ .setLowLevelHttpResponse(
+ new MockLowLevelHttpResponse()
+ .setContentType("application/json")
+ .setContent(
+ "{\"encodedLocations\": \""
+ + newEncoded
+ + "\", \"locations\": [\"new-loc\"]}"))
+ .build();
+ HttpTransportFactory transportFactory = () -> transport;
+ RegionalAccessBoundaryProvider provider = () -> url;
+
+ RegionalAccessBoundaryManager manager = new RegionalAccessBoundaryManager(testClock);
+
+ // 1. Let's first get a RAB into the cache
+ manager.triggerAsyncRefresh(transportFactory, provider, token);
+
+ // Wait for it to be cached
+ int retries = 0;
+ while (manager.getCachedRAB() == null && retries < 50) {
+ Thread.sleep(50);
+ retries++;
+ }
+ assertEquals(newEncoded, manager.getCachedRAB().getEncodedLocations());
+
+ // 2. Advance clock to grace period
+ testClock.set(testClock.currentTimeMillis() + TTL - REFRESH_THRESHOLD + 1000);
+
+ assertTrue(manager.getCachedRAB().shouldRefresh());
+ assertFalse(manager.getCachedRAB().isExpired());
+
+ // 3. Prepare mock for SECOND refresh
+ final String newerEncoded = "newer-encoded";
+ MockHttpTransport transport2 =
+ new MockHttpTransport.Builder()
+ .setLowLevelHttpResponse(
+ new MockLowLevelHttpResponse()
+ .setContentType("application/json")
+ .setContent(
+ "{\"encodedLocations\": \""
+ + newerEncoded
+ + "\", \"locations\": [\"newer-loc\"]}"))
+ .build();
+ HttpTransportFactory transportFactory2 = () -> transport2;
+
+ // 4. Trigger refresh - should start because we are in grace period
+ manager.triggerAsyncRefresh(transportFactory2, provider, token);
+
+ // 5. Wait for background refresh to complete
+ // We expect the cached RAB to eventually change to newerEncoded
+ retries = 0;
+ RegionalAccessBoundary resultRab = null;
+ while (retries < 100) {
+ resultRab = manager.getCachedRAB();
+ if (resultRab != null && newerEncoded.equals(resultRab.getEncodedLocations())) {
+ break;
+ }
+ Thread.sleep(50);
+ retries++;
+ }
+
+ assertTrue(
+ resultRab != null && newerEncoded.equals(resultRab.getEncodedLocations()),
+ "Refresh should have completed and updated the cache within 5 seconds");
+ assertEquals(newerEncoded, resultRab.getEncodedLocations());
+ }
+
+ @Test
+ public void testExecutorQueueCapacityLimit() throws Exception {
+ final String url = "https://example.com/rab";
+ final AccessToken token =
+ new AccessToken("token", new java.util.Date(System.currentTimeMillis() + 3600000L));
+ RegionalAccessBoundaryProvider provider = () -> url;
+
+ int poolSize = 5;
+ int queueCapacity = 100;
+ int totalCapacity = poolSize + queueCapacity;
+
+ java.util.concurrent.ThreadPoolExecutor testExecutor =
+ new java.util.concurrent.ThreadPoolExecutor(
+ poolSize,
+ poolSize,
+ 1,
+ java.util.concurrent.TimeUnit.HOURS,
+ new java.util.concurrent.LinkedBlockingQueue<>(queueCapacity),
+ r -> {
+ Thread t = new Thread(r, "test-RAB-refresh");
+ t.setDaemon(true);
+ return t;
+ });
+
+ CountDownLatch latch = new CountDownLatch(1);
+
+ java.io.InputStream blockingStream =
+ new java.io.InputStream() {
+ private final java.io.InputStream delegate =
+ new ByteArrayInputStream(
+ "{\"encodedLocations\": \"encoded\", \"locations\": [\"loc\"]}".getBytes());
+ private boolean blocked = false;
+
+ @Override
+ public int read() throws java.io.IOException {
+ if (!blocked) {
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ blocked = true;
+ }
+ return delegate.read();
+ }
+ };
+
+ MockHttpTransport transport =
+ new MockHttpTransport.Builder()
+ .setLowLevelHttpResponse(
+ new MockLowLevelHttpResponse()
+ .setContent(blockingStream)
+ .setContentType("application/json"))
+ .build();
+ HttpTransportFactory transportFactory = () -> transport;
+
+ RegionalAccessBoundaryManager[] managers = new RegionalAccessBoundaryManager[totalCapacity];
+ for (int i = 0; i < totalCapacity; i++) {
+ managers[i] =
+ new RegionalAccessBoundaryManager(
+ testClock,
+ RegionalAccessBoundaryManager.DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS,
+ testExecutor);
+ managers[i].triggerAsyncRefresh(transportFactory, provider, token);
+ }
+
+ RegionalAccessBoundaryManager extraManager =
+ new RegionalAccessBoundaryManager(
+ testClock,
+ RegionalAccessBoundaryManager.DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS,
+ testExecutor);
+ assertFalse(extraManager.isCooldownActive());
+
+ extraManager.triggerAsyncRefresh(transportFactory, provider, token);
+
+ assertFalse(
+ extraManager.isCooldownActive(),
+ "106th task should NOT have entered cooldown on scheduling failure");
+
+ latch.countDown();
+ testExecutor.shutdownNow();
+ }
+
+ private static class TestClock implements Clock {
+ private final AtomicLong currentTime = new AtomicLong(System.currentTimeMillis());
+
+ @Override
+ public long currentTimeMillis() {
+ return currentTime.get();
+ }
+
+ public void set(long millis) {
+ currentTime.set(millis);
+ }
+ }
+
+ private static class TrackingMockLowLevelHttpResponse extends MockLowLevelHttpResponse {
+ private boolean disconnected = false;
+
+ @Override
+ public void disconnect() throws IOException {
+ super.disconnect();
+ disconnected = true;
+ }
+
+ public boolean isDisconnected() {
+ return disconnected;
+ }
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java
index 6ee26e3338a0..e1582ebe2b3a 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java
@@ -31,6 +31,8 @@
package com.google.auth.oauth2;
+import static com.google.auth.oauth2.RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY;
+import static com.google.auth.oauth2.TestUtils.createDummyRab;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -155,6 +157,12 @@ static ServiceAccountCredentials.Builder createDefaultBuilder() throws IOExcepti
return createDefaultBuilderWithKey(privateKey);
}
+ @org.junit.jupiter.api.BeforeEach
+ void setUp() {}
+
+ @org.junit.jupiter.api.AfterEach
+ void tearDown() {}
+
@Test
void setLifetime() throws IOException {
ServiceAccountCredentials.Builder builder = createDefaultBuilder();
@@ -359,6 +367,7 @@ void createAssertionForIdToken_incorrect() throws IOException {
@Test
void createdScoped_withAud_noUniverse_jwtWithScopesDisabled_accessToken() throws IOException {
GoogleCredentials credentials = createDefaultBuilderWithToken(ACCESS_TOKEN).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
// No aud, no scopes gives an exception.
IOException exception =
@@ -368,6 +377,8 @@ void createdScoped_withAud_noUniverse_jwtWithScopesDisabled_accessToken() throws
"expected to fail with exception");
GoogleCredentials scopedCredentials = credentials.createScoped(SCOPES);
+ scopedCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(scopedCredentials.clock));
assertEquals(false, credentials.isExplicitUniverseDomain());
assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, credentials.getUniverseDomain());
Map> metadata = scopedCredentials.getRequestMetadata(CALL_URI);
@@ -378,17 +389,22 @@ void createdScoped_withAud_noUniverse_jwtWithScopesDisabled_accessToken() throws
void createdScoped_withUniverse_selfSignedJwt() throws IOException {
ServiceAccountCredentials credentials =
createDefaultBuilder().setUniverseDomain("foo.bar").build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
IOException exception =
assertThrows(IOException.class, () -> credentials.getRequestMetadata(null));
assertTrue(
exception.getMessage().contains("Scopes and uri are not configured for service account"));
GoogleCredentials scopedCredentials = credentials.createScoped("dummy.scope");
+ scopedCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(scopedCredentials.clock));
Map> metadata = scopedCredentials.getRequestMetadata(null);
verifyJwtAccess(metadata, "dummy.scope");
// Recreate to avoid jwt caching.
scopedCredentials = credentials.createScoped("dummy.scope2");
+ scopedCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(scopedCredentials.clock));
assertEquals(true, scopedCredentials.isExplicitUniverseDomain());
assertEquals("foo.bar", scopedCredentials.getUniverseDomain());
metadata = scopedCredentials.getRequestMetadata(CALL_URI);
@@ -398,6 +414,8 @@ void createdScoped_withUniverse_selfSignedJwt() throws IOException {
scopedCredentials =
credentials.createScoped(
Collections.emptyList(), Arrays.asList("dummy.default.scope"));
+ scopedCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(scopedCredentials.clock));
metadata = scopedCredentials.getRequestMetadata(null);
verifyJwtAccess(metadata, "dummy.default.scope");
@@ -405,6 +423,8 @@ void createdScoped_withUniverse_selfSignedJwt() throws IOException {
scopedCredentials =
credentials.createScoped(
Collections.emptyList(), Arrays.asList("dummy.default.scope2"));
+ scopedCredentials.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(scopedCredentials.clock));
metadata = scopedCredentials.getRequestMetadata(CALL_URI);
verifyJwtAccess(metadata, "dummy.default.scope2");
}
@@ -525,6 +545,7 @@ void fromJSON_hasAccessToken() throws IOException {
GenericJson json = writeServiceAccountJson(PROJECT_ID, null, null);
GoogleCredentials credentials = ServiceAccountCredentials.fromJson(json, transportFactory);
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
credentials = credentials.createScoped(SCOPES);
Map> metadata = credentials.getRequestMetadata(CALL_URI);
@@ -538,6 +559,7 @@ void fromJSON_withUniverse_selfSignedJwt() throws IOException {
GenericJson json = writeServiceAccountJson(PROJECT_ID, null, "foo.bar");
GoogleCredentials credentials = ServiceAccountCredentials.fromJson(json, transportFactory);
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
credentials = credentials.createScoped(SCOPES);
Map> metadata = credentials.getRequestMetadata(null);
@@ -562,6 +584,7 @@ void fromJson_hasQuotaProjectId() throws IOException {
transportFactory.transport.addServiceAccount(CLIENT_EMAIL, ACCESS_TOKEN);
GenericJson json = writeServiceAccountJson(PROJECT_ID, QUOTA_PROJECT, null);
GoogleCredentials credentials = ServiceAccountCredentials.fromJson(json, transportFactory);
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
credentials = credentials.createScoped(SCOPES);
Map> metadata = credentials.getRequestMetadata(CALL_URI);
@@ -576,6 +599,7 @@ void fromJson_hasQuotaProjectId() throws IOException {
void getRequestMetadata_hasAccessToken() throws IOException {
GoogleCredentials credentials =
createDefaultBuilderWithToken(ACCESS_TOKEN).setScopes(SCOPES).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN);
}
@@ -586,12 +610,13 @@ void getRequestMetadata_customTokenServer_hasAccessToken() throws IOException {
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
transportFactory.transport.addServiceAccount(CLIENT_EMAIL, ACCESS_TOKEN);
transportFactory.transport.setTokenServerUri(tokenServerUri);
- OAuth2Credentials credentials =
+ ServiceAccountCredentials credentials =
createDefaultBuilder()
.setScopes(SCOPES)
.setHttpTransportFactory(transportFactory)
.setTokenServerUri(tokenServerUri)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN);
@@ -616,6 +641,7 @@ void refreshAccessToken_refreshesToken() throws IOException {
MockTokenServerTransport transport = transportFactory.transport;
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, accessToken1);
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), accessToken1);
@@ -631,6 +657,7 @@ void refreshAccessToken_tokenExpiry() throws IOException {
transport.addServiceAccount(CLIENT_EMAIL, ACCESS_TOKEN);
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
credentials.clock = new FixedClock(0L);
AccessToken accessToken = credentials.refreshAccessToken();
@@ -652,6 +679,7 @@ void refreshAccessToken_IOException_Retry() throws IOException {
MockTokenServerTransport transport = transportFactory.transport;
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, accessToken1);
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), accessToken1);
@@ -670,6 +698,7 @@ void refreshAccessToken_retriesServerErrors() throws IOException {
MockTokenServerTransport transport = transportFactory.transport;
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, accessToken1);
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), accessToken1);
@@ -690,6 +719,7 @@ void refreshAccessToken_retriesTimeoutAndThrottled() throws IOException {
MockTokenServerTransport transport = transportFactory.transport;
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, accessToken1);
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), accessToken1);
@@ -714,6 +744,7 @@ void refreshAccessToken_defaultRetriesDisabled() throws IOException {
.setHttpTransportFactory(transportFactory)
.build()
.createWithCustomRetryStrategy(false);
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, accessToken1);
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), accessToken1);
@@ -735,6 +766,7 @@ void refreshAccessToken_maxRetries_maxDelay() throws IOException {
MockTokenServerTransport transport = transportFactory.transport;
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, ACCESS_TOKEN);
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), ACCESS_TOKEN);
@@ -764,6 +796,7 @@ void refreshAccessToken_RequestFailure_retried() throws IOException {
MockTokenServerTransport transport = transportFactory.transport;
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, ACCESS_TOKEN);
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), ACCESS_TOKEN);
@@ -795,6 +828,7 @@ void refreshAccessToken_4xx_5xx_NonRetryableFails() throws IOException {
MockTokenServerTransport transport = transportFactory.transport;
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, accessToken1);
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), accessToken1);
@@ -820,6 +854,7 @@ void idTokenWithAudience_oauthFlow_targetAudienceMatchesAudClaim() throws IOExce
MockTokenServerTransport transport = transportFactory.transport;
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, accessToken1);
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), accessToken1);
@@ -857,6 +892,7 @@ void idTokenWithAudience_oauthFlow_targetAudienceDoesNotMatchAudClaim() throws I
MockTokenServerTransport transport = transportFactory.transport;
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, accessToken1);
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), accessToken1);
@@ -889,6 +925,7 @@ void idTokenWithAudience_iamFlow_targetAudienceMatchesAudClaim() throws IOExcept
.setHttpTransportFactory(transportFactory)
.setUniverseDomain(nonGDU)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://foo.bar";
IdTokenCredentials tokenCredential =
@@ -920,6 +957,7 @@ void idTokenWithAudience_iamFlow_targetAudienceDoesNotMatchAudClaim() throws IOE
.setHttpTransportFactory(transportFactory)
.setUniverseDomain(nonGDU)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "differentAudience";
IdTokenCredentials tokenCredential =
@@ -941,6 +979,7 @@ void idTokenWithAudience_oauthEndpoint_non2XXStatusCode() throws IOException {
transportFactory.transport.setError(new IOException("404 Not Found"));
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "audience";
IdTokenCredentials tokenCredential =
@@ -969,6 +1008,7 @@ void idTokenWithAudience_iamEndpoint_non2XXStatusCode() throws IOException {
.setHttpTransportFactory(transportFactory)
.setUniverseDomain(universeDomain)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "audience";
IdTokenCredentials tokenCredential =
@@ -1370,6 +1410,7 @@ void fromStream_providesToken() throws IOException {
GoogleCredentials credentials =
ServiceAccountCredentials.fromStream(serviceAccountStream, transportFactory);
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
assertNotNull(credentials);
credentials = credentials.createScoped(SCOPES);
@@ -1412,6 +1453,7 @@ void getIdTokenWithAudience_badEmailError_issClaimTraced() throws IOException {
transport.setError(new IOException("Invalid grant: Account not found"));
ServiceAccountCredentials credentials =
createDefaultBuilder().setScopes(SCOPES).setHttpTransportFactory(transportFactory).build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
String targetAudience = "https://bar";
IdTokenCredentials tokenCredential =
@@ -1496,6 +1538,7 @@ void getRequestMetadata_setsQuotaProjectId() throws IOException {
.setQuotaProjectId("my-quota-project-id")
.setHttpTransportFactory(transportFactory)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
assertTrue(metadata.containsKey("x-goog-user-project"));
@@ -1522,6 +1565,7 @@ void getRequestMetadata_noQuotaProjectId() throws IOException {
.setProjectId(PROJECT_ID)
.setHttpTransportFactory(transportFactory)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
assertFalse(metadata.containsKey("x-goog-user-project"));
@@ -1545,6 +1589,7 @@ void getRequestMetadata_withCallback() throws IOException {
.setQuotaProjectId("my-quota-project-id")
.setHttpTransportFactory(transportFactory)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
final Map> plainMetadata = credentials.getRequestMetadata();
final AtomicBoolean success = new AtomicBoolean(false);
@@ -1585,6 +1630,7 @@ void getRequestMetadata_withScopes_withUniverseDomain_SelfSignedJwt() throws IOE
.setHttpTransportFactory(transportFactory)
.setUniverseDomain("foo.bar")
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
final Map> plainMetadata = credentials.getRequestMetadata();
final AtomicBoolean success = new AtomicBoolean(false);
@@ -1621,6 +1667,7 @@ void getRequestMetadata_withScopes_selfSignedJWT() throws IOException {
.setHttpTransportFactory(new MockTokenServerTransportFactory())
.setUseJwtAccessWithScope(true)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
assertNotNull(((ServiceAccountCredentials) credentials).getSelfSignedJwtCredentialsWithScope());
@@ -1652,6 +1699,7 @@ void refreshAccessToken_withDomainDelegation_selfSignedJWT_disabled() throws IOE
.setHttpTransportFactory(transportFactory)
.setUseJwtAccessWithScope(true)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
transport.addServiceAccount(CLIENT_EMAIL, accessToken1);
Map> metadata = credentials.getRequestMetadata(CALL_URI);
@@ -1676,6 +1724,7 @@ void getRequestMetadata_withAudience_selfSignedJWT() throws IOException {
.setProjectId(PROJECT_ID)
.setHttpTransportFactory(new MockTokenServerTransportFactory())
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(CALL_URI);
assertNull(((ServiceAccountCredentials) credentials).getSelfSignedJwtCredentialsWithScope());
@@ -1696,6 +1745,7 @@ void getRequestMetadata_withDefaultScopes_selfSignedJWT() throws IOException {
.setHttpTransportFactory(new MockTokenServerTransportFactory())
.setUseJwtAccessWithScope(true)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
Map> metadata = credentials.getRequestMetadata(null);
verifyJwtAccess(metadata, "dummy.scope");
@@ -1716,6 +1766,7 @@ void getRequestMetadataWithCallback_selfSignedJWT() throws IOException {
.setUseJwtAccessWithScope(true)
.setScopes(SCOPES)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
final AtomicBoolean success = new AtomicBoolean(false);
credentials.getRequestMetadata(
@@ -1753,6 +1804,7 @@ void createScopes_existingAccessTokenInvalidated() throws IOException {
.setHttpTransportFactory(transportFactory)
.setScopes(SCOPES)
.build();
+ credentials.regionalAccessBoundaryManager.setCachedRAB(createDummyRab(credentials.clock));
TestUtils.assertContainsBearerToken(credentials.getRequestMetadata(CALL_URI), ACCESS_TOKEN);
// Calling createScoped() again will invalidate the existing access token and calling
@@ -1773,6 +1825,96 @@ void getRequestMetadata_withMultipleScopes_selfSignedJWT() throws IOException {
verifyJwtAccess(credentials.getRequestMetadata(CALL_URI), "scope1 scope2");
}
+ @Test
+ public void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {
+
+ // Mock regional access boundary response
+ RegionalAccessBoundary regionalAccessBoundary =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS,
+ null);
+
+ MockTokenServerTransport transport = new MockTokenServerTransport();
+ transport.addServiceAccount(CLIENT_EMAIL, "test-access-token");
+ transport.setRegionalAccessBoundary(regionalAccessBoundary);
+
+ ServiceAccountCredentials credentials =
+ ServiceAccountCredentials.newBuilder()
+ .setClientEmail(CLIENT_EMAIL)
+ .setPrivateKey(
+ OAuth2Utils.privateKeyFromPkcs8(ServiceAccountCredentialsTest.PRIVATE_KEY_PKCS8))
+ .setPrivateKeyId("test-key-id")
+ .setHttpTransportFactory(() -> transport)
+ .setScopes(SCOPES)
+ .build();
+
+ // First call: initiates async refresh.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+ }
+
+ @Test
+ public void refresh_regionalAccessBoundary_selfSignedJWT()
+ throws IOException, InterruptedException {
+
+ RegionalAccessBoundary regionalAccessBoundary =
+ new RegionalAccessBoundary(
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
+ TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS,
+ null);
+
+ MockTokenServerTransport transport = new MockTokenServerTransport();
+ transport.setRegionalAccessBoundary(regionalAccessBoundary);
+
+ ServiceAccountCredentials credentials =
+ ServiceAccountCredentials.newBuilder()
+ .setClientEmail(CLIENT_EMAIL)
+ .setPrivateKey(
+ OAuth2Utils.privateKeyFromPkcs8(ServiceAccountCredentialsTest.PRIVATE_KEY_PKCS8))
+ .setPrivateKeyId("test-key-id")
+ .setHttpTransportFactory(() -> transport)
+ .setUseJwtAccessWithScope(true)
+ .setScopes(SCOPES)
+ .build();
+
+ // First call: initiates async refresh using the SSJWT as the token.
+ Map> headers = credentials.getRequestMetadata();
+ assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
+
+ waitForRegionalAccessBoundary(credentials);
+
+ // Second call: should have header.
+ headers = credentials.getRequestMetadata();
+ assertEquals(
+ headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY),
+ Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
+
+ 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)
throws IOException {
assertNotNull(metadata);
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java
index 4efc138bbfa8..52652a71c458 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java
@@ -69,4 +69,9 @@ static void validateMetricsHeader(
}
assertEquals(expectedMetricsValue, actualMetricsValue);
}
+
+ static RegionalAccessBoundary createDummyRab(com.google.api.client.util.Clock clock) {
+ return new RegionalAccessBoundary(
+ "dummy-locations", java.util.Arrays.asList("dummy-loc"), clock);
+ }
}
diff --git a/google-auth-library-java/samples/snippets/pom.xml b/google-auth-library-java/samples/snippets/pom.xml
index 5b721797222a..941191a80ee0 100644
--- a/google-auth-library-java/samples/snippets/pom.xml
+++ b/google-auth-library-java/samples/snippets/pom.xml
@@ -80,4 +80,3 @@
-
diff --git a/java-dataplex/grpc-google-cloud-dataplex-v1/src/main/java/com/google/cloud/dataplex/v1/ContentServiceGrpc.java b/java-dataplex/grpc-google-cloud-dataplex-v1/src/main/java/com/google/cloud/dataplex/v1/ContentServiceGrpc.java
index a087ae2f0103..7d657f076cf9 100644
--- a/java-dataplex/grpc-google-cloud-dataplex-v1/src/main/java/com/google/cloud/dataplex/v1/ContentServiceGrpc.java
+++ b/java-dataplex/grpc-google-cloud-dataplex-v1/src/main/java/com/google/cloud/dataplex/v1/ContentServiceGrpc.java
@@ -15,7 +15,6 @@
*/
package com.google.cloud.dataplex.v1;
-
/**
*
*