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 3e658322347a..d3a593c6e0aa 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; @@ -64,7 +65,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 a5c4c0f86e77..2dfabb818a8e 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,15 +31,20 @@ 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 org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Utility class for mTLS related operations. @@ -49,10 +54,27 @@ @NullMarked @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 } @@ -65,7 +87,9 @@ private MtlsUtils() { * @throws IOException if the certificate configuration cannot be found or loaded. */ public static String getCertificatePath( - EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride) + EnvironmentProvider envProvider, + PropertyProvider propProvider, + @Nullable String certConfigPathOverride) throws IOException { String certPath = getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride) @@ -78,39 +102,38 @@ 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) + EnvironmentProvider envProvider, + PropertyProvider propProvider, + @Nullable 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); } @@ -139,4 +162,204 @@ 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, + @Nullable 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); + if (certConfigFile == null) { + return false; + } + + try { + WorkloadCertificateConfiguration config = + getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride); + File certFile = new File(config.getCertPath()); + File keyFile = new File(config.getPrivateKeyPath()); + return certFile.isFile() && keyFile.isFile(); + } catch (IOException e) { + return false; + } + } + + /** + * 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, + @Nullable 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: + * + *
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 + */ + static @Nullable File resolveCertificateConfigFile( + EnvironmentProvider envProvider, + PropertyProvider propProvider, + @Nullable 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 @Nullable HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( + @Nullable HttpTransportFactory baseTransportFactory, + EnvironmentProvider envProvider, + PropertyProvider propProvider, + @Nullable String certConfigPathOverride) + throws IOException { + + if (baseTransportFactory == null) { + return null; + } + + 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 911c0fdcf922..4b52b7d218d4 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 @@ -103,7 +103,7 @@ public X509Provider() { * *
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.
*/
+@NullMarked
@InternalApi
final class RegionalAccessBoundaryManager {
@@ -68,6 +72,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.
@@ -149,8 +156,7 @@ final class RegionalAccessBoundaryManager {
*
* @return The cached RAB, or null.
*/
- @Nullable
- RegionalAccessBoundary getCachedRAB() {
+ @Nullable RegionalAccessBoundary getCachedRAB() {
RegionalAccessBoundary rab = cachedRAB.get();
if (rab != null && !rab.isExpired()) {
return rab;
@@ -159,10 +165,21 @@ RegionalAccessBoundary getCachedRAB() {
}
@VisibleForTesting
- void setCachedRAB(RegionalAccessBoundary rab) {
+ void setCachedRAB(@Nullable RegionalAccessBoundary rab) {
this.cachedRAB.set(rab);
}
+ private boolean shouldStartBackgroundRefresh() {
+ if (skipRAB.get() || isCooldownActive()) {
+ return false;
+ }
+ RegionalAccessBoundary currentRab = cachedRAB.get();
+ if (currentRab != null && !currentRab.shouldRefresh()) {
+ return false;
+ }
+ return true;
+ }
+
/**
* Triggers an asynchronous refresh of the RegionalAccessBoundary if it is not already being
* refreshed and if the cooldown period is not active.
@@ -177,13 +194,10 @@ void setCachedRAB(RegionalAccessBoundary rab) {
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()) {
+ final AccessToken accessToken,
+ final EnvironmentProvider envProvider,
+ final PropertyProvider propProvider) {
+ if (!shouldStartBackgroundRefresh()) {
return;
}
@@ -191,6 +205,10 @@ 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)) {
+ if (!shouldStartBackgroundRefresh()) {
+ isRefreshing.set(false);
+ return;
+ }
Runnable refreshTask =
() -> {
try {
@@ -199,9 +217,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 +241,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,
@@ -224,7 +250,6 @@ void triggerAsyncRefresh(
"Could not submit background refresh task for Regional Access Boundary. "
+ "This is non-blocking and the library will attempt to refresh on the next access. Error: "
+ e.getMessage());
- handleRefreshFailure(e);
isRefreshing.set(false);
}
}
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..1f536b218db4 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,372 @@ 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 shouldMtlsEndpointBeUsed_autoPolicy_withMissingCertFiles_returnsFalse() throws IOException {
+ Path configFile = tempDir.resolve("config.json");
+ Path nonExistentCert = tempDir.resolve("non_existent_cert.pem");
+ Files.write(
+ configFile,
+ ("{\"cert_configs\":{\"workload\":{\"cert_path\":\""
+ + nonExistentCert.toString().replace("\\", "\\\\")
+ + "\",\"key_path\":\"key.pem\"}}}")
+ .getBytes());
+
+ EnvironmentProvider envProvider =
+ name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null));
+ assertFalse(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 a9828b6bcf7d..5d8db5a8a0db 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;
@@ -1317,18 +1317,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");
- }
- }
-
@Test
void isOnGce_clientError_doesNotRetry_returnsFalseOnUnknownOs() {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
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..4e0b155ce584 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;
@@ -64,7 +65,7 @@
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -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 2a73ceb92534..b02816057448 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 9324f6ad564f..05706533d37e 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;
import static org.mockito.Mockito.withSettings;
@@ -515,6 +515,8 @@ void refreshAccessToken_success_SSJflow() throws IOException, IllegalStateExcept
.setHttpTransportFactory(mockTransportFactory)
.setUseJwtAccessWithScope(true)
.build();
+ sourceCredentialsSSJ.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(sourceCredentialsSSJ.clock));
ImpersonatedCredentials targetCredentials =
ImpersonatedCredentials.create(
sourceCredentialsSSJ,
@@ -544,6 +546,8 @@ void refreshAccessToken_success_nonGDU() throws IOException, IllegalStateExcepti
.setUniverseDomain(TEST_UNIVERSE_DOMAIN)
.setHttpTransportFactory(transportFactory)
.build();
+ sourceCredentialsNonGDU.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(sourceCredentialsNonGDU.clock));
ImpersonatedCredentials impersonatedCredentials =
ImpersonatedCredentials.create(
sourceCredentialsNonGDU,
@@ -845,6 +849,8 @@ void sign_sameAs_nonGDU() {
.setUniverseDomain("test.com")
.setHttpTransportFactory(transportFactory)
.build();
+ sourceCredentialsNonGDU.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(sourceCredentialsNonGDU.clock));
ImpersonatedCredentials targetCredentials =
ImpersonatedCredentials.create(
sourceCredentialsNonGDU,
@@ -972,6 +978,8 @@ void idTokenWithAudience_sameAs_nonGDU() throws IOException {
.setUniverseDomain("test.com")
.setHttpTransportFactory(transportFactory)
.build();
+ sourceCredentialsNonGDU.regionalAccessBoundaryManager.setCachedRAB(
+ createDummyRab(sourceCredentialsNonGDU.clock));
ImpersonatedCredentials targetCredentials =
ImpersonatedCredentials.create(
sourceCredentialsNonGDU,
@@ -1314,18 +1322,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