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..545d3f4242ac 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 @@ -51,6 +51,19 @@ public class MtlsUtils { static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json"; static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud"; + @com.google.common.annotations.VisibleForTesting + static String spiffeDirectory = "/var/run/secrets/workload-spiffe-credentials/"; + + static final String SPIFFE_CREDENTIAL_BUNDLE_FILE = "credentialbundle.pem"; + static final String SPIFFE_CERTIFICATE_FILE = "certificates.pem"; + static final String SPIFFE_PRIVATE_KEY_FILE = "private_key.pem"; + + public enum MtlsEndpointUsagePolicy { + ALWAYS, + NEVER, + AUTO + } + private MtlsUtils() { // Prevent instantiation for Utility class } @@ -137,4 +150,98 @@ 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 canMtlsBeEnabled( + 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; + } + + if (getMtlsEndpointUsagePolicy(envProvider) == MtlsEndpointUsagePolicy.NEVER) { + return false; + } + + // Locate and process the certificate configuration file + String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE); + if (certConfigPathOverride != null || !Strings.isNullOrEmpty(envPath)) { + File certConfigFile = + new File(certConfigPathOverride != null ? certConfigPathOverride : envPath); + if (!certConfigFile.isFile()) { + throw new CertificateSourceUnavailableException( + "Certificate configuration file does not exist or is not a file: " + + certConfigFile.getAbsolutePath()); + } + } + + WorkloadCertificateConfiguration workloadCertConfig = null; + try { + workloadCertConfig = + getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride); + } catch (CertificateSourceUnavailableException e) { + // Config file is simply not present. This is fine, fallback to SPIFFE. + } catch (IOException e) { + // Config file exists but is malformed or points to invalid paths -> throw hard error + throw e; + } + + if (workloadCertConfig != null) { + // Validate referenced files exist + File certFile = new File(workloadCertConfig.getCertPath()); + File keyFile = new File(workloadCertConfig.getPrivateKeyPath()); + if (!certFile.isFile() || !keyFile.isFile()) { + throw new IOException( + String.format( + "Certificate configuration exists but referenced files are missing: cert_path=%s, key_path=%s", + workloadCertConfig.getCertPath(), workloadCertConfig.getPrivateKeyPath())); + } + return true; + } + + // Fallback to SPIFFE discovery if the directory exists + File spiffeDir = new File(spiffeDirectory); + if (spiffeDir.isDirectory()) { + File credentialBundle = new File(spiffeDir, SPIFFE_CREDENTIAL_BUNDLE_FILE); + if (credentialBundle.isFile()) { + return true; + } + File certsFile = new File(spiffeDir, SPIFFE_CERTIFICATE_FILE); + File keyFile = new File(spiffeDir, SPIFFE_PRIVATE_KEY_FILE); + if (certsFile.isFile() && keyFile.isFile()) { + return true; + } + } + + return false; + } + + /** + * 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".equals(mtlsEndpointUsagePolicy)) { + return MtlsEndpointUsagePolicy.NEVER; + } else if ("always".equals(mtlsEndpointUsagePolicy)) { + return MtlsEndpointUsagePolicy.ALWAYS; + } + return MtlsEndpointUsagePolicy.AUTO; + } } 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 4127b1492460..396805c220f4 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 @@ -36,6 +36,7 @@ import com.google.auth.oauth2.PropertyProvider; import com.google.auth.oauth2.SystemEnvironmentProvider; import com.google.auth.oauth2.SystemPropertyProvider; +import com.google.common.base.Strings; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -102,10 +103,13 @@ public X509Provider() { * *
If none of the above are available, it will attempt to discover and load certificates from
+ * SPIFFE credentials located under "/var/run/secrets/workload-spiffe-credentials/".
+ *
* @return a KeyStore containing the X.509 certificate specified by the certificate configuration.
* @throws CertificateSourceUnavailableException if the certificate source is unavailable (ex.
* missing configuration file)
@@ -113,40 +117,69 @@ public X509Provider() {
*/
@Override
public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
- WorkloadCertificateConfiguration workloadCertConfig =
- MtlsUtils.getWorkloadCertificateConfiguration(
- envProvider, propProvider, certConfigPathOverride);
+ // Attempt to load from resolved Config File
+ WorkloadCertificateConfiguration workloadCertConfig = null;
+ try {
+ workloadCertConfig =
+ MtlsUtils.getWorkloadCertificateConfiguration(
+ envProvider, propProvider, certConfigPathOverride);
+ } catch (CertificateSourceUnavailableException e) {
+ // Ignore config-not-found error to fall back to SPIFFE discovery ONLY if not explicitly
+ // configured.
+ boolean isExplicitlyConfigured =
+ certConfigPathOverride != null
+ || !Strings.isNullOrEmpty(
+ envProvider.getEnv(MtlsUtils.CERTIFICATE_CONFIGURATION_ENV_VARIABLE));
+ if (isExplicitlyConfigured) {
+ throw e;
+ }
+ }
- // 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)) {
+ if (workloadCertConfig != null) {
+ 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 (Exception e) {
+ throw new IOException("X509Provider: Unexpected error loading from config file:", e);
+ }
+ }
- // Build a key store using the combined stream.
- 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);
+ // Fallback: Load from SPIFFE Credentials
+ File spiffeDir = new File(MtlsUtils.spiffeDirectory);
+ if (spiffeDir.isDirectory()) {
+ File credentialBundle = new File(spiffeDir, MtlsUtils.SPIFFE_CREDENTIAL_BUNDLE_FILE);
+ if (credentialBundle.isFile()) {
+ try (InputStream bundleStream = new FileInputStream(credentialBundle)) {
+ return SecurityUtils.createMtlsKeyStore(bundleStream);
+ } catch (Exception e) {
+ throw new IOException("X509Provider: Unexpected error loading from SPIFFE bundle:", e);
+ }
+ }
+
+ File certsFile = new File(spiffeDir, MtlsUtils.SPIFFE_CERTIFICATE_FILE);
+ File keyFile = new File(spiffeDir, MtlsUtils.SPIFFE_PRIVATE_KEY_FILE);
+ if (certsFile.isFile() && keyFile.isFile()) {
+ try (InputStream certStream = new FileInputStream(certsFile);
+ InputStream privateKeyStream = new FileInputStream(keyFile);
+ SequenceInputStream certAndPrivateKeyStream =
+ new SequenceInputStream(certStream, privateKeyStream)) {
+ return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
+ } catch (Exception e) {
+ throw new IOException(
+ "X509Provider: Unexpected error loading from separate SPIFFE files:", e);
+ }
+ }
}
+
+ throw new CertificateSourceUnavailableException("No certificate source was resolved.");
}
- /**
- * 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 {
- this.getKeyStore();
- } catch (CertificateSourceUnavailableException e) {
- return false;
- }
- return true;
+ return MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride);
}
}
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 e423a68ac18b..37068076df4b 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
@@ -39,6 +39,9 @@
import com.google.auth.RequestMetadataCallback;
import com.google.auth.http.AuthHttpConstants;
import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.mtls.MtlsHttpTransportFactory;
+import com.google.auth.mtls.MtlsUtils;
+import com.google.auth.mtls.X509Provider;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
@@ -51,6 +54,7 @@
import java.io.ObjectInputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
@@ -397,6 +401,20 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT
return;
}
+ try {
+ if (!(transportFactory instanceof MtlsHttpTransportFactory)
+ && MtlsUtils.canMtlsBeEnabled(getEnvironmentProvider(), getPropertyProvider(), null)) {
+ X509Provider x509Provider =
+ new X509Provider(getEnvironmentProvider(), getPropertyProvider(), null);
+ KeyStore mtlsKeyStore = x509Provider.getKeyStore();
+ if (mtlsKeyStore != null) {
+ transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore);
+ }
+ }
+ } catch (Exception e) {
+ // Graceful fallback to standard transport if mTLS initialization fails
+ }
+
regionalAccessBoundaryManager.triggerAsyncRefresh(
transportFactory, (RegionalAccessBoundaryProvider) this, token);
}
@@ -843,6 +861,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 dfcbe8491cd5..e832a145087a 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
@@ -189,6 +189,10 @@ static RegionalAccessBoundary refresh(
throw new IllegalArgumentException("The provided access token is expired.");
}
+ if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory) {
+ url = url.replace("iamcredentials.googleapis.com", "iamcredentials.mtls.googleapis.com");
+ }
+
HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url));
// Disable automatic logging by google-http-java-client to prevent leakage of sensitive tokens.
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..8531e5615f4b 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
@@ -243,4 +243,240 @@ 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, canMtlsBeEnabled should return false.
+ @Test
+ void canMtlsBeEnabled_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.canMtlsBeEnabled(envProvider, propProvider, null));
+ }
+
+ // If client certificate usage is explicitly enabled and a valid configuration is present,
+ // canMtlsBeEnabled should return true.
+ @Test
+ void canMtlsBeEnabled_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.canMtlsBeEnabled(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 canMtlsBeEnabled_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.canMtlsBeEnabled(envProvider, propProvider, null));
+ }
+
+ // If the GOOGLE_API_CERTIFICATE_CONFIG environment variable points to a non-existent file,
+ // canMtlsBeEnabled should throw an IOException.
+ @Test
+ void canMtlsBeEnabled_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.canMtlsBeEnabled(envProvider, propProvider, null));
+ }
+
+ // If the well-known gcloud certificate configuration file exists, canMtlsBeEnabled should return
+ // true.
+ @Test
+ void canMtlsBeEnabled_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.canMtlsBeEnabled(envProvider, propProvider, null));
+ }
+
+ // If the configuration file exists but the certificate path it references does not exist,
+ // canMtlsBeEnabled should throw an IOException.
+ @Test
+ void canMtlsBeEnabled_configMissingCertFile_throwsIOException() throws IOException {
+ Path configFile = tempDir.resolve("config.json");
+ Path nonExistentCert = tempDir.resolve("non_existent_cert.pem");
+ Path keyFile = tempDir.resolve("key.pem");
+ Files.createFile(keyFile);
+ Files.write(configFile, createJsonConfigString(nonExistentCert, keyFile).getBytes());
+
+ EnvironmentProvider envProvider =
+ name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertThrows(
+ IOException.class, () -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null));
+ }
+
+ // If the configuration file exists but the private key path it references does not exist,
+ // canMtlsBeEnabled should throw an IOException.
+ @Test
+ void canMtlsBeEnabled_configMissingKeyFile_throwsIOException() throws IOException {
+ Path configFile = tempDir.resolve("config.json");
+ Path certFile = tempDir.resolve("cert.pem");
+ Path nonExistentKey = tempDir.resolve("non_existent_key.pem");
+ Files.createFile(certFile);
+ Files.write(configFile, createJsonConfigString(certFile, nonExistentKey).getBytes());
+
+ EnvironmentProvider envProvider =
+ name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertThrows(
+ IOException.class, () -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null));
+ }
+
+ // If no configuration file exists but a SPIFFE credential bundle file is present,
+ // canMtlsBeEnabled should return true.
+ @Test
+ void canMtlsBeEnabled_unset_spiffeBundlePresent_returnsTrue() throws IOException {
+ Path spiffeDir = tempDir.resolve("spiffe_workload_bundle");
+ Files.createDirectory(spiffeDir);
+ Files.createFile(spiffeDir.resolve("credentialbundle.pem"));
+
+ String originalSpiffeDir = MtlsUtils.spiffeDirectory;
+ MtlsUtils.spiffeDirectory = spiffeDir.toString() + "/";
+ try {
+ EnvironmentProvider envProvider = name -> null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertTrue(MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null));
+ } finally {
+ MtlsUtils.spiffeDirectory = originalSpiffeDir;
+ }
+ }
+
+ // If no configuration file exists but separate SPIFFE certificate and key files are present,
+ // canMtlsBeEnabled should return true.
+ @Test
+ void canMtlsBeEnabled_unset_spiffeCertsPresent_returnsTrue() throws IOException {
+ Path spiffeDir = tempDir.resolve("spiffe_workload_certs");
+ Files.createDirectory(spiffeDir);
+ Files.createFile(spiffeDir.resolve("certificates.pem"));
+ Files.createFile(spiffeDir.resolve("private_key.pem"));
+
+ String originalSpiffeDir = MtlsUtils.spiffeDirectory;
+ MtlsUtils.spiffeDirectory = spiffeDir.toString() + "/";
+ try {
+ EnvironmentProvider envProvider = name -> null;
+ PropertyProvider propProvider = (name, def) -> def;
+
+ assertTrue(MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null));
+ } finally {
+ MtlsUtils.spiffeDirectory = originalSpiffeDir;
+ }
+ }
+
+ @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 canMtlsBeEnabled_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.canMtlsBeEnabled(envProvider, propProvider, null));
+ }
+
+ 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..beec6dafc6cf 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
@@ -208,4 +208,64 @@ void x509Provider_malformedCert_throws() throws IOException {
assertThrows(Exception.class, testProvider::getKeyStore);
}
+
+ // Success Path: SPIFFE Bundle loading
+ @Test
+ void x509Provider_loadSpiffeBundle_succeeds() throws Exception {
+ Path spiffeDir = Files.createTempDirectory("spiffe_bundle");
+ spiffeDir.toFile().deleteOnExit();
+ Path credentialBundle = spiffeDir.resolve("credentialbundle.pem");
+
+ // Create credentialbundle.pem by combining valid test cert and key
+ byte[] certBytes = Files.readAllBytes(new File(TEST_CERT_PATH).toPath());
+ byte[] keyBytes = Files.readAllBytes(new File("testresources/mtls/test_key.pem").toPath());
+ byte[] bundleBytes = new byte[certBytes.length + keyBytes.length];
+ System.arraycopy(certBytes, 0, bundleBytes, 0, certBytes.length);
+ System.arraycopy(keyBytes, 0, bundleBytes, certBytes.length, keyBytes.length);
+ Files.write(credentialBundle, bundleBytes);
+
+ String originalSpiffeDir = MtlsUtils.spiffeDirectory;
+ MtlsUtils.spiffeDirectory = spiffeDir.toString() + "/";
+ try {
+ X509Provider provider = new X509Provider(name -> null, (name, def) -> def, null);
+ KeyStore keyStore = provider.getKeyStore();
+ assertNotNull(keyStore);
+ assertEquals(1, keyStore.size());
+ } finally {
+ MtlsUtils.spiffeDirectory = originalSpiffeDir;
+ }
+ }
+
+ // Success Path: SPIFFE Separate Files loading
+ @Test
+ void x509Provider_loadSpiffeSeparateFiles_succeeds() throws Exception {
+ Path spiffeDir = Files.createTempDirectory("spiffe_separate");
+ spiffeDir.toFile().deleteOnExit();
+
+ Files.copy(new File(TEST_CERT_PATH).toPath(), spiffeDir.resolve("certificates.pem"));
+ Files.copy(
+ new File("testresources/mtls/test_key.pem").toPath(), spiffeDir.resolve("private_key.pem"));
+
+ String originalSpiffeDir = MtlsUtils.spiffeDirectory;
+ MtlsUtils.spiffeDirectory = spiffeDir.toString() + "/";
+ try {
+ X509Provider provider = new X509Provider(name -> null, (name, def) -> def, null);
+ KeyStore keyStore = provider.getKeyStore();
+ assertNotNull(keyStore);
+ assertEquals(1, keyStore.size());
+ } finally {
+ MtlsUtils.spiffeDirectory = originalSpiffeDir;
+ }
+ }
+
+ // 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);
+ }
}
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 78bfd5ddaaa4..603736fd069f 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;
@@ -1242,18 +1242,6 @@ void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedExce
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");
- }
- }
-
static class MockMetadataServerTransportFactory implements HttpTransportFactory {
MockMetadataServerTransport transport =
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
index b374e08111ff..1647a46b73e0 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
@@ -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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -62,7 +63,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -1270,18 +1270,6 @@ void testRefresh_regionalAccessBoundarySuccess() throws IOException, Interrupted
Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
}
- private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
- throws InterruptedException {
- long deadline = System.currentTimeMillis() + 5000;
- while (credentials.getRegionalAccessBoundary() == null
- && System.currentTimeMillis() < deadline) {
- Thread.sleep(100);
- }
- if (credentials.getRegionalAccessBoundary() == null) {
- Assertions.fail("Timed out waiting for regional access boundary refresh");
- }
- }
-
static GenericJson buildJsonCredentials() {
GenericJson json = new GenericJson();
json.put(
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 ae4fbb1aac8b..4dae7d77106c 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;
@@ -1490,18 +1490,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 18e5c4585eef..e6bea047056c 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;
@@ -1332,18 +1333,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()
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..9ae6b67cc294 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
@@ -202,7 +202,8 @@ public LowLevelHttpResponse execute() throws IOException {
.setContent(response.toPrettyString());
}
- if (url.contains(IAM_ENDPOINT)) {
+ if (url.contains("iamcredentials.googleapis.com")
+ || url.contains("iamcredentials.mtls.googleapis.com")) {
if (url.endsWith(REGIONAL_ACCESS_BOUNDARY_URL_END)) {
RegionalAccessBoundary rab = regionalAccessBoundaries.get(url);
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java
index 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 5664582ef059..3f38339b44d2 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,34 @@
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.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 {
@@ -334,4 +343,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