From 2429a2bea6a8f9db1cf9528a440576a8fefc44aa Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Sun, 31 May 2026 14:30:12 -0700 Subject: [PATCH 1/5] Added changes to enable cert-discovery and mtls by default in the auth library. --- .../java/com/google/auth/mtls/MtlsUtils.java | 75 +++++++ .../com/google/auth/mtls/X509Provider.java | 81 ++++--- .../com/google/auth/mtls/MtlsUtilsTest.java | 199 ++++++++++++++++++ .../google/auth/mtls/X509ProviderTest.java | 59 ++++++ 4 files changed, 385 insertions(+), 29 deletions(-) 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..6b7d4b6bb09f 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,12 @@ 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"; + private MtlsUtils() { // Prevent instantiation for Utility class } @@ -137,4 +143,73 @@ 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; + } + + // 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; + } } 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..9db8c643081a 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 @@ -112,41 +112,64 @@ public X509Provider() { * @throws IOException if a general I/O error occurs while creating the KeyStore */ @Override - public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException { - WorkloadCertificateConfiguration workloadCertConfig = - MtlsUtils.getWorkloadCertificateConfiguration( - envProvider, propProvider, certConfigPathOverride); - - // Read the certificate and private key file paths into streams. - try (InputStream certStream = new FileInputStream(new File(workloadCertConfig.getCertPath())); - InputStream privateKeyStream = - new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath())); - SequenceInputStream certAndPrivateKeyStream = - new SequenceInputStream(certStream, privateKeyStream)) { - - // Build a key store using the combined stream. - return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream); - } catch (CertificateSourceUnavailableException e) { - // Throw the CertificateSourceUnavailableException without wrapping. + public boolean isAvailable() throws IOException { + try { + return MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride); + } catch (IOException e) { + // Broken configuration state defaults to throwing a failure throw e; - } catch (Exception e) { - // Wrap all other exception types to an IOException. - throw new IOException("X509Provider: Unexpected IOException:", e); } } - /** - * Returns true if the X509 mTLS provider is available. - * - * @throws IOException if a general I/O error occurs while determining availability. - */ @Override - public boolean isAvailable() throws IOException { + public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException { + if (!MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride)) { + throw new CertificateSourceUnavailableException("mTLS is not enabled or cannot be established."); + } + + // 1. Attempt to load from resolved Config File + WorkloadCertificateConfiguration workloadCertConfig = null; try { - this.getKeyStore(); - } catch (CertificateSourceUnavailableException e) { - return false; + workloadCertConfig = MtlsUtils.getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride); + } catch (IOException e) { + // Ignore configuration file errors here to fall back to SPIFFE discovery + } + + 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); + } + } + + // 2. 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); + } + } } - return true; + + throw new CertificateSourceUnavailableException("mTLS is enabled, but no certificate source was resolved."); } } 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..e41e0ecf0d04 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,203 @@ 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 { + Path configFile = tempDir.resolve("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 = + 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 configFile.toString(); + } + 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 { + Path configFile = tempDir.resolve("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 = + new EnvironmentProvider() { + @Override + public String getEnv(String name) { + if ("GOOGLE_API_CERTIFICATE_CONFIG".equals(name)) { + return configFile.toString(); + } + 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; + } + } + + 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..86ac1586e4d4 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,63 @@ 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); + } } From 619805f995eb1381a72d99a4dd73e2389102b8f2 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Sun, 31 May 2026 14:51:35 -0700 Subject: [PATCH 2/5] Added changes for RAB mtls endpoint. --- .../google/auth/oauth2/GoogleCredentials.java | 23 +++++++++++++++++++ .../auth/oauth2/RegionalAccessBoundary.java | 4 ++++ 2 files changed, 27 insertions(+) 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..2b68ad28fc0d 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 @@ -59,6 +59,11 @@ import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; +import com.google.auth.mtls.MtlsHttpTransportFactory; +import com.google.auth.mtls.MtlsUtils; +import com.google.auth.mtls.X509Provider; +import java.security.KeyStore; + /** Base type for credentials for authorizing calls to Google APIs using OAuth2. */ public class GoogleCredentials extends OAuth2Credentials implements QuotaProjectIdProvider { @@ -397,6 +402,24 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT return; } + try { + if (MtlsUtils.canMtlsBeEnabled( + SystemEnvironmentProvider.getInstance(), + SystemPropertyProvider.getInstance(), + null)) { + X509Provider x509Provider = new X509Provider( + SystemEnvironmentProvider.getInstance(), + SystemPropertyProvider.getInstance(), + 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); } 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..37ba5de893eb 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("https://iamcredentials.googleapis.com/", "https://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. From 6d899299d8ecc9b8cdda9ef2c4a2e93a4b75ed1f Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 4 Jun 2026 01:31:54 -0700 Subject: [PATCH 3/5] Removed redundant call to canMtlsbeEnabled. --- .../java/com/google/auth/mtls/MtlsUtils.java | 15 ++++--- .../com/google/auth/mtls/X509Provider.java | 41 ++++++++++++------- .../google/auth/oauth2/GoogleCredentials.java | 22 +++++----- .../auth/oauth2/RegionalAccessBoundary.java | 5 ++- .../com/google/auth/mtls/MtlsUtilsTest.java | 41 +++++++++++-------- .../google/auth/mtls/X509ProviderTest.java | 15 +++---- 6 files changed, 83 insertions(+), 56 deletions(-) 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 6b7d4b6bb09f..37af755b8693 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 @@ -53,6 +53,7 @@ public class MtlsUtils { @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"; @@ -151,11 +152,13 @@ private static File getWellKnownCertificateConfigFile( * @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 + * @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 { - + 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)) { @@ -165,7 +168,8 @@ public static boolean canMtlsBeEnabled( // 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); + 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: " @@ -175,7 +179,8 @@ public static boolean canMtlsBeEnabled( WorkloadCertificateConfiguration workloadCertConfig = null; try { - workloadCertConfig = getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride); + workloadCertConfig = + getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride); } catch (CertificateSourceUnavailableException e) { // Config file is simply not present. This is fine, fallback to SPIFFE. } catch (IOException e) { 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 9db8c643081a..af43cc5d9fb0 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; @@ -123,22 +124,31 @@ public boolean isAvailable() throws IOException { @Override public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException { - if (!MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride)) { - throw new CertificateSourceUnavailableException("mTLS is not enabled or cannot be established."); - } - // 1. Attempt to load from resolved Config File WorkloadCertificateConfiguration workloadCertConfig = null; try { - workloadCertConfig = MtlsUtils.getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride); - } catch (IOException e) { - // Ignore configuration file errors here to fall back to SPIFFE discovery + 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; + } } 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)) { + 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); @@ -156,20 +166,23 @@ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOEx 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)) { + 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 IOException( + "X509Provider: Unexpected error loading from separate SPIFFE files:", e); } } } - throw new CertificateSourceUnavailableException("mTLS is enabled, but no certificate source was resolved."); + throw new CertificateSourceUnavailableException( + "mTLS is enabled, but no certificate source was resolved."); } } 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 2b68ad28fc0d..b4e3df85f763 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; @@ -59,11 +63,6 @@ import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; -import com.google.auth.mtls.MtlsHttpTransportFactory; -import com.google.auth.mtls.MtlsUtils; -import com.google.auth.mtls.X509Provider; -import java.security.KeyStore; - /** Base type for credentials for authorizing calls to Google APIs using OAuth2. */ public class GoogleCredentials extends OAuth2Credentials implements QuotaProjectIdProvider { @@ -404,13 +403,12 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT try { if (MtlsUtils.canMtlsBeEnabled( - SystemEnvironmentProvider.getInstance(), - SystemPropertyProvider.getInstance(), - null)) { - X509Provider x509Provider = new X509Provider( - SystemEnvironmentProvider.getInstance(), - SystemPropertyProvider.getInstance(), - null); + SystemEnvironmentProvider.getInstance(), SystemPropertyProvider.getInstance(), null)) { + X509Provider x509Provider = + new X509Provider( + SystemEnvironmentProvider.getInstance(), + SystemPropertyProvider.getInstance(), + null); KeyStore mtlsKeyStore = x509Provider.getKeyStore(); if (mtlsKeyStore != null) { transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore); 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 37ba5de893eb..cd394e75d9b4 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 @@ -190,7 +190,10 @@ static RegionalAccessBoundary refresh( } if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory) { - url = url.replace("https://iamcredentials.googleapis.com/", "https://iamcredentials.mtls.googleapis.com/"); + url = + url.replace( + "https://iamcredentials.googleapis.com/", + "https://iamcredentials.mtls.googleapis.com/"); } HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); 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 e41e0ecf0d04..cf066a3c8a83 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 @@ -258,11 +258,12 @@ public String getEnv(String name) { } }; 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. + // 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 { Path configFile = tempDir.resolve("config.json"); @@ -290,7 +291,8 @@ public String getEnv(String name) { 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). + // 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 { Path configFile = tempDir.resolve("config.json"); @@ -315,7 +317,8 @@ public String getEnv(String name) { 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. + // 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"); @@ -332,11 +335,11 @@ public String getEnv(String name) { PropertyProvider propProvider = (name, def) -> def; assertThrows( - IOException.class, - () -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); + IOException.class, () -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); } - // If the well-known gcloud certificate configuration file exists, canMtlsBeEnabled should return true. + // 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"); @@ -362,7 +365,8 @@ public String getProperty(String name, String 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. + // 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"); @@ -371,15 +375,16 @@ void canMtlsBeEnabled_configMissingCertFile_throwsIOException() throws IOExcepti Files.createFile(keyFile); Files.write(configFile, createJsonConfigString(nonExistentCert, keyFile).getBytes()); - EnvironmentProvider envProvider = name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null; + 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)); + 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. + // 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"); @@ -388,15 +393,16 @@ void canMtlsBeEnabled_configMissingKeyFile_throwsIOException() throws IOExceptio Files.createFile(certFile); Files.write(configFile, createJsonConfigString(certFile, nonExistentKey).getBytes()); - EnvironmentProvider envProvider = name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null; + 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)); + IOException.class, () -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); } - // If no configuration file exists but a SPIFFE credential bundle file is present, canMtlsBeEnabled should return true. + // 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"); @@ -415,7 +421,8 @@ void canMtlsBeEnabled_unset_spiffeBundlePresent_returnsTrue() throws IOException } } - // If no configuration file exists but separate SPIFFE certificate and key files are present, canMtlsBeEnabled should return true. + // 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"); 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 86ac1586e4d4..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 @@ -241,9 +241,10 @@ void x509Provider_loadSpiffeBundle_succeeds() throws Exception { 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")); + Files.copy( + new File("testresources/mtls/test_key.pem").toPath(), spiffeDir.resolve("private_key.pem")); String originalSpiffeDir = MtlsUtils.spiffeDirectory; MtlsUtils.spiffeDirectory = spiffeDir.toString() + "/"; @@ -260,11 +261,11 @@ void x509Provider_loadSpiffeSeparateFiles_succeeds() throws Exception { // 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 - ); + X509Provider provider = + new X509Provider( + name -> "GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name) ? "false" : null, + (name, def) -> def, + null); assertThrows(CertificateSourceUnavailableException.class, provider::getKeyStore); } } From c4e24f12995f3fa1d1a107cd31dac204cc7b567b Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 4 Jun 2026 14:51:29 -0700 Subject: [PATCH 4/5] Added GOOGLE_API_USE_MTLS_ENDPOINT check to canMtlsBeEnabled. Added an E2E test to check the call to RAB mtls endpoint and check x-allowed-locations header. Unified redundant method(s) waitForRegionalAccessBoundary. --- .../auth/mtls/MtlsHttpTransportFactory.java | 3 +- .../java/com/google/auth/mtls/MtlsUtils.java | 27 ++++++++ .../com/google/auth/mtls/X509Provider.java | 14 ++--- .../google/auth/oauth2/GoogleCredentials.java | 17 +++-- .../com/google/auth/mtls/MtlsUtilsTest.java | 62 ++++++++++++++----- .../auth/oauth2/AwsCredentialsTest.java | 14 +---- .../oauth2/ComputeEngineCredentialsTest.java | 14 +---- ...lAccountAuthorizedUserCredentialsTest.java | 14 +---- .../ExternalAccountCredentialsTest.java | 14 +---- .../auth/oauth2/GoogleCredentialsTest.java | 13 +--- .../oauth2/IdentityPoolCredentialsTest.java | 14 +---- .../oauth2/ImpersonatedCredentialsTest.java | 14 +---- ...ckExternalAccountCredentialsTransport.java | 3 +- .../oauth2/PluggableAuthCredentialsTest.java | 14 +---- .../oauth2/RegionalAccessBoundaryTest.java | 56 +++++++++++++++++ .../oauth2/ServiceAccountCredentialsTest.java | 13 +--- .../com/google/auth/oauth2/TestUtils.java | 13 ++++ 17 files changed, 170 insertions(+), 149 deletions(-) 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 37af755b8693..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 @@ -58,6 +58,12 @@ public class MtlsUtils { 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 } @@ -165,6 +171,10 @@ public static boolean canMtlsBeEnabled( 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)) { @@ -217,4 +227,21 @@ public static boolean canMtlsBeEnabled( 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 af43cc5d9fb0..f1ebb362e820 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 @@ -114,17 +114,12 @@ public X509Provider() { */ @Override public boolean isAvailable() throws IOException { - try { - return MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride); - } catch (IOException e) { - // Broken configuration state defaults to throwing a failure - throw e; - } + return MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride); } @Override public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException { - // 1. Attempt to load from resolved Config File + // Attempt to load from resolved Config File WorkloadCertificateConfiguration workloadCertConfig = null; try { workloadCertConfig = @@ -155,7 +150,7 @@ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOEx } } - // 2. Fallback: Load from SPIFFE Credentials + // Fallback: Load from SPIFFE Credentials File spiffeDir = new File(MtlsUtils.spiffeDirectory); if (spiffeDir.isDirectory()) { File credentialBundle = new File(spiffeDir, MtlsUtils.SPIFFE_CREDENTIAL_BUNDLE_FILE); @@ -182,7 +177,6 @@ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOEx } } - throw new CertificateSourceUnavailableException( - "mTLS is enabled, but no certificate source was resolved."); + throw new CertificateSourceUnavailableException("No certificate source was resolved."); } } 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 b4e3df85f763..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 @@ -402,13 +402,10 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT } try { - if (MtlsUtils.canMtlsBeEnabled( - SystemEnvironmentProvider.getInstance(), SystemPropertyProvider.getInstance(), null)) { + if (!(transportFactory instanceof MtlsHttpTransportFactory) + && MtlsUtils.canMtlsBeEnabled(getEnvironmentProvider(), getPropertyProvider(), null)) { X509Provider x509Provider = - new X509Provider( - SystemEnvironmentProvider.getInstance(), - SystemPropertyProvider.getInstance(), - null); + new X509Provider(getEnvironmentProvider(), getPropertyProvider(), null); KeyStore mtlsKeyStore = x509Provider.getKeyStore(); if (mtlsKeyStore != null) { transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore); @@ -864,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/javatests/com/google/auth/mtls/MtlsUtilsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java index cf066a3c8a83..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 @@ -266,13 +266,6 @@ public String getEnv(String name) { // canMtlsBeEnabled should return true. @Test void canMtlsBeEnabled_allowanceExplicitTrue_withConfig_returnsTrue() throws IOException { - Path configFile = tempDir.resolve("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 = new EnvironmentProvider() { @Override @@ -281,7 +274,7 @@ public String getEnv(String name) { return "true"; } if ("GOOGLE_API_CERTIFICATE_CONFIG".equals(name)) { - return configFile.toString(); + return "testresources/mtls/certificate_config.json"; } return null; } @@ -295,19 +288,12 @@ public String getEnv(String name) { // enabled by default (returns true). @Test void canMtlsBeEnabled_allowanceUnset_withConfig_returnsTrue() throws IOException { - Path configFile = tempDir.resolve("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 = new EnvironmentProvider() { @Override public String getEnv(String name) { if ("GOOGLE_API_CERTIFICATE_CONFIG".equals(name)) { - return configFile.toString(); + return "testresources/mtls/certificate_config.json"; } return null; } @@ -442,6 +428,50 @@ void canMtlsBeEnabled_unset_spiffeCertsPresent_returnsTrue() throws IOException } } + @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("\\", "\\\\") 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> headers = credentials.getRequestMetadata(); + assertNull(headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY)); + + waitForRegionalAccessBoundary(credentials); + + // Second call: should have header. + headers = credentials.getRequestMetadata(); + assertEquals( + headers.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY), + Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION)); + + // Verify that MtlsHttpTransportFactory.create() was called to retrieve the mTLS transport + Mockito.verify(mockMtlsFactory, Mockito.times(2)).create(); + } } diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java index e9bf7c0e7d6a..c0d49530bff2 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java @@ -33,6 +33,7 @@ import static com.google.auth.oauth2.RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY; import static com.google.auth.oauth2.TestUtils.createDummyRab; +import static com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -1892,18 +1893,6 @@ public void refresh_regionalAccessBoundary_selfSignedJWT() credentials.getRegionalAccessBoundary().getEncodedLocations()); } - private void waitForRegionalAccessBoundary(GoogleCredentials credentials) - throws InterruptedException { - long deadline = System.currentTimeMillis() + 5000; - while (credentials.getRegionalAccessBoundary() == null - && System.currentTimeMillis() < deadline) { - Thread.sleep(100); - } - if (credentials.getRegionalAccessBoundary() == null) { - fail("Timed out waiting for regional access boundary refresh"); - } - } - void verifyJwtAccess(Map> metadata, String expectedScopeClaim) throws IOException { assertNotNull(metadata); diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java index 52652a71c458..a0434b514a05 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java @@ -74,4 +74,17 @@ static RegionalAccessBoundary createDummyRab(com.google.api.client.util.Clock cl return new RegionalAccessBoundary( "dummy-locations", java.util.Arrays.asList("dummy-loc"), clock); } + + static void waitForRegionalAccessBoundary(GoogleCredentials credentials) + throws InterruptedException { + long deadline = System.currentTimeMillis() + 5000; + while (credentials.getRegionalAccessBoundary() == null + && System.currentTimeMillis() < deadline) { + Thread.sleep(100); + } + if (credentials.getRegionalAccessBoundary() == null) { + org.junit.jupiter.api.Assertions.fail( + "Timed out waiting for regional access boundary refresh"); + } + } } From 4c8b06114c96885c48ae8989780bc60c796c458a Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 4 Jun 2026 15:02:09 -0700 Subject: [PATCH 5/5] RAB lookup url replacement to mtls is made more robust. --- .../java/com/google/auth/mtls/X509Provider.java | 15 +++++++++------ .../auth/oauth2/RegionalAccessBoundary.java | 5 +---- 2 files changed, 10 insertions(+), 10 deletions(-) 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 f1ebb362e820..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 @@ -103,20 +103,18 @@ public X509Provider() { * *
    *
  • The certificate config override path, if set. - *
  • The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable + *
  • The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable. *
  • The well known gcloud location for the certificate configuration file. *
* + *

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) * @throws IOException if a general I/O error occurs while creating the KeyStore */ - @Override - public boolean isAvailable() throws IOException { - return MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride); - } - @Override public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException { // Attempt to load from resolved Config File @@ -179,4 +177,9 @@ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOEx throw new CertificateSourceUnavailableException("No certificate source was resolved."); } + + @Override + public boolean isAvailable() throws IOException { + return MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride); + } } 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 cd394e75d9b4..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 @@ -190,10 +190,7 @@ static RegionalAccessBoundary refresh( } if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory) { - url = - url.replace( - "https://iamcredentials.googleapis.com/", - "https://iamcredentials.mtls.googleapis.com/"); + url = url.replace("iamcredentials.googleapis.com", "iamcredentials.mtls.googleapis.com"); } HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();