diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java
index fef033350a7a..d5a992189167 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java
@@ -31,6 +31,7 @@
package com.google.auth.mtls;
+import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.core.InternalApi;
import com.google.auth.http.HttpTransportFactory;
@@ -62,7 +63,7 @@ public MtlsHttpTransportFactory(KeyStore mtlsKeyStore) {
}
@Override
- public NetHttpTransport create() {
+ public HttpTransport create() {
try {
// Build the mTLS transport using the provided KeyStore.
return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build();
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java
index 0d34cf271986..c8cb0083a83a 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java
@@ -31,14 +31,19 @@
package com.google.auth.mtls;
import com.google.api.core.InternalApi;
+import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.EnvironmentProvider;
+import com.google.auth.oauth2.OAuth2Utils;
import com.google.auth.oauth2.PropertyProvider;
import com.google.common.base.Strings;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.security.KeyStore;
import java.util.Locale;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
/**
* Utility class for mTLS related operations.
@@ -47,10 +52,27 @@
*/
@InternalApi
public class MtlsUtils {
+ private static final Logger LOGGER = Logger.getLogger(MtlsUtils.class.getName());
+
static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG";
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
+ /**
+ * The policy determining when to use mutual TLS (mTLS) endpoints.
+ *
+ *
See AIP-4114 for the specification on mTLS
+ * endpoint usage.
+ */
+ public enum MtlsEndpointUsagePolicy {
+ /** Always use the mTLS endpoint, and fail if client certificates are not configured. */
+ ALWAYS,
+ /** Never use the mTLS endpoint. */
+ NEVER,
+ /** Use the mTLS endpoint if client certificates are configured (auto-discovery). */
+ AUTO
+ }
+
private MtlsUtils() {
// Prevent instantiation for Utility class
}
@@ -76,39 +98,36 @@ public static String getCertificatePath(
}
/**
- * Resolves and loads the workload certificate configuration.
+ * Resolves and parses the workload certificate configuration.
*
- *
The configuration file is resolved in the following order of precedence: 1. The provided
- * certConfigPathOverride (if not null). 2. The path specified by the
- * GOOGLE_API_CERTIFICATE_CONFIG environment variable. 3. The well-known certificate configuration
- * file in the gcloud config directory.
+ *
This locates the certificate configuration file via {@link #resolveCertificateConfigFile}
+ * and parses its contents into a {@link WorkloadCertificateConfiguration}.
*
* @param envProvider the environment provider to use for resolving environment variables
* @param propProvider the property provider to use for resolving system properties
* @param certConfigPathOverride optional override path for the configuration file
* @return the loaded WorkloadCertificateConfiguration
- * @throws IOException if the configuration file cannot be found, read, or parsed
+ * @throws IOException if the configuration file cannot be resolved, read, or parsed
*/
static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration(
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
throws IOException {
- File certConfig;
- if (certConfigPathOverride != null) {
- certConfig = new File(certConfigPathOverride);
- } else {
- String envCredentialsPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
- if (!Strings.isNullOrEmpty(envCredentialsPath)) {
- certConfig = new File(envCredentialsPath);
- } else {
- certConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
+ File certConfig =
+ resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride);
+ if (certConfig == null) {
+ try {
+ File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
+ throw new CertificateSourceUnavailableException(
+ "Certificate configuration file does not exist or is not a file: "
+ + wellKnownConfig.getAbsolutePath());
+ } catch (IOException e) {
+ if (e instanceof CertificateSourceUnavailableException) {
+ throw (CertificateSourceUnavailableException) e;
+ }
+ throw new CertificateSourceUnavailableException(
+ "Failed to get well-known certificate config file path", e);
}
}
-
- if (!certConfig.isFile()) {
- throw new CertificateSourceUnavailableException(
- "Certificate configuration file does not exist or is not a file: "
- + certConfig.getAbsolutePath());
- }
try (InputStream certConfigStream = new FileInputStream(certConfig)) {
return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream);
}
@@ -137,4 +156,195 @@ private static File getWellKnownCertificateConfigFile(
}
return new File(cloudConfigPath, WELL_KNOWN_CERTIFICATE_CONFIG_FILE);
}
+
+ /**
+ * Centralized helper method to determine if mutual TLS (mTLS) can be enabled.
+ *
+ * @param envProvider the environment provider to use for resolving environment variables
+ * @param propProvider the property provider to use for resolving system properties
+ * @param certConfigPathOverride optional override path for the configuration file
+ * @return true if mTLS should be enabled, false otherwise
+ * @throws IOException if the configuration file is present but contains missing or malformed
+ * files
+ */
+ public static boolean canBeEnabled(
+ EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
+ throws IOException {
+
+ // Check if client certificate usage is allowed
+ String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
+ if ("false".equalsIgnoreCase(useClientCertificate)) {
+ return false;
+ }
+
+ MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider);
+ if (policy == MtlsEndpointUsagePolicy.NEVER) {
+ return false;
+ }
+
+ File certConfigFile =
+ resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride);
+ return certConfigFile != null;
+ }
+
+ /**
+ * Returns whether the mutual TLS (mTLS) endpoint should be used.
+ *
+ * @param envProvider the environment provider to use for resolving environment variables
+ * @param propProvider the property provider to use for resolving system properties
+ * @param certConfigPathOverride optional override path for the configuration file
+ * @return true if the mTLS endpoint should be used, false otherwise
+ */
+ public static boolean shouldMtlsEndpointBeUsed(
+ EnvironmentProvider envProvider,
+ PropertyProvider propProvider,
+ String certConfigPathOverride) {
+ MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider);
+ if (policy == MtlsEndpointUsagePolicy.ALWAYS) {
+ return true;
+ }
+ if (policy == MtlsEndpointUsagePolicy.NEVER) {
+ return false;
+ }
+ // policy is AUTO: use mTLS endpoint if client certificate can be enabled
+ try {
+ return canBeEnabled(envProvider, propProvider, certConfigPathOverride);
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Resolves the mutual TLS (mTLS) certificate configuration file.
+ *
+ *
The configuration file is resolved in the following order of precedence:
+ *
+ *
+ * - The developer-provided {@code certConfigPathOverride} (if not null).
+ *
- The path specified by the {@code GOOGLE_API_CERTIFICATE_CONFIG} environment variable.
+ *
- The well-known automatic gcloud workload identity provisioning location (via {@link
+ * #getWellKnownCertificateConfigFile}).
+ *
+ *
+ * If an explicit configuration file is specified (via override or environment variable) and it
+ * is missing or invalid, an exception is thrown. If no explicit file is specified and the default
+ * well-known file is missing, {@code null} is returned.
+ *
+ * @param envProvider the environment provider to use for resolving environment variables
+ * @param propProvider the property provider to use for resolving system properties
+ * @param certConfigPathOverride optional override path for the configuration file
+ * @return the resolved File object, or null if no configuration was found
+ * @throws IOException if an explicit configuration file is missing or malformed
+ */
+ @Nullable
+ static File resolveCertificateConfigFile(
+ EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
+ throws IOException {
+ // 1. Check explicit developer override
+ if (certConfigPathOverride != null) {
+ File certConfigFile = new File(certConfigPathOverride);
+ if (!certConfigFile.isFile()) {
+ throw new CertificateSourceUnavailableException(
+ "Certificate configuration file does not exist or is not a file: "
+ + certConfigFile.getAbsolutePath());
+ }
+ return certConfigFile;
+ }
+
+ // 2. Check explicit environment variable
+ String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
+ if (!Strings.isNullOrEmpty(envPath)) {
+ File certConfigFile = new File(envPath);
+ if (!certConfigFile.isFile()) {
+ throw new CertificateSourceUnavailableException(
+ "Certificate configuration file does not exist or is not a file: "
+ + certConfigFile.getAbsolutePath());
+ }
+ return certConfigFile;
+ }
+
+ // 3. Check optional well-known automatic provisioning location
+ try {
+ File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
+ if (wellKnownConfig.isFile()) {
+ return wellKnownConfig;
+ }
+ } catch (IOException e) {
+ LOGGER.info(
+ "Could not get the mutual TLS (mTLS) client certificate configuration. The library will fall back to making standard non-mTLS requests.");
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the current mutual TLS endpoint usage policy.
+ *
+ * @param envProvider the environment provider to use for resolving environment variables
+ * @return the MtlsEndpointUsagePolicy enum value
+ */
+ public static MtlsEndpointUsagePolicy getMtlsEndpointUsagePolicy(
+ EnvironmentProvider envProvider) {
+ String mtlsEndpointUsagePolicy = envProvider.getEnv("GOOGLE_API_USE_MTLS_ENDPOINT");
+ if ("never".equalsIgnoreCase(mtlsEndpointUsagePolicy)) {
+ return MtlsEndpointUsagePolicy.NEVER;
+ } else if ("always".equalsIgnoreCase(mtlsEndpointUsagePolicy)) {
+ return MtlsEndpointUsagePolicy.ALWAYS;
+ }
+ return MtlsEndpointUsagePolicy.AUTO;
+ }
+
+ /**
+ * Prepares and upgrades the HTTP transport factory for mutual TLS (mTLS) if applicable.
+ *
+ * @param baseTransportFactory the base HTTP transport factory to upgrade
+ * @param envProvider the environment provider to use for resolving environment variables
+ * @param propProvider the property provider to use for resolving system properties
+ * @param certConfigPathOverride optional override path for the configuration file
+ * @return the mTLS-configured HTTP transport factory, or the base factory if mTLS is not enabled
+ * @throws IOException if mTLS is required/enabled but certificate initialization fails or an
+ * incompatible transport factory was provided
+ */
+ public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled(
+ HttpTransportFactory baseTransportFactory,
+ EnvironmentProvider envProvider,
+ PropertyProvider propProvider,
+ String certConfigPathOverride)
+ throws IOException {
+
+ if (baseTransportFactory == null) {
+ return null;
+ }
+
+ if (baseTransportFactory instanceof MtlsHttpTransportFactory) {
+ // A custom MtlsHttpTransportFactory was already pre-configured by the user.
+ // Keep using it as-is without re-initializing.
+ return baseTransportFactory;
+ }
+
+ if (!canBeEnabled(envProvider, propProvider, certConfigPathOverride)) {
+ return baseTransportFactory;
+ }
+
+ if (baseTransportFactory != OAuth2Utils.HTTP_TRANSPORT_FACTORY) {
+ // A user configured HttpTransportFactory was explicitly injected.
+ // Trust the developer's custom factory and return it as-is.
+ return baseTransportFactory;
+ }
+
+ try {
+ // This is the default HttpTransportFactory assigned by credentials.
+ // Automatically discover and load client certificates to construct an mTLS factory.
+ X509Provider x509Provider =
+ new X509Provider(envProvider, propProvider, certConfigPathOverride);
+ KeyStore mtlsKeyStore = x509Provider.getKeyStore();
+ return new MtlsHttpTransportFactory(mtlsKeyStore);
+ } catch (Exception e) {
+ LOGGER.warning(
+ "mTLS transport factory initialization failed, falling back to non-mTLS transport: "
+ + e.getMessage());
+ // Graceful fallback to standard transport if mTLS initialization fails
+ return baseTransportFactory;
+ }
+ }
}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java
index c87398940538..8e4d6969a1e1 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java
@@ -100,7 +100,7 @@ public X509Provider() {
*
*
* - The certificate config override path, if set.
- *
- The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable
+ *
- The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable.
*
- The well known gcloud location for the certificate configuration file.
*
*
@@ -111,33 +111,27 @@ public X509Provider() {
*/
@Override
public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
- WorkloadCertificateConfiguration workloadCertConfig =
- MtlsUtils.getWorkloadCertificateConfiguration(
- envProvider, propProvider, certConfigPathOverride);
-
- // Read the certificate and private key file paths into streams.
- try (InputStream certStream = new FileInputStream(new File(workloadCertConfig.getCertPath()));
- InputStream privateKeyStream =
- new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
- SequenceInputStream certAndPrivateKeyStream =
- new SequenceInputStream(certStream, privateKeyStream)) {
+ try {
+ // Attempt to load from resolved Config File
+ WorkloadCertificateConfiguration workloadCertConfig =
+ MtlsUtils.getWorkloadCertificateConfiguration(
+ envProvider, propProvider, certConfigPathOverride);
- // Build a key store using the combined stream.
- return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
+ try (InputStream certStream =
+ new FileInputStream(new File(workloadCertConfig.getCertPath()));
+ InputStream privateKeyStream =
+ new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
+ SequenceInputStream certAndPrivateKeyStream =
+ new SequenceInputStream(certStream, privateKeyStream)) {
+ return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
+ }
} catch (CertificateSourceUnavailableException e) {
- // Throw the CertificateSourceUnavailableException without wrapping.
throw e;
} catch (Exception e) {
- // Wrap all other exception types to an IOException.
- throw new IOException("X509Provider: Unexpected IOException:", e);
+ throw new IOException("X509Provider: Unexpected error loading from config file:", e);
}
}
- /**
- * Returns true if the X509 mTLS provider is available.
- *
- * @throws IOException if a general I/O error occurs while determining availability.
- */
@Override
public boolean isAvailable() throws IOException {
try {
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 e564c32c97c6..f69762d066a6 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
@@ -56,8 +56,10 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.Executor;
import javax.annotation.Nullable;
/** Base type for credentials for authorizing calls to Google APIs using OAuth2. */
@@ -379,7 +381,7 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT
// Skip refresh for regional endpoints.
if (uri != null && uri.getHost() != null) {
- String host = uri.getHost().toLowerCase(java.util.Locale.US);
+ String host = uri.getHost().toLowerCase(Locale.US);
if (host.endsWith(".rep.googleapis.com") || host.endsWith(".rep.sandbox.googleapis.com")) {
return;
}
@@ -388,7 +390,7 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT
// We need a valid access token for the refresh.
if (token == null
|| (token.getExpirationTimeMillis() != null
- && token.getExpirationTimeMillis() < clock.currentTimeMillis())) {
+ && token.getExpirationTimeMillis() <= clock.currentTimeMillis() + 180_000L)) {
return;
}
@@ -398,7 +400,11 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT
}
regionalAccessBoundaryManager.triggerAsyncRefresh(
- transportFactory, (RegionalAccessBoundaryProvider) this, token);
+ transportFactory,
+ (RegionalAccessBoundaryProvider) this,
+ token,
+ getEnvironmentProvider(),
+ getPropertyProvider());
}
/**
@@ -462,9 +468,7 @@ public Map> getRequestMetadata(URI uri) throws IOException
*/
@Override
public void getRequestMetadata(
- final URI uri,
- final java.util.concurrent.Executor executor,
- final RequestMetadataCallback callback) {
+ final URI uri, final Executor executor, final RequestMetadataCallback callback) {
super.getRequestMetadata(
uri,
executor,
@@ -552,7 +556,7 @@ Map> addRegionalAccessBoundaryToRequestMetadata(
Preconditions.checkNotNull(requestMetadata);
if (uri != null && uri.getHost() != null) {
- String host = uri.getHost().toLowerCase(java.util.Locale.US);
+ String host = uri.getHost().toLowerCase(Locale.US);
if (host.endsWith(".rep.googleapis.com") || host.endsWith(".rep.sandbox.googleapis.com")) {
return requestMetadata;
}
@@ -843,6 +847,14 @@ HttpTransportFactory getTransportFactory() {
return null;
}
+ EnvironmentProvider getEnvironmentProvider() {
+ return SystemEnvironmentProvider.getInstance();
+ }
+
+ PropertyProvider getPropertyProvider() {
+ return SystemPropertyProvider.getInstance();
+ }
+
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/RegionalAccessBoundary.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java
index 444b35ef0328..678183192047 100644
--- 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
@@ -185,8 +185,8 @@ static RegionalAccessBoundary refresh(
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.");
+ && accessToken.getExpirationTimeMillis() <= clock.currentTimeMillis() + 180_000L) {
+ throw new IOException("The provided access token is expired or expiring within skew buffer.");
}
HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java
index ee5d9511d755..db64deaf8d56 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java
@@ -36,7 +36,9 @@
import com.google.api.client.util.Clock;
import com.google.api.core.InternalApi;
import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.mtls.MtlsUtils;
import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
@@ -68,6 +70,9 @@ final class RegionalAccessBoundaryManager {
*/
static final int DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS = 60000;
+ static final String IAM_ENDPOINT = "iamcredentials.googleapis.com";
+ static final String MTLS_IAM_ENDPOINT = "iamcredentials.mtls.googleapis.com";
+
/**
* cachedRAB uses AtomicReference to provide thread-safe, lock-free access to the cached data for
* high-concurrency request threads.
@@ -177,7 +182,9 @@ void setCachedRAB(RegionalAccessBoundary rab) {
void triggerAsyncRefresh(
final HttpTransportFactory transportFactory,
final RegionalAccessBoundaryProvider provider,
- final AccessToken accessToken) {
+ final AccessToken accessToken,
+ final EnvironmentProvider envProvider,
+ final PropertyProvider propProvider) {
if (skipRAB.get() || isCooldownActive()) {
return;
}
@@ -191,6 +198,11 @@ void triggerAsyncRefresh(
// 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)) {
+ currentRab = cachedRAB.get();
+ if (currentRab != null && !currentRab.shouldRefresh()) {
+ isRefreshing.set(false);
+ return;
+ }
Runnable refreshTask =
() -> {
try {
@@ -199,9 +211,15 @@ void triggerAsyncRefresh(
skipRAB.set(true);
return;
}
+ HttpTransportFactory upgradedTransportFactory =
+ MtlsUtils.prepareTransportFactoryIfMtlsEnabled(
+ transportFactory, envProvider, propProvider, null);
+ if (MtlsUtils.shouldMtlsEndpointBeUsed(envProvider, propProvider, null)) {
+ url = url.replace(IAM_ENDPOINT, MTLS_IAM_ENDPOINT);
+ }
RegionalAccessBoundary newRAB =
RegionalAccessBoundary.refresh(
- transportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis);
+ upgradedTransportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis);
cachedRAB.set(newRAB);
resetCooldown();
} catch (Throwable e) {
@@ -217,6 +235,8 @@ void triggerAsyncRefresh(
} 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.
+ handleRefreshFailure(
+ new IOException("Failed to submit background refresh task: " + e.getMessage(), e));
log(
LOGGER_PROVIDER,
Level.FINE,
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java
index f3fdf05a4c32..34e6891a26df 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java
@@ -33,7 +33,9 @@
import static org.junit.jupiter.api.Assertions.*;
+import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.EnvironmentProvider;
+import com.google.auth.oauth2.OAuth2Utils;
import com.google.auth.oauth2.PropertyProvider;
import java.io.File;
import java.io.IOException;
@@ -243,4 +245,353 @@ public String getProperty(String name, String def) {
assertEquals("APPDATA environment variable is not set on Windows.", exception.getMessage());
}
+
+ // If client certificate usage is explicitly disabled, canBeEnabled should return false.
+ @Test
+ void canBeEnabled_allowanceExplicitFalse_returnsFalse() throws IOException {
+ EnvironmentProvider envProvider =
+ new EnvironmentProvider() {
+ @Override
+ public String getEnv(String name) {
+ if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) {
+ return "false";
+ }
+ return null;
+ }
+ };
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null));
+ }
+
+ // If client certificate usage is explicitly enabled and a valid configuration is present,
+ // canBeEnabled should return true.
+ @Test
+ void canBeEnabled_allowanceExplicitTrue_withConfig_returnsTrue() throws IOException {
+ EnvironmentProvider envProvider =
+ new EnvironmentProvider() {
+ @Override
+ public String getEnv(String name) {
+ if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) {
+ return "true";
+ }
+ if ("GOOGLE_API_CERTIFICATE_CONFIG".equals(name)) {
+ return "testresources/mtls/certificate_config.json";
+ }
+ return null;
+ }
+ };
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertTrue(MtlsUtils.canBeEnabled(envProvider, propProvider, null));
+ }
+
+ // If client certificate usage is unset but a valid configuration is present, mTLS should be
+ // enabled by default (returns true).
+ @Test
+ void canBeEnabled_allowanceUnset_withConfig_returnsTrue() throws IOException {
+ EnvironmentProvider envProvider =
+ new EnvironmentProvider() {
+ @Override
+ public String getEnv(String name) {
+ if ("GOOGLE_API_CERTIFICATE_CONFIG".equals(name)) {
+ return "testresources/mtls/certificate_config.json";
+ }
+ return null;
+ }
+ };
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertTrue(MtlsUtils.canBeEnabled(envProvider, propProvider, null));
+ }
+
+ // If the GOOGLE_API_CERTIFICATE_CONFIG environment variable points to a non-existent file,
+ // canBeEnabled should throw an IOException.
+ @Test
+ void canBeEnabled_envVarConfigMissingFile_throwsIOException() throws IOException {
+ Path nonExistentConfig = tempDir.resolve("non_existent.json");
+ EnvironmentProvider envProvider =
+ new EnvironmentProvider() {
+ @Override
+ public String getEnv(String name) {
+ if ("GOOGLE_API_CERTIFICATE_CONFIG".equals(name)) {
+ return nonExistentConfig.toString();
+ }
+ return null;
+ }
+ };
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertThrows(IOException.class, () -> MtlsUtils.canBeEnabled(envProvider, propProvider, null));
+ }
+
+ // If the well-known gcloud certificate configuration file exists, canBeEnabled should return
+ // true.
+ @Test
+ void canBeEnabled_wellKnownConfigExists_returnsTrue() throws IOException {
+ Path gcloudDir = tempDir.resolve(".config/gcloud");
+ Files.createDirectories(gcloudDir);
+ Path configFile = gcloudDir.resolve("certificate_config.json");
+ Path certFile = tempDir.resolve("cert.pem");
+ Path keyFile = tempDir.resolve("key.pem");
+ Files.createFile(certFile);
+ Files.createFile(keyFile);
+ Files.write(configFile, createJsonConfigString(certFile, keyFile).getBytes());
+
+ EnvironmentProvider envProvider = name -> null;
+ PropertyProvider propProvider =
+ new PropertyProvider() {
+ @Override
+ public String getProperty(String name, String def) {
+ if ("os.name".equals(name)) return "Linux";
+ if ("user.home".equals(name)) return tempDir.toString();
+ return def;
+ }
+ };
+
+ assertTrue(MtlsUtils.canBeEnabled(envProvider, propProvider, null));
+ }
+
+ @Test
+ void canBeEnabled_alwaysPolicy_clientCertDisabled_returnsFalse() throws IOException {
+ EnvironmentProvider envProvider =
+ name -> {
+ if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) {
+ return "false";
+ }
+ if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) {
+ return "always";
+ }
+ return null;
+ };
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null));
+ assertTrue(MtlsUtils.shouldMtlsEndpointBeUsed(envProvider, propProvider, null));
+ }
+
+ @Test
+ void getMtlsEndpointUsagePolicy_never() {
+ EnvironmentProvider envProvider =
+ name -> "GOOGLE_API_USE_MTLS_ENDPOINT".equals(name) ? "never" : null;
+ assertEquals(
+ MtlsUtils.MtlsEndpointUsagePolicy.NEVER, MtlsUtils.getMtlsEndpointUsagePolicy(envProvider));
+ }
+
+ @Test
+ void getMtlsEndpointUsagePolicy_always() {
+ EnvironmentProvider envProvider =
+ name -> "GOOGLE_API_USE_MTLS_ENDPOINT".equals(name) ? "always" : null;
+ assertEquals(
+ MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS,
+ MtlsUtils.getMtlsEndpointUsagePolicy(envProvider));
+ }
+
+ @Test
+ void getMtlsEndpointUsagePolicy_auto() {
+ EnvironmentProvider envProvider = name -> null;
+ assertEquals(
+ MtlsUtils.MtlsEndpointUsagePolicy.AUTO, MtlsUtils.getMtlsEndpointUsagePolicy(envProvider));
+ }
+
+ @Test
+ void canBeEnabled_policyNever_returnsFalse() throws IOException {
+ EnvironmentProvider envProvider =
+ new EnvironmentProvider() {
+ @Override
+ public String getEnv(String name) {
+ if ("GOOGLE_API_CERTIFICATE_CONFIG".equals(name)) {
+ return "testresources/mtls/certificate_config.json";
+ }
+ if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) {
+ return "never";
+ }
+ return null;
+ }
+ };
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null));
+ }
+
+ @Test
+ void canBeEnabled_autoPolicy_noConfig_returnsFalse() throws IOException {
+ EnvironmentProvider envProvider = name -> null;
+ PropertyProvider propProvider =
+ new PropertyProvider() {
+ @Override
+ public String getProperty(String name, String def) {
+ if ("user.home".equals(name)) return tempDir.toString();
+ return def;
+ }
+ };
+
+ assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null));
+ }
+
+ @Test
+ void canBeEnabled_alwaysPolicy_returnsFalse() throws IOException {
+ EnvironmentProvider envProvider =
+ name -> "GOOGLE_API_USE_MTLS_ENDPOINT".equals(name) ? "always" : null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null));
+ assertTrue(MtlsUtils.shouldMtlsEndpointBeUsed(envProvider, propProvider, null));
+ }
+
+ @Test
+ void getWorkloadCertificateConfiguration_malformedJson_throwsException() throws IOException {
+ Path configFile = tempDir.resolve("malformed.json");
+ Files.write(configFile, "{invalid-json}".getBytes());
+
+ EnvironmentProvider envProvider = name -> null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertThrows(
+ Exception.class,
+ () ->
+ MtlsUtils.getWorkloadCertificateConfiguration(
+ envProvider, propProvider, configFile.toString()));
+ }
+
+ @Test
+ void prepareTransportFactoryIfMtlsEnabled_nullInput_returnsNull() throws IOException {
+ assertNull(
+ MtlsUtils.prepareTransportFactoryIfMtlsEnabled(
+ null, name -> null, (name, def) -> def, null));
+ }
+
+ @Test
+ void prepareTransportFactoryIfMtlsEnabled_mtlsFactory_returnsAsIs()
+ throws java.security.GeneralSecurityException, IOException {
+ java.security.KeyStore dummyKeyStore =
+ java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType());
+ dummyKeyStore.load(null, null);
+ MtlsHttpTransportFactory mtlsFactory = new MtlsHttpTransportFactory(dummyKeyStore);
+
+ HttpTransportFactory result =
+ MtlsUtils.prepareTransportFactoryIfMtlsEnabled(
+ mtlsFactory, name -> null, (name, def) -> def, null);
+
+ assertSame(mtlsFactory, result);
+ }
+
+ @Test
+ void prepareTransportFactoryIfMtlsEnabled_customFactory_mtlsAlways_returnsAsIs()
+ throws IOException {
+ HttpTransportFactory customFactory = () -> null;
+ EnvironmentProvider envProvider =
+ name -> "GOOGLE_API_USE_MTLS_ENDPOINT".equals(name) ? "always" : null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ HttpTransportFactory result =
+ MtlsUtils.prepareTransportFactoryIfMtlsEnabled(
+ customFactory, envProvider, propProvider, null);
+
+ assertSame(customFactory, result);
+ }
+
+ @Test
+ void prepareTransportFactoryIfMtlsEnabled_customFactory_mtlsAuto_withConfig_returnsAsIs()
+ throws IOException {
+ HttpTransportFactory customFactory = () -> null;
+ EnvironmentProvider envProvider =
+ name ->
+ "GOOGLE_API_CERTIFICATE_CONFIG".equals(name)
+ ? "testresources/mtls/certificate_config.json"
+ : null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ HttpTransportFactory result =
+ MtlsUtils.prepareTransportFactoryIfMtlsEnabled(
+ customFactory, envProvider, propProvider, null);
+
+ assertSame(customFactory, result);
+ }
+
+ @Test
+ void prepareTransportFactoryIfMtlsEnabled_defaultFactory_mtlsAlways_upgradesToMtlsFactory()
+ throws IOException {
+ EnvironmentProvider envProvider =
+ new EnvironmentProvider() {
+ @Override
+ public String getEnv(String name) {
+ if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) {
+ return "true";
+ }
+ if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) {
+ return "always";
+ }
+ if ("GOOGLE_API_CERTIFICATE_CONFIG".equals(name)) {
+ return "testresources/mtls/certificate_config.json";
+ }
+ return null;
+ }
+ };
+ PropertyProvider propProvider = (name, def) -> def;
+
+ HttpTransportFactory result =
+ MtlsUtils.prepareTransportFactoryIfMtlsEnabled(
+ OAuth2Utils.HTTP_TRANSPORT_FACTORY, envProvider, propProvider, null);
+
+ assertTrue(result instanceof MtlsHttpTransportFactory);
+ }
+
+ @Test
+ void
+ prepareTransportFactoryIfMtlsEnabled_defaultFactory_mtlsAlways_clientCertDisabled_returnsAsIs()
+ throws IOException {
+ EnvironmentProvider envProvider =
+ name -> {
+ if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) {
+ return "false";
+ }
+ if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) {
+ return "always";
+ }
+ return null;
+ };
+ PropertyProvider propProvider = (name, def) -> def;
+
+ HttpTransportFactory result =
+ MtlsUtils.prepareTransportFactoryIfMtlsEnabled(
+ OAuth2Utils.HTTP_TRANSPORT_FACTORY, envProvider, propProvider, null);
+
+ assertSame(OAuth2Utils.HTTP_TRANSPORT_FACTORY, result);
+ }
+
+ @Test
+ void prepareTransportFactoryIfMtlsEnabled_defaultFactory_mtlsAlways_missingConfig_returnsAsIs()
+ throws IOException {
+ EnvironmentProvider envProvider =
+ name -> "GOOGLE_API_USE_MTLS_ENDPOINT".equals(name) ? "always" : null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ HttpTransportFactory result =
+ MtlsUtils.prepareTransportFactoryIfMtlsEnabled(
+ OAuth2Utils.HTTP_TRANSPORT_FACTORY, envProvider, propProvider, null);
+
+ assertSame(OAuth2Utils.HTTP_TRANSPORT_FACTORY, result);
+ }
+
+ @Test
+ void prepareTransportFactoryIfMtlsEnabled_defaultFactory_mtlsAuto_noConfig_returnsAsIs()
+ throws IOException {
+ EnvironmentProvider envProvider = name -> null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ HttpTransportFactory result =
+ MtlsUtils.prepareTransportFactoryIfMtlsEnabled(
+ OAuth2Utils.HTTP_TRANSPORT_FACTORY, envProvider, propProvider, null);
+
+ assertSame(OAuth2Utils.HTTP_TRANSPORT_FACTORY, result);
+ }
+
+ private String createJsonConfigString(Path certPath, Path keyPath) {
+ return "{\"cert_configs\":{\"workload\":{\"cert_path\":\""
+ + certPath.toString().replace("\\", "\\\\")
+ + "\",\"key_path\":\""
+ + keyPath.toString().replace("\\", "\\\\")
+ + "\"}}}";
+ }
}
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/X509ProviderTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/X509ProviderTest.java
index 5ddd1a169d29..6a2693a62273 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/X509ProviderTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/X509ProviderTest.java
@@ -32,6 +32,7 @@
package com.google.auth.mtls;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -50,9 +51,12 @@
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
class X509ProviderTest {
+ @TempDir Path tempDir;
+
private static final String TEST_CERT_PATH = "testresources/mtls/test_cert.pem";
private static final String TEST_CONFIG_PATH = "testresources/mtls/certificate_config.json";
@@ -70,15 +74,16 @@ void x509Provider_fileDoesntExist_throws() {
@Test
void x509Provider_emptyFile_throws() throws IOException {
- Path emptyConfig = Files.createTempFile("emptyConfig", ".txt");
- emptyConfig.toFile().deleteOnExit();
+ Path emptyConfig = tempDir.resolve("emptyConfig.txt");
+ Files.createFile(emptyConfig);
X509Provider testProvider = new X509Provider(emptyConfig.toString());
String expectedErrorMessage = "no JSON input found";
- IllegalArgumentException exception =
- assertThrows(IllegalArgumentException.class, testProvider::getKeyStore);
- assertTrue(exception.getMessage().contains(expectedErrorMessage));
+ IOException exception = assertThrows(IOException.class, testProvider::getKeyStore);
+ assertNotNull(exception.getCause());
+ assertTrue(exception.getCause() instanceof IllegalArgumentException);
+ assertTrue(exception.getCause().getMessage().contains(expectedErrorMessage));
}
@Test
@@ -142,8 +147,8 @@ void x509Provider_succeeds_withWellKnownPath()
@Test
void x509Provider_succeeds_withWindowsPath()
throws IOException, KeyStoreException, CertificateException {
- Path windowsTempDir = Files.createTempDirectory("windowsTempDir");
- windowsTempDir.toFile().deleteOnExit();
+ Path windowsTempDir = tempDir.resolve("windowsTempDir");
+ Files.createDirectory(windowsTempDir);
Path gcloudDir = windowsTempDir.resolve("gcloud");
Files.createDirectory(gcloudDir);
Path configPath = gcloudDir.resolve("certificate_config.json");
@@ -172,9 +177,8 @@ void x509Provider_succeeds_withWindowsPath()
@Test
void x509Provider_certFileDoesntExist_throws() throws IOException {
- Path tempConfig = Files.createTempFile("config", ".json");
- tempConfig.toFile().deleteOnExit();
- Path nonExistentCert = tempConfig.getParent().resolve("non_existent_cert.pem");
+ Path tempConfig = tempDir.resolve("config_no_cert.json");
+ Path nonExistentCert = tempDir.resolve("non_existent_cert.pem");
Files.write(
tempConfig,
@@ -190,10 +194,8 @@ void x509Provider_certFileDoesntExist_throws() throws IOException {
@Test
void x509Provider_malformedCert_throws() throws IOException {
- Path tempConfig = Files.createTempFile("config", ".json");
- tempConfig.toFile().deleteOnExit();
- Path malformedCert = Files.createTempFile("badcert", ".pem");
- malformedCert.toFile().deleteOnExit();
+ Path tempConfig = tempDir.resolve("config_malformed_cert.json");
+ Path malformedCert = tempDir.resolve("badcert.pem");
Files.write(malformedCert, "This is not a valid certificate".getBytes());
@@ -208,4 +210,75 @@ void x509Provider_malformedCert_throws() throws IOException {
assertThrows(Exception.class, testProvider::getKeyStore);
}
+
+ @Test
+ void x509Provider_missingCertConfigs_throws() throws IOException {
+ Path tempConfig = tempDir.resolve("config_missing_configs.json");
+
+ Files.write(tempConfig, "{}".getBytes());
+
+ X509Provider testProvider = new X509Provider(tempConfig.toString());
+
+ IOException exception = assertThrows(IOException.class, testProvider::getKeyStore);
+ assertNotNull(exception.getCause());
+ assertTrue(exception.getCause() instanceof IllegalArgumentException);
+ assertTrue(
+ exception.getCause().getMessage().contains("The cert_configs object must be provided"));
+ }
+
+ @Test
+ void x509Provider_missingCertPath_throws() throws IOException {
+ Path tempConfig = tempDir.resolve("config_missing_path.json");
+
+ Files.write(
+ tempConfig, "{\"cert_configs\":{\"workload\":{\"key_path\":\"key.pem\"}}}".getBytes());
+
+ X509Provider testProvider = new X509Provider(tempConfig.toString());
+
+ IOException exception = assertThrows(IOException.class, testProvider::getKeyStore);
+ assertNotNull(exception.getCause());
+ assertTrue(exception.getCause() instanceof IllegalArgumentException);
+ assertTrue(exception.getCause().getMessage().contains("The cert_path field must be provided"));
+ }
+
+ // Failure Path: mTLS disabled (allowance = false) throws CertificateSourceUnavailableException
+ @Test
+ void x509Provider_allowanceDisabled_throws() throws Exception {
+ X509Provider provider =
+ new X509Provider(
+ name -> "GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name) ? "false" : null,
+ (name, def) -> def,
+ null);
+ assertThrows(CertificateSourceUnavailableException.class, provider::getKeyStore);
+ }
+
+ @Test
+ void x509Provider_isAvailable_succeeds() throws IOException {
+ X509Provider testProvider = new X509Provider(TEST_CONFIG_PATH);
+ assertTrue(testProvider.isAvailable());
+ }
+
+ @Test
+ void x509Provider_isAvailable_missingConfig_returnsFalse() throws IOException {
+ X509Provider testProvider = new X509Provider("badfile.json");
+ assertFalse(testProvider.isAvailable());
+ }
+
+ @Test
+ void x509Provider_isAvailable_unaffectedByMtlsFlags() throws IOException {
+ X509Provider provider =
+ new X509Provider(
+ name -> {
+ if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) {
+ return "false";
+ }
+ if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) {
+ return "never";
+ }
+ return null;
+ },
+ (name, def) -> def,
+ TEST_CONFIG_PATH);
+ assertTrue(provider.isAvailable());
+ }
}
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 26fe9151955b..9274d4d8d6ac 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
@@ -33,6 +33,7 @@
import static com.google.auth.Credentials.GOOGLE_DEFAULT_UNIVERSE;
import static com.google.auth.oauth2.TestUtils.createDummyRab;
+import static com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -57,7 +58,6 @@
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}. */
@@ -1435,16 +1435,4 @@ public void testRefresh_regionalAccessBoundarySuccess() throws IOException, Inte
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 b6de475ae510..c591d64d9e73 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
@@ -35,6 +35,7 @@
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 com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -45,7 +46,6 @@
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;
@@ -1312,18 +1312,6 @@ void getRegionalAccessBoundaryUrl_invalidEmail_returnsNull() throws IOException
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/ExternalAccountCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
index db3d1601ed94..36325cb4377b 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
@@ -36,6 +36,7 @@
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 com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@@ -58,7 +59,6 @@
import java.math.BigDecimal;
import java.net.URI;
import java.util.*;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -1516,18 +1516,6 @@ public void refresh_impersonated_workforce_regionalAccessBoundarySuccess()
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 67d0e990ddda..4cb19c837d52 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
@@ -32,6 +32,7 @@
package com.google.auth.oauth2;
import static com.google.auth.oauth2.RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY;
+import static com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary;
import static org.junit.Assert.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -1358,18 +1359,6 @@ private GoogleCredentials createTestCredentials(MockTokenServerTransport transpo
.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()
@@ -1381,6 +1370,65 @@ private void waitForCooldownActive(GoogleCredentials credentials) throws Interru
}
}
+ @Test
+ public void regionalAccessBoundary_refreshUsesMtlsEndpointWhenConfigured()
+ throws IOException, InterruptedException {
+ MockTokenServerTransport transport = new MockTokenServerTransport();
+ transport.setRegionalAccessBoundary(
+ new RegionalAccessBoundary("valid", Collections.singletonList("us-central1"), null));
+
+ // Configure the environment provider to enable mTLS with ALWAYS policy
+ EnvironmentProvider envProvider =
+ new EnvironmentProvider() {
+ @Override
+ public String getEnv(String name) {
+ if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) {
+ return "true";
+ }
+ if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) {
+ return "always";
+ }
+ return null;
+ }
+ };
+
+ AccessToken token =
+ new AccessToken("test-token", new java.util.Date(System.currentTimeMillis() + 3600000L));
+
+ com.google.auth.mtls.MtlsHttpTransportFactory mockMtlsFactory;
+ try {
+ java.security.KeyStore dummyKeyStore =
+ java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType());
+ dummyKeyStore.load(null, null);
+ mockMtlsFactory =
+ new com.google.auth.mtls.MtlsHttpTransportFactory(dummyKeyStore) {
+ @Override
+ public com.google.api.client.http.HttpTransport create() {
+ return transport;
+ }
+ };
+ } catch (java.security.GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+
+ TestRegionalCredentials credentials =
+ new TestRegionalCredentials(token, envProvider, mockMtlsFactory);
+
+ credentials.getRequestMetadata(URI.create("https://foo.com"));
+
+ // Wait for the async refresh to complete
+ waitForRegionalAccessBoundary(credentials);
+
+ // Verify a request was made
+ assertEquals(1, transport.getRegionalAccessBoundaryRequestCount());
+ // Verify the request URL used the mTLS subdomain: "iamcredentials.mtls.googleapis.com"
+ String requestedUrl = transport.getRequest().getUrl();
+ assertNotNull(requestedUrl);
+ assertTrue(
+ requestedUrl.contains("iamcredentials.mtls.googleapis.com"),
+ "Expected mTLS URL, but got: " + requestedUrl);
+ }
+
private static class TestClock implements Clock {
private final AtomicLong currentTime = new AtomicLong(System.currentTimeMillis());
@@ -1393,4 +1441,36 @@ public void advanceTime(long millis) {
currentTime.addAndGet(millis);
}
}
+
+ private static class TestRegionalCredentials extends GoogleCredentials
+ implements RegionalAccessBoundaryProvider {
+ private final EnvironmentProvider envProvider;
+ private final HttpTransportFactory transportFactory;
+
+ TestRegionalCredentials(AccessToken token, EnvironmentProvider envProvider) {
+ this(token, envProvider, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ }
+
+ TestRegionalCredentials(
+ AccessToken token, EnvironmentProvider envProvider, HttpTransportFactory transportFactory) {
+ super(GoogleCredentials.newBuilder().setAccessToken(token));
+ this.envProvider = envProvider;
+ this.transportFactory = transportFactory;
+ }
+
+ @Override
+ EnvironmentProvider getEnvironmentProvider() {
+ return envProvider;
+ }
+
+ @Override
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
+ @Override
+ public String getRegionalAccessBoundaryUrl() {
+ return "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/foo/allowedLocations";
+ }
+ }
}
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 3a5dcd8720e7..90b288ffe793 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
@@ -35,6 +35,7 @@
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 com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -42,7 +43,6 @@
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;
@@ -1377,16 +1377,4 @@ public void testRefresh_regionalAccessBoundarySuccess() throws IOException, Inte
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 3664fb22c2ff..8d3a2e3c6ef9 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
@@ -33,6 +33,7 @@
import static com.google.auth.oauth2.RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY;
import static com.google.auth.oauth2.TestUtils.createDummyRab;
+import static com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -42,7 +43,6 @@
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;
@@ -1312,18 +1312,6 @@ void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedExce
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/MockExternalAccountCredentialsTransport.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
index c53eda5b2bd5..fa1e8884a401 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
@@ -84,7 +84,8 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
static final String SERVICE_ACCOUNT_IMPERSONATION_URL =
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/testn@test.iam.gserviceaccount.com:generateAccessToken";
- static final String IAM_ENDPOINT = "https://iamcredentials.googleapis.com";
+ static final String IAM_ENDPOINT = "iamcredentials.googleapis.com";
+ static final String MTLS_IAM_ENDPOINT = "iamcredentials.mtls.googleapis.com";
private final Queue responseSequence = new ArrayDeque<>();
private final Queue responseErrorSequence = new ArrayDeque<>();
@@ -202,10 +203,11 @@ public LowLevelHttpResponse execute() throws IOException {
.setContent(response.toPrettyString());
}
- if (url.contains(IAM_ENDPOINT)) {
+ if (url.contains(IAM_ENDPOINT) || url.contains(MTLS_IAM_ENDPOINT)) {
if (url.endsWith(REGIONAL_ACCESS_BOUNDARY_URL_END)) {
- RegionalAccessBoundary rab = regionalAccessBoundaries.get(url);
+ String normalizedUrl = url.replace(MTLS_IAM_ENDPOINT, IAM_ENDPOINT);
+ RegionalAccessBoundary rab = regionalAccessBoundaries.get(normalizedUrl);
if (rab == null) {
rab =
new RegionalAccessBoundary(
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 8576ffe38e3a..1a3d64fc0171 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
@@ -33,10 +33,10 @@
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.TestUtils.waitForRegionalAccessBoundary;
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;
@@ -637,18 +637,6 @@ public void testRefresh_regionalAccessBoundarySuccess() throws IOException, Inte
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
index 0bc7fbe467c9..a43da03c41e6 100644
--- 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
@@ -31,25 +31,35 @@
package com.google.auth.oauth2;
+import static com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
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.TestUtils;
import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.mtls.MtlsHttpTransportFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
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;
+import org.mockito.Mockito;
public class RegionalAccessBoundaryTest {
@@ -147,6 +157,25 @@ public void testRefreshClosesResponse() throws Exception {
assertTrue(mockResponse.isDisconnected(), "Response should have been disconnected");
}
+ @Test
+ public void testRefreshThrowsIOExceptionOnExpiringToken() {
+ final String url = "https://example.com/rab";
+ final AccessToken token =
+ new AccessToken(
+ "token",
+ new java.util.Date(
+ testClock.currentTimeMillis() + 120_000L)); // 2 minutes, within 3 min skew
+
+ MockHttpTransport transport = new MockHttpTransport.Builder().build();
+ HttpTransportFactory transportFactory = () -> transport;
+
+ assertThrows(
+ IOException.class,
+ () -> {
+ RegionalAccessBoundary.refresh(transportFactory, url, token, testClock, 1000);
+ });
+ }
+
@Test
public void testManagerTriggersRefreshInGracePeriod() throws InterruptedException {
final String url =
@@ -173,7 +202,12 @@ public void testManagerTriggersRefreshInGracePeriod() throws InterruptedExceptio
RegionalAccessBoundaryManager manager = new RegionalAccessBoundaryManager(testClock);
// 1. Let's first get a RAB into the cache
- manager.triggerAsyncRefresh(transportFactory, provider, token);
+ manager.triggerAsyncRefresh(
+ transportFactory,
+ provider,
+ token,
+ SystemEnvironmentProvider.getInstance(),
+ SystemPropertyProvider.getInstance());
// Wait for it to be cached
int retries = 0;
@@ -204,7 +238,12 @@ public void testManagerTriggersRefreshInGracePeriod() throws InterruptedExceptio
HttpTransportFactory transportFactory2 = () -> transport2;
// 4. Trigger refresh - should start because we are in grace period
- manager.triggerAsyncRefresh(transportFactory2, provider, token);
+ manager.triggerAsyncRefresh(
+ transportFactory2,
+ provider,
+ token,
+ SystemEnvironmentProvider.getInstance(),
+ SystemPropertyProvider.getInstance());
// 5. Wait for background refresh to complete
// We expect the cached RAB to eventually change to newerEncoded
@@ -288,7 +327,12 @@ public int read() throws java.io.IOException {
testClock,
RegionalAccessBoundaryManager.DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS,
testExecutor);
- managers[i].triggerAsyncRefresh(transportFactory, provider, token);
+ managers[i].triggerAsyncRefresh(
+ transportFactory,
+ provider,
+ token,
+ SystemEnvironmentProvider.getInstance(),
+ SystemPropertyProvider.getInstance());
}
RegionalAccessBoundaryManager extraManager =
@@ -298,7 +342,12 @@ public int read() throws java.io.IOException {
testExecutor);
assertFalse(extraManager.isCooldownActive());
- extraManager.triggerAsyncRefresh(transportFactory, provider, token);
+ extraManager.triggerAsyncRefresh(
+ transportFactory,
+ provider,
+ token,
+ SystemEnvironmentProvider.getInstance(),
+ SystemPropertyProvider.getInstance());
assertTrue(
extraManager.isCooldownActive(),
@@ -334,4 +383,51 @@ public boolean isDisconnected() {
return disconnected;
}
}
+
+ @Test
+ public void
+ regionalAccessBoundary_withMtlsEnabled_shouldCallAllowedLocationsUsingMtlsTransportFactory()
+ throws IOException, InterruptedException {
+
+ MockExternalAccountCredentialsTransport transport =
+ new MockExternalAccountCredentialsTransport();
+
+ // Configure the environment provider to enable mTLS.
+ // X509Provider will use certificate_config.json to load keys.
+ TestEnvironmentProvider testEnvProvider = new TestEnvironmentProvider();
+ testEnvProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true");
+ testEnvProvider.setEnv(
+ "GOOGLE_API_CERTIFICATE_CONFIG",
+ new File("testresources/mtls/certificate_config.json").getAbsolutePath());
+
+ // Mock MtlsHttpTransportFactory to return our mock transport
+ MtlsHttpTransportFactory mockMtlsFactory = Mockito.mock(MtlsHttpTransportFactory.class);
+ Mockito.doReturn(transport).when(mockMtlsFactory).create();
+
+ IdentityPoolCredentials credentials =
+ IdentityPoolCredentials.newBuilder()
+ .setHttpTransportFactory(mockMtlsFactory)
+ .setEnvironmentProvider(testEnvProvider)
+ .setAudience(
+ "//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/pool/providers/provider")
+ .setSubjectTokenType("subjectTokenType")
+ .setSubjectTokenSupplier(context -> "testSubjectToken")
+ .setTokenUrl("https://sts.googleapis.com/v1/token")
+ .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));
+
+ // Verify that MtlsHttpTransportFactory.create() was called to retrieve the mTLS transport
+ Mockito.verify(mockMtlsFactory, Mockito.times(2)).create();
+ }
}
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 4bbcaa96c606..db3b4f3b8160 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
@@ -33,6 +33,7 @@
import static com.google.auth.oauth2.RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY;
import static com.google.auth.oauth2.TestUtils.createDummyRab;
+import static com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
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 52652a71c458..a0434b514a05 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
@@ -74,4 +74,17 @@ static RegionalAccessBoundary createDummyRab(com.google.api.client.util.Clock cl
return new RegionalAccessBoundary(
"dummy-locations", java.util.Arrays.asList("dummy-loc"), clock);
}
+
+ static 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) {
+ org.junit.jupiter.api.Assertions.fail(
+ "Timed out waiting for regional access boundary refresh");
+ }
+ }
}