From 88d72e6a383acfa7a752825e016df800b23aca0f Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Sun, 31 May 2026 14:30:12 -0700 Subject: [PATCH 01/24] 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 c87398940538..f82a43dc8743 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 @@ -110,41 +110,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 9744fbc7e07fa82947531d7f5621ae81200d2ac0 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Sun, 31 May 2026 14:51:35 -0700 Subject: [PATCH 02/24] 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 e564c32c97c6..8b7d45d1855f 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 444b35ef0328..16292589233d 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 0f854b250a8316eadfde3c77557a1dc34691ab24 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 4 Jun 2026 01:31:54 -0700 Subject: [PATCH 03/24] 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 f82a43dc8743..8025e04ee5da 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; @@ -121,22 +122,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); @@ -154,20 +164,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 8b7d45d1855f..5cb3a1f2466b 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 16292589233d..98cc669142e6 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 e39f22bbb5c892d577733e0acd42b6874bb30fb2 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 4 Jun 2026 14:51:29 -0700 Subject: [PATCH 04/24] 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 | 2 +- ...lAccountAuthorizedUserCredentialsTest.java | 3 +- .../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 | 1 + .../com/google/auth/oauth2/TestUtils.java | 13 ++++ 17 files changed, 170 insertions(+), 114 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 8025e04ee5da..0fc09af28ac5 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,17 +112,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 = @@ -153,7 +148,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); @@ -180,7 +175,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 5cb3a1f2466b..8b0e660f0998 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 b6de475ae510..578030286587 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; 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 071500f679ee..86d4d95793f4 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; @@ -1344,7 +1344,6 @@ private void waitForRegionalAccessBoundary(GoogleCredentials credentials) 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 db3d1601ed94..36325cb4377b 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -36,6 +36,7 @@ import static com.google.auth.oauth2.OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL; import static com.google.auth.oauth2.OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKLOAD_POOL; import static com.google.auth.oauth2.TestUtils.createDummyRab; +import static com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -58,7 +59,6 @@ import java.math.BigDecimal; import java.net.URI; import java.util.*; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -1516,18 +1516,6 @@ public void refresh_impersonated_workforce_regionalAccessBoundarySuccess() requestHeaders.get(RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY)); } - private void waitForRegionalAccessBoundary(GoogleCredentials credentials) - throws InterruptedException { - long deadline = System.currentTimeMillis() + 5000; - while (credentials.getRegionalAccessBoundary() == null - && System.currentTimeMillis() < deadline) { - Thread.sleep(100); - } - if (credentials.getRegionalAccessBoundary() == null) { - Assertions.fail("Timed out waiting for regional access boundary refresh"); - } - } - private GenericJson buildJsonIdentityPoolCredential() { GenericJson json = new GenericJson(); json.put( diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java index 67d0e990ddda..225a66bce705 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java @@ -32,6 +32,7 @@ package com.google.auth.oauth2; import static com.google.auth.oauth2.RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY; +import static com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary; import static org.junit.Assert.fail; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -1358,18 +1359,6 @@ private GoogleCredentials createTestCredentials(MockTokenServerTransport transpo .build(); } - private void waitForRegionalAccessBoundary(GoogleCredentials credentials) - throws InterruptedException { - long deadline = System.currentTimeMillis() + 5000; - while (credentials.getRegionalAccessBoundary() == null - && System.currentTimeMillis() < deadline) { - Thread.sleep(100); - } - if (credentials.getRegionalAccessBoundary() == null) { - Assertions.fail("Timed out waiting for regional access boundary refresh"); - } - } - private void waitForCooldownActive(GoogleCredentials credentials) throws InterruptedException { long deadline = System.currentTimeMillis() + 5000; while (!credentials.regionalAccessBoundaryManager.isCooldownActive() 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 0bc7fbe467c9..12d37b633f25 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 4bbcaa96c606..db3b4f3b8160 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java @@ -33,6 +33,7 @@ import static com.google.auth.oauth2.RegionalAccessBoundary.X_ALLOWED_LOCATIONS_HEADER_KEY; import static com.google.auth.oauth2.TestUtils.createDummyRab; +import static com.google.auth.oauth2.TestUtils.waitForRegionalAccessBoundary; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java index 52652a71c458..a0434b514a05 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/TestUtils.java @@ -74,4 +74,17 @@ static RegionalAccessBoundary createDummyRab(com.google.api.client.util.Clock cl return new RegionalAccessBoundary( "dummy-locations", java.util.Arrays.asList("dummy-loc"), clock); } + + static void waitForRegionalAccessBoundary(GoogleCredentials credentials) + throws InterruptedException { + long deadline = System.currentTimeMillis() + 5000; + while (credentials.getRegionalAccessBoundary() == null + && System.currentTimeMillis() < deadline) { + Thread.sleep(100); + } + if (credentials.getRegionalAccessBoundary() == null) { + org.junit.jupiter.api.Assertions.fail( + "Timed out waiting for regional access boundary refresh"); + } + } } From cc55e6f39a64c12c2b87489825176c6f7eb3bd32 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 4 Jun 2026 15:02:09 -0700 Subject: [PATCH 05/24] 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 0fc09af28ac5..54030258bf74 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 @@ -101,20 +101,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 @@ -177,4 +175,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 98cc669142e6..c8a32a97af78 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(); From f2776089cbb9bfbeb844bcd30beea5bd57403380 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 10 Jun 2026 19:05:24 -0700 Subject: [PATCH 06/24] Added changes to always call mTLS RAB endpoint. Removed SPIFFE fallback. --- .../java/com/google/auth/mtls/MtlsUtils.java | 64 ++++++--------- .../com/google/auth/mtls/X509Provider.java | 71 +++------------- .../google/auth/oauth2/GoogleCredentials.java | 29 +++++-- .../auth/oauth2/RegionalAccessBoundary.java | 6 +- .../com/google/auth/mtls/MtlsUtilsTest.java | 82 +++---------------- .../google/auth/mtls/X509ProviderTest.java | 49 ----------- .../auth/oauth2/GoogleCredentialsTest.java | 36 ++++++++ 7 files changed, 111 insertions(+), 226 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 545d3f4242ac..376f45d1f2d5 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,16 +51,18 @@ 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"; - + /** + * The policy determining when to use mutual TLS (mTLS) endpoints. + * + *

See AIP-4114 for the specification on mTLS + * endpoint usage. + */ public enum MtlsEndpointUsagePolicy { + /** Always use the mTLS endpoint, and fail if client certificates are not configured. */ ALWAYS, + /** Never use the mTLS endpoint. */ NEVER, + /** Use the mTLS endpoint if client certificates are configured (auto-discovery). */ AUTO } @@ -167,13 +169,21 @@ public static boolean canMtlsBeEnabled( // Check if client certificate usage is allowed String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE"); + MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider); if ("false".equalsIgnoreCase(useClientCertificate)) { + if (policy == MtlsEndpointUsagePolicy.ALWAYS) { + throw new CertificateSourceUnavailableException( + "mTLS is configured to ALWAYS, but client certificate usage was explicitly disabled via GOOGLE_API_USE_CLIENT_CERTIFICATE=false."); + } return false; } - if (getMtlsEndpointUsagePolicy(envProvider) == MtlsEndpointUsagePolicy.NEVER) { + if (policy == MtlsEndpointUsagePolicy.NEVER) { return false; } + if (policy == MtlsEndpointUsagePolicy.ALWAYS) { + return true; + } // Locate and process the certificate configuration file String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE); @@ -185,44 +195,16 @@ public static boolean canMtlsBeEnabled( "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()) { + try { + File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider); + if (wellKnownConfig.isFile()) { return true; } + } catch (IOException e) { + // ignore if well-known directory resolution fails (e.g. APPDATA not set on Windows) } 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 54030258bf74..592aaaf71ba6 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,7 +36,6 @@ 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; @@ -105,9 +104,6 @@ public X509Provider() { *

  • 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) @@ -116,64 +112,19 @@ public X509Provider() { @Override public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException { // 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; - } - } - - 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); - } - } - - // 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); - } - } + WorkloadCertificateConfiguration workloadCertConfig = + MtlsUtils.getWorkloadCertificateConfiguration( + envProvider, propProvider, certConfigPathOverride); - 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); - } - } + 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); } - - throw new CertificateSourceUnavailableException("No certificate source was resolved."); } @Override 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 8b0e660f0998..c8bae49133e2 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 @@ -401,17 +401,27 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT return; } + MtlsUtils.MtlsEndpointUsagePolicy mtlsPolicy = + MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider()); try { - if (!(transportFactory instanceof MtlsHttpTransportFactory) - && MtlsUtils.canMtlsBeEnabled(getEnvironmentProvider(), getPropertyProvider(), null)) { + boolean canMtls = + MtlsUtils.canMtlsBeEnabled(getEnvironmentProvider(), getPropertyProvider(), null); + // Initialize mTLS transport factory if mTLS can be enabled and the user hasn't already + // configured a custom MtlsHttpTransportFactory. + if (!(transportFactory instanceof MtlsHttpTransportFactory) && canMtls) { X509Provider x509Provider = new X509Provider(getEnvironmentProvider(), getPropertyProvider(), null); KeyStore mtlsKeyStore = x509Provider.getKeyStore(); - if (mtlsKeyStore != null) { - transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore); - } + transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore); } } catch (Exception e) { + if (mtlsPolicy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { + if (e instanceof IOException) { + throw (IOException) e; + } + throw new IOException( + "mTLS is configured to ALWAYS, but initialization failed: " + e.getMessage(), e); + } // Graceful fallback to standard transport if mTLS initialization fails } @@ -463,6 +473,10 @@ public Map> getRequestMetadata(URI uri) throws IOException // Sets off an async refresh for request-metadata. refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken()); } catch (IOException e) { + if (MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider()) + == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { + throw e; + } // Ignore failure in async refresh trigger. } return metadata; @@ -493,6 +507,11 @@ public void onSuccess(Map> metadata) { try { refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken()); } catch (IOException e) { + if (MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider()) + == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { + callback.onFailure(e); + return; + } // Ignore failure in async refresh trigger. } callback.onSuccess(metadata); 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 c8a32a97af78..ea543ea5f9c1 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 @@ -45,6 +45,7 @@ import com.google.api.client.util.ExponentialBackOff; import com.google.api.client.util.Key; import com.google.auth.http.HttpTransportFactory; +import com.google.auth.mtls.MtlsUtils; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import java.io.IOException; @@ -189,7 +190,10 @@ static RegionalAccessBoundary refresh( throw new IllegalArgumentException("The provided access token is expired."); } - if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory) { + MtlsUtils.MtlsEndpointUsagePolicy mtlsPolicy = + MtlsUtils.getMtlsEndpointUsagePolicy(SystemEnvironmentProvider.getInstance()); + if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory + || mtlsPolicy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { url = url.replace("iamcredentials.googleapis.com", "iamcredentials.mtls.googleapis.com"); } 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 8531e5615f4b..10bc54019015 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 @@ -351,81 +351,23 @@ 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. @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()); - + void canMtlsBeEnabled_alwaysPolicy_clientCertDisabled_throwsException() { EnvironmentProvider envProvider = - name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null; + name -> { + if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) { + return "false"; + } + if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) { + return "always"; + } + return null; + }; PropertyProvider propProvider = (name, def) -> def; 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; - } + CertificateSourceUnavailableException.class, + () -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); } @Test 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 beec6dafc6cf..cec775faac0b 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 @@ -209,55 +209,6 @@ 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 { 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 225a66bce705..6c3a65b696e5 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 @@ -1232,6 +1232,17 @@ public void regionalAccessBoundary_shouldFailOpenWhenRefreshCannotBeStarted() th assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY)); } + @Test + public void regionalAccessBoundary_alwaysPolicy_missingCertConfig_throwsException() { + TestEnvironmentProvider envProvider = new TestEnvironmentProvider(); + envProvider.setEnv("GOOGLE_API_USE_MTLS_ENDPOINT", "always"); + + GoogleCredentials credentials = + new TestRegionalCredentials(new AccessToken("some-token", null), envProvider); + + assertThrows(IOException.class, () -> credentials.getRequestMetadata()); + } + @Test public void regionalAccessBoundary_deduplicationOfConcurrentRefreshes() throws IOException, InterruptedException { @@ -1382,4 +1393,29 @@ public void advanceTime(long millis) { currentTime.addAndGet(millis); } } + + private static class TestRegionalCredentials extends GoogleCredentials + implements RegionalAccessBoundaryProvider { + private final EnvironmentProvider envProvider; + + TestRegionalCredentials(AccessToken token, EnvironmentProvider envProvider) { + super(GoogleCredentials.newBuilder().setAccessToken(token)); + this.envProvider = envProvider; + } + + @Override + EnvironmentProvider getEnvironmentProvider() { + return envProvider; + } + + @Override + HttpTransportFactory getTransportFactory() { + return DUMMY_TRANSPORT_FACTORY; + } + + @Override + public String getRegionalAccessBoundaryUrl() { + return "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/foo/allowedLocations"; + } + } } From c9bd60b7a191fb1a150efba21cb67c1882572355 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 10 Jun 2026 19:13:28 -0700 Subject: [PATCH 07/24] nit Fixes. --- .../auth/oauth2/ComputeEngineCredentialsTest.java | 12 ------------ 1 file changed, 12 deletions(-) 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 578030286587..c591d64d9e73 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -1312,18 +1312,6 @@ void getRegionalAccessBoundaryUrl_invalidEmail_returnsNull() throws IOException assertNull(credentials.getRegionalAccessBoundaryUrl()); } - private void waitForRegionalAccessBoundary(GoogleCredentials credentials) - throws InterruptedException { - long deadline = System.currentTimeMillis() + 5000; - while (credentials.getRegionalAccessBoundary() == null - && System.currentTimeMillis() < deadline) { - Thread.sleep(100); - } - if (credentials.getRegionalAccessBoundary() == null) { - fail("Timed out waiting for regional access boundary refresh"); - } - } - static class MockMetadataServerTransportFactory implements HttpTransportFactory { MockMetadataServerTransport transport = From 4ba1d2f4b34f8435449f9b1e2b13242e5949415b Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 11 Jun 2026 19:29:54 -0700 Subject: [PATCH 08/24] Moved cert-discovery to a common function. Addressed PR comments. --- .../java/com/google/auth/mtls/MtlsUtils.java | 146 ++++++++++++++---- .../google/auth/oauth2/GoogleCredentials.java | 30 +--- .../auth/oauth2/GoogleCredentialsTest.java | 23 ++- 3 files changed, 144 insertions(+), 55 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 376f45d1f2d5..0894673c2c87 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java @@ -31,14 +31,19 @@ package com.google.auth.mtls; import com.google.api.core.InternalApi; +import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.EnvironmentProvider; +import com.google.auth.oauth2.OAuth2Utils; import com.google.auth.oauth2.PropertyProvider; import com.google.common.base.Strings; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.security.KeyStore; import java.util.Locale; +import java.util.logging.Logger; +import javax.annotation.Nullable; /** * Utility class for mTLS related operations. @@ -47,6 +52,8 @@ */ @InternalApi public class MtlsUtils { + private static final Logger LOGGER = Logger.getLogger(MtlsUtils.class.getName()); + static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG"; static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json"; static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud"; @@ -91,38 +98,27 @@ public static String getCertificatePath( } /** - * Resolves and loads the workload certificate configuration. + * Resolves and parses the workload certificate configuration. * - *

    The configuration file is resolved in the following order of precedence: 1. The provided - * certConfigPathOverride (if not null). 2. The path specified by the - * GOOGLE_API_CERTIFICATE_CONFIG environment variable. 3. The well-known certificate configuration - * file in the gcloud config directory. + *

    This locates the certificate configuration file via {@link #resolveCertificateConfigFile} + * and parses its contents into a {@link WorkloadCertificateConfiguration}. * * @param envProvider the environment provider to use for resolving environment variables * @param propProvider the property provider to use for resolving system properties * @param certConfigPathOverride optional override path for the configuration file * @return the loaded WorkloadCertificateConfiguration - * @throws IOException if the configuration file cannot be found, read, or parsed + * @throws IOException if the configuration file cannot be resolved, read, or parsed */ static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration( EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride) throws IOException { - File certConfig; - if (certConfigPathOverride != null) { - certConfig = new File(certConfigPathOverride); - } else { - String envCredentialsPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE); - if (!Strings.isNullOrEmpty(envCredentialsPath)) { - certConfig = new File(envCredentialsPath); - } else { - certConfig = getWellKnownCertificateConfigFile(envProvider, propProvider); - } - } - - if (!certConfig.isFile()) { + File certConfig = + resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride); + if (certConfig == null) { + File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider); throw new CertificateSourceUnavailableException( "Certificate configuration file does not exist or is not a file: " - + certConfig.getAbsolutePath()); + + wellKnownConfig.getAbsolutePath()); } try (InputStream certConfigStream = new FileInputStream(certConfig)) { return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream); @@ -181,33 +177,76 @@ public static boolean canMtlsBeEnabled( if (policy == MtlsEndpointUsagePolicy.NEVER) { return false; } + if (policy == MtlsEndpointUsagePolicy.ALWAYS) { return true; } - // Locate and process the certificate configuration file + File certConfigFile = + resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride); + return certConfigFile != null; + } + + /** + * Resolves the mutual TLS (mTLS) certificate configuration file. + * + *

    The configuration file is resolved in the following order of precedence: + *

      + *
    1. The developer-provided {@code certConfigPathOverride} (if not null). + *
    2. The path specified by the {@code GOOGLE_API_CERTIFICATE_CONFIG} environment variable. + *
    3. The well-known automatic gcloud workload identity provisioning location (via {@link + * #getWellKnownCertificateConfigFile}). + *
    + * + *

    If an explicit configuration file is specified (via override or environment variable) and it + * is missing or invalid, an exception is thrown. If no explicit file is specified and the default + * well-known file is missing, {@code null} is returned. + * + * @param envProvider the environment provider to use for resolving environment variables + * @param propProvider the property provider to use for resolving system properties + * @param certConfigPathOverride optional override path for the configuration file + * @return the resolved File object, or null if no configuration was found + * @throws IOException if an explicit configuration file is missing or malformed + */ + @Nullable + static File resolveCertificateConfigFile( + EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride) + throws IOException { + // 1. Check explicit developer override + if (certConfigPathOverride != null) { + File certConfigFile = new File(certConfigPathOverride); + if (!certConfigFile.isFile()) { + throw new CertificateSourceUnavailableException( + "Certificate configuration file does not exist or is not a file: " + + certConfigFile.getAbsolutePath()); + } + return certConfigFile; + } + + // 2. Check explicit environment variable String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE); - if (certConfigPathOverride != null || !Strings.isNullOrEmpty(envPath)) { - File certConfigFile = - new File(certConfigPathOverride != null ? certConfigPathOverride : envPath); + if (!Strings.isNullOrEmpty(envPath)) { + File certConfigFile = new File(envPath); if (!certConfigFile.isFile()) { throw new CertificateSourceUnavailableException( "Certificate configuration file does not exist or is not a file: " + certConfigFile.getAbsolutePath()); } - return true; + return certConfigFile; } + // 3. Check optional well-known automatic provisioning location try { File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider); if (wellKnownConfig.isFile()) { - return true; + return wellKnownConfig; } } catch (IOException e) { - // ignore if well-known directory resolution fails (e.g. APPDATA not set on Windows) + LOGGER.info( + "Could not get the mutual TLS (mTLS) client certificate configuration. The library will fall back to making standard non-mTLS requests."); } - return false; + return null; } /** @@ -226,4 +265,55 @@ public static MtlsEndpointUsagePolicy getMtlsEndpointUsagePolicy( } return MtlsEndpointUsagePolicy.AUTO; } + + /** + * Prepares and upgrades the HTTP transport factory for mutual TLS (mTLS) if applicable. + * + * @param baseTransportFactory the base HTTP transport factory to upgrade + * @param envProvider the environment provider to use for resolving environment variables + * @param propProvider the property provider to use for resolving system properties + * @param certConfigPathOverride optional override path for the configuration file + * @return the mTLS-configured HTTP transport factory, or the base factory if mTLS is not enabled + * @throws IOException if mTLS is required/enabled but certificate initialization fails or an + * incompatible transport factory was provided + */ + public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( + HttpTransportFactory baseTransportFactory, + EnvironmentProvider envProvider, + PropertyProvider propProvider, + String certConfigPathOverride) + throws IOException { + + MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy(envProvider); + try { + boolean canMtls = canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride); + if (canMtls) { + if (baseTransportFactory instanceof MtlsHttpTransportFactory) { + // A custom MtlsHttpTransportFactory was already pre-configured by the user. + // Keep using it as-is without re-initializing. + return baseTransportFactory; + } else if (baseTransportFactory == OAuth2Utils.HTTP_TRANSPORT_FACTORY) { + // This is the default HttpTransportFactory assigned by credentials. + // Automatically discover and load client certificates to construct an mTLS factory. + X509Provider x509Provider = + new X509Provider(envProvider, propProvider, certConfigPathOverride); + KeyStore mtlsKeyStore = x509Provider.getKeyStore(); + return new MtlsHttpTransportFactory(mtlsKeyStore); + } else { + // A user configured non-mTLS HttpTransportFactory was explicitly injected. + // Reject it to avoid bypassing mTLS enforcement or overriding the user's factory. + throw new IOException( + "mTLS is enabled on the system, but a user configured non-mTLS HttpTransportFactory was provided: " + + baseTransportFactory.getClass().getName()); + } + } + } catch (Exception e) { + if (mtlsPolicy == MtlsEndpointUsagePolicy.ALWAYS) { + throw new IOException( + "mTLS is configured to ALWAYS, but initialization failed: " + e.getMessage(), e); + } + // Graceful fallback to standard transport if mTLS initialization fails under AUTO policy + } + return baseTransportFactory; + } } 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 c8bae49133e2..ff8f9ce9884d 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,9 +39,7 @@ 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; @@ -54,7 +52,6 @@ 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; @@ -401,29 +398,10 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT return; } - MtlsUtils.MtlsEndpointUsagePolicy mtlsPolicy = - MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider()); - try { - boolean canMtls = - MtlsUtils.canMtlsBeEnabled(getEnvironmentProvider(), getPropertyProvider(), null); - // Initialize mTLS transport factory if mTLS can be enabled and the user hasn't already - // configured a custom MtlsHttpTransportFactory. - if (!(transportFactory instanceof MtlsHttpTransportFactory) && canMtls) { - X509Provider x509Provider = - new X509Provider(getEnvironmentProvider(), getPropertyProvider(), null); - KeyStore mtlsKeyStore = x509Provider.getKeyStore(); - transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore); - } - } catch (Exception e) { - if (mtlsPolicy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { - if (e instanceof IOException) { - throw (IOException) e; - } - throw new IOException( - "mTLS is configured to ALWAYS, but initialization failed: " + e.getMessage(), e); - } - // Graceful fallback to standard transport if mTLS initialization fails - } + // Automatically discover certificates or enforce mTLS policy if applicable + transportFactory = + MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + transportFactory, getEnvironmentProvider(), getPropertyProvider(), null); regionalAccessBoundaryManager.triggerAsyncRefresh( transportFactory, (RegionalAccessBoundaryProvider) this, token); 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 6c3a65b696e5..e1aec86bfba1 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 @@ -1243,6 +1243,18 @@ public void regionalAccessBoundary_alwaysPolicy_missingCertConfig_throwsExceptio assertThrows(IOException.class, () -> credentials.getRequestMetadata()); } + @Test + public void regionalAccessBoundary_alwaysPolicy_userConfiguredNonMtlsFactory_throwsException() { + TestEnvironmentProvider envProvider = new TestEnvironmentProvider(); + envProvider.setEnv("GOOGLE_API_USE_MTLS_ENDPOINT", "always"); + + GoogleCredentials credentials = + new TestRegionalCredentials( + new AccessToken("some-token", null), envProvider, DUMMY_TRANSPORT_FACTORY); + + assertThrows(IOException.class, () -> credentials.getRequestMetadata()); + } + @Test public void regionalAccessBoundary_deduplicationOfConcurrentRefreshes() throws IOException, InterruptedException { @@ -1397,10 +1409,19 @@ public void advanceTime(long millis) { private static class TestRegionalCredentials extends GoogleCredentials implements RegionalAccessBoundaryProvider { private final EnvironmentProvider envProvider; + private final HttpTransportFactory transportFactory; TestRegionalCredentials(AccessToken token, EnvironmentProvider envProvider) { + this(token, envProvider, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + } + + TestRegionalCredentials( + AccessToken token, + EnvironmentProvider envProvider, + HttpTransportFactory transportFactory) { super(GoogleCredentials.newBuilder().setAccessToken(token)); this.envProvider = envProvider; + this.transportFactory = transportFactory; } @Override @@ -1410,7 +1431,7 @@ EnvironmentProvider getEnvironmentProvider() { @Override HttpTransportFactory getTransportFactory() { - return DUMMY_TRANSPORT_FACTORY; + return transportFactory; } @Override From 9bd60fd5f818a61b6d097cbffe53070e20ae0e4a Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Fri, 12 Jun 2026 20:34:02 -0700 Subject: [PATCH 09/24] cache userMtlsPolicy so we read only once other comment addressals. --- .../java/com/google/auth/mtls/MtlsUtils.java | 48 ++++++++++--------- .../com/google/auth/mtls/X509Provider.java | 2 +- .../google/auth/oauth2/GoogleCredentials.java | 10 +--- .../auth/oauth2/RegionalAccessBoundary.java | 10 ++-- .../com/google/auth/mtls/MtlsUtilsTest.java | 36 +++++++------- .../auth/oauth2/GoogleCredentialsTest.java | 22 --------- 6 files changed, 53 insertions(+), 75 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 0894673c2c87..1dd78bdd7cdd 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 @@ -159,7 +159,7 @@ private static File getWellKnownCertificateConfigFile( * @throws IOException if the configuration file is present but contains missing or malformed * files */ - public static boolean canMtlsBeEnabled( + public static boolean canBeEnabled( EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride) throws IOException { @@ -286,34 +286,38 @@ public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy(envProvider); try { - boolean canMtls = canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride); - if (canMtls) { - if (baseTransportFactory instanceof MtlsHttpTransportFactory) { - // A custom MtlsHttpTransportFactory was already pre-configured by the user. - // Keep using it as-is without re-initializing. - return baseTransportFactory; - } else if (baseTransportFactory == OAuth2Utils.HTTP_TRANSPORT_FACTORY) { - // This is the default HttpTransportFactory assigned by credentials. - // Automatically discover and load client certificates to construct an mTLS factory. - X509Provider x509Provider = - new X509Provider(envProvider, propProvider, certConfigPathOverride); - KeyStore mtlsKeyStore = x509Provider.getKeyStore(); - return new MtlsHttpTransportFactory(mtlsKeyStore); - } else { - // A user configured non-mTLS HttpTransportFactory was explicitly injected. - // Reject it to avoid bypassing mTLS enforcement or overriding the user's factory. - throw new IOException( - "mTLS is enabled on the system, but a user configured non-mTLS HttpTransportFactory was provided: " - + baseTransportFactory.getClass().getName()); - } + if (!canBeEnabled(envProvider, propProvider, certConfigPathOverride)) { + return baseTransportFactory; + } + + if (baseTransportFactory instanceof MtlsHttpTransportFactory) { + // A custom MtlsHttpTransportFactory was already pre-configured by the user. + // Keep using it as-is without re-initializing. + return baseTransportFactory; } + + if (baseTransportFactory == OAuth2Utils.HTTP_TRANSPORT_FACTORY) { + // This is the default HttpTransportFactory assigned by credentials. + // Automatically discover and load client certificates to construct an mTLS factory. + X509Provider x509Provider = + new X509Provider(envProvider, propProvider, certConfigPathOverride); + KeyStore mtlsKeyStore = x509Provider.getKeyStore(); + return new MtlsHttpTransportFactory(mtlsKeyStore); + } + + // A user configured non-mTLS HttpTransportFactory was explicitly injected. + // Reject it to avoid bypassing mTLS enforcement or overriding the user's factory. + throw new IOException( + "mTLS is enabled on the system, but a user configured non-mTLS HttpTransportFactory was provided: " + + baseTransportFactory.getClass().getName()); + } catch (Exception e) { if (mtlsPolicy == MtlsEndpointUsagePolicy.ALWAYS) { throw new IOException( "mTLS is configured to ALWAYS, but initialization failed: " + e.getMessage(), e); } // Graceful fallback to standard transport if mTLS initialization fails under AUTO policy + return baseTransportFactory; } - return baseTransportFactory; } } diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java index 592aaaf71ba6..2371682a102d 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 @@ -129,6 +129,6 @@ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOEx @Override public boolean isAvailable() throws IOException { - return MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, certConfigPathOverride); + return MtlsUtils.canBeEnabled(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 ff8f9ce9884d..c48ee4b63863 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 @@ -399,6 +399,7 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT } // Automatically discover certificates or enforce mTLS policy if applicable + // TODO: https://github.com/googleapis/google-cloud-java/issues/13461 transportFactory = MtlsUtils.prepareTransportFactoryIfMtlsEnabled( transportFactory, getEnvironmentProvider(), getPropertyProvider(), null); @@ -451,10 +452,6 @@ public Map> getRequestMetadata(URI uri) throws IOException // Sets off an async refresh for request-metadata. refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken()); } catch (IOException e) { - if (MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider()) - == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { - throw e; - } // Ignore failure in async refresh trigger. } return metadata; @@ -485,11 +482,6 @@ public void onSuccess(Map> metadata) { try { refreshRegionalAccessBoundaryIfExpired(uri, getAccessToken()); } catch (IOException e) { - if (MtlsUtils.getMtlsEndpointUsagePolicy(getEnvironmentProvider()) - == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { - callback.onFailure(e); - return; - } // Ignore failure in async refresh trigger. } callback.onSuccess(metadata); 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 ea543ea5f9c1..008a6990753f 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 @@ -69,6 +69,8 @@ final class RegionalAccessBoundary implements Serializable { static final long TTL_MILLIS = 6 * 60 * 60 * 1000L; // 6 hours static final long REFRESH_THRESHOLD_MILLIS = 1 * 60 * 60 * 1000L; // 1 hour + private static MtlsUtils.MtlsEndpointUsagePolicy userMtlsPolicy = null; + private final String encodedLocations; private final List locations; private final long refreshTime; @@ -190,10 +192,12 @@ static RegionalAccessBoundary refresh( throw new IllegalArgumentException("The provided access token is expired."); } - MtlsUtils.MtlsEndpointUsagePolicy mtlsPolicy = - MtlsUtils.getMtlsEndpointUsagePolicy(SystemEnvironmentProvider.getInstance()); + if (userMtlsPolicy == null) { + userMtlsPolicy = + MtlsUtils.getMtlsEndpointUsagePolicy(SystemEnvironmentProvider.getInstance()); + } if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory - || mtlsPolicy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { + || userMtlsPolicy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { url = url.replace("iamcredentials.googleapis.com", "iamcredentials.mtls.googleapis.com"); } 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 10bc54019015..b10d9d8abd55 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 @@ -244,9 +244,9 @@ 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. + // If client certificate usage is explicitly disabled, canBeEnabled should return false. @Test - void canMtlsBeEnabled_allowanceExplicitFalse_returnsFalse() throws IOException { + void canBeEnabled_allowanceExplicitFalse_returnsFalse() throws IOException { EnvironmentProvider envProvider = new EnvironmentProvider() { @Override @@ -259,13 +259,13 @@ public String getEnv(String name) { }; PropertyProvider propProvider = (name, def) -> def; - assertFalse(MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); + assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); } // If client certificate usage is explicitly enabled and a valid configuration is present, - // canMtlsBeEnabled should return true. + // canBeEnabled should return true. @Test - void canMtlsBeEnabled_allowanceExplicitTrue_withConfig_returnsTrue() throws IOException { + void canBeEnabled_allowanceExplicitTrue_withConfig_returnsTrue() throws IOException { EnvironmentProvider envProvider = new EnvironmentProvider() { @Override @@ -281,13 +281,13 @@ public String getEnv(String name) { }; PropertyProvider propProvider = (name, def) -> def; - assertTrue(MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); + assertTrue(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); } // If client certificate usage is unset but a valid configuration is present, mTLS should be // enabled by default (returns true). @Test - void canMtlsBeEnabled_allowanceUnset_withConfig_returnsTrue() throws IOException { + void canBeEnabled_allowanceUnset_withConfig_returnsTrue() throws IOException { EnvironmentProvider envProvider = new EnvironmentProvider() { @Override @@ -300,13 +300,13 @@ public String getEnv(String name) { }; PropertyProvider propProvider = (name, def) -> def; - assertTrue(MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); + assertTrue(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); } // If the GOOGLE_API_CERTIFICATE_CONFIG environment variable points to a non-existent file, - // canMtlsBeEnabled should throw an IOException. + // canBeEnabled should throw an IOException. @Test - void canMtlsBeEnabled_envVarConfigMissingFile_throwsIOException() throws IOException { + void canBeEnabled_envVarConfigMissingFile_throwsIOException() throws IOException { Path nonExistentConfig = tempDir.resolve("non_existent.json"); EnvironmentProvider envProvider = new EnvironmentProvider() { @@ -321,13 +321,13 @@ public String getEnv(String name) { PropertyProvider propProvider = (name, def) -> def; assertThrows( - IOException.class, () -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); + IOException.class, () -> MtlsUtils.canBeEnabled(envProvider, propProvider, null)); } - // If the well-known gcloud certificate configuration file exists, canMtlsBeEnabled should return + // If the well-known gcloud certificate configuration file exists, canBeEnabled should return // true. @Test - void canMtlsBeEnabled_wellKnownConfigExists_returnsTrue() throws IOException { + void canBeEnabled_wellKnownConfigExists_returnsTrue() throws IOException { Path gcloudDir = tempDir.resolve(".config/gcloud"); Files.createDirectories(gcloudDir); Path configFile = gcloudDir.resolve("certificate_config.json"); @@ -348,11 +348,11 @@ public String getProperty(String name, String def) { } }; - assertTrue(MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); + assertTrue(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); } @Test - void canMtlsBeEnabled_alwaysPolicy_clientCertDisabled_throwsException() { + void canBeEnabled_alwaysPolicy_clientCertDisabled_throwsException() { EnvironmentProvider envProvider = name -> { if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) { @@ -367,7 +367,7 @@ void canMtlsBeEnabled_alwaysPolicy_clientCertDisabled_throwsException() { assertThrows( CertificateSourceUnavailableException.class, - () -> MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); + () -> MtlsUtils.canBeEnabled(envProvider, propProvider, null)); } @Test @@ -395,7 +395,7 @@ void getMtlsEndpointUsagePolicy_auto() { } @Test - void canMtlsBeEnabled_policyNever_returnsFalse() throws IOException { + void canBeEnabled_policyNever_returnsFalse() throws IOException { EnvironmentProvider envProvider = new EnvironmentProvider() { @Override @@ -411,7 +411,7 @@ public String getEnv(String name) { }; PropertyProvider propProvider = (name, def) -> def; - assertFalse(MtlsUtils.canMtlsBeEnabled(envProvider, propProvider, null)); + assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); } private String createJsonConfigString(Path certPath, Path keyPath) { 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 e1aec86bfba1..696147acc567 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 @@ -1232,28 +1232,6 @@ public void regionalAccessBoundary_shouldFailOpenWhenRefreshCannotBeStarted() th assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY)); } - @Test - public void regionalAccessBoundary_alwaysPolicy_missingCertConfig_throwsException() { - TestEnvironmentProvider envProvider = new TestEnvironmentProvider(); - envProvider.setEnv("GOOGLE_API_USE_MTLS_ENDPOINT", "always"); - - GoogleCredentials credentials = - new TestRegionalCredentials(new AccessToken("some-token", null), envProvider); - - assertThrows(IOException.class, () -> credentials.getRequestMetadata()); - } - - @Test - public void regionalAccessBoundary_alwaysPolicy_userConfiguredNonMtlsFactory_throwsException() { - TestEnvironmentProvider envProvider = new TestEnvironmentProvider(); - envProvider.setEnv("GOOGLE_API_USE_MTLS_ENDPOINT", "always"); - - GoogleCredentials credentials = - new TestRegionalCredentials( - new AccessToken("some-token", null), envProvider, DUMMY_TRANSPORT_FACTORY); - - assertThrows(IOException.class, () -> credentials.getRequestMetadata()); - } @Test public void regionalAccessBoundary_deduplicationOfConcurrentRefreshes() From e6d3be9724d472800c8f98f14c893a2864d36540 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Fri, 12 Jun 2026 20:34:29 -0700 Subject: [PATCH 10/24] lint fixes. --- .../oauth2_http/java/com/google/auth/mtls/MtlsUtils.java | 5 +++-- .../javatests/com/google/auth/mtls/MtlsUtilsTest.java | 3 +-- .../com/google/auth/oauth2/GoogleCredentialsTest.java | 5 +---- 3 files changed, 5 insertions(+), 8 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 1dd78bdd7cdd..a886686165b2 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 @@ -191,6 +191,7 @@ public static boolean canBeEnabled( * Resolves the mutual TLS (mTLS) certificate configuration file. * *

    The configuration file is resolved in the following order of precedence: + * *

      *
    1. The developer-provided {@code certConfigPathOverride} (if not null). *
    2. The path specified by the {@code GOOGLE_API_CERTIFICATE_CONFIG} environment variable. @@ -295,7 +296,7 @@ public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( // Keep using it as-is without re-initializing. return baseTransportFactory; } - + if (baseTransportFactory == OAuth2Utils.HTTP_TRANSPORT_FACTORY) { // This is the default HttpTransportFactory assigned by credentials. // Automatically discover and load client certificates to construct an mTLS factory. @@ -304,7 +305,7 @@ public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( KeyStore mtlsKeyStore = x509Provider.getKeyStore(); return new MtlsHttpTransportFactory(mtlsKeyStore); } - + // A user configured non-mTLS HttpTransportFactory was explicitly injected. // Reject it to avoid bypassing mTLS enforcement or overriding the user's factory. throw new IOException( 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 b10d9d8abd55..d792cb6de6d3 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 @@ -320,8 +320,7 @@ public String getEnv(String name) { }; PropertyProvider propProvider = (name, def) -> def; - assertThrows( - IOException.class, () -> MtlsUtils.canBeEnabled(envProvider, propProvider, null)); + assertThrows(IOException.class, () -> MtlsUtils.canBeEnabled(envProvider, propProvider, null)); } // If the well-known gcloud certificate configuration file exists, canBeEnabled should return 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 696147acc567..5b9652528bc2 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 @@ -1232,7 +1232,6 @@ public void regionalAccessBoundary_shouldFailOpenWhenRefreshCannotBeStarted() th assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY)); } - @Test public void regionalAccessBoundary_deduplicationOfConcurrentRefreshes() throws IOException, InterruptedException { @@ -1394,9 +1393,7 @@ private static class TestRegionalCredentials extends GoogleCredentials } TestRegionalCredentials( - AccessToken token, - EnvironmentProvider envProvider, - HttpTransportFactory transportFactory) { + AccessToken token, EnvironmentProvider envProvider, HttpTransportFactory transportFactory) { super(GoogleCredentials.newBuilder().setAccessToken(token)); this.envProvider = envProvider; this.transportFactory = transportFactory; From dcb4c1812106a69b7c541e60da4e0396497a4867 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 24 Jun 2026 16:13:28 -0700 Subject: [PATCH 11/24] canMtlsBeEnabled changes. --- .../oauth2_http/java/com/google/auth/mtls/MtlsUtils.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 a886686165b2..a1b9d336eff6 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 @@ -287,16 +287,16 @@ public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy(envProvider); try { - if (!canBeEnabled(envProvider, propProvider, certConfigPathOverride)) { - return baseTransportFactory; - } - if (baseTransportFactory instanceof MtlsHttpTransportFactory) { // A custom MtlsHttpTransportFactory was already pre-configured by the user. // Keep using it as-is without re-initializing. return baseTransportFactory; } + if (!canBeEnabled(envProvider, propProvider, certConfigPathOverride)) { + return baseTransportFactory; + } + if (baseTransportFactory == OAuth2Utils.HTTP_TRANSPORT_FACTORY) { // This is the default HttpTransportFactory assigned by credentials. // Automatically discover and load client certificates to construct an mTLS factory. From afde0d39cce013034d1e381330c1fcc54bfd67d9 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 24 Jun 2026 16:18:29 -0700 Subject: [PATCH 12/24] userMtlsPolicy DCL lock check for concurrency. --- .../com/google/auth/oauth2/RegionalAccessBoundary.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 008a6990753f..e8ea3bc183f2 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 @@ -69,7 +69,7 @@ final class RegionalAccessBoundary implements Serializable { static final long TTL_MILLIS = 6 * 60 * 60 * 1000L; // 6 hours static final long REFRESH_THRESHOLD_MILLIS = 1 * 60 * 60 * 1000L; // 1 hour - private static MtlsUtils.MtlsEndpointUsagePolicy userMtlsPolicy = null; + private static volatile MtlsUtils.MtlsEndpointUsagePolicy userMtlsPolicy = null; private final String encodedLocations; private final List locations; @@ -193,8 +193,12 @@ static RegionalAccessBoundary refresh( } if (userMtlsPolicy == null) { - userMtlsPolicy = - MtlsUtils.getMtlsEndpointUsagePolicy(SystemEnvironmentProvider.getInstance()); + synchronized (RegionalAccessBoundary.class) { + if (userMtlsPolicy == null) { + userMtlsPolicy = + MtlsUtils.getMtlsEndpointUsagePolicy(SystemEnvironmentProvider.getInstance()); + } + } } if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory || userMtlsPolicy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { From b65c895c92230fa8fe546d21b51e7cf7c2819d9c Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Tue, 30 Jun 2026 18:55:44 -0700 Subject: [PATCH 13/24] fix: address technical review findings for regional access boundary mTLS refresh - MtlsUtils: - Validate custom transport factory outside try-block to prevent swallowing exceptions. - Add null check for baseTransportFactory to prevent NullPointerException. - Wrap getWellKnownCertificateConfigFile call to enforce the exception contract. - Use case-insensitive matching for GOOGLE_API_USE_MTLS_ENDPOINT. - RegionalAccessBoundary & Manager: - Remove JVM-wide userMtlsPolicy static cache to prevent test pollution. - Inject EnvironmentProvider dynamically to refresh methods. - MockExternalAccountCredentialsTransport: - Strip .mtls. subdomain before looking up regional access boundaries. - Define host-only IAM_ENDPOINT and MTLS_IAM_ENDPOINT constants. - GoogleCredentialsTest: - Add test asserting boundary refresh hits .mtls. subdomain when required. - oauth2_http/pom.xml: - Set GOOGLE_API_USE_CLIENT_CERTIFICATE=false in surefire config to isolate tests. --- .../java/com/google/auth/mtls/MtlsUtils.java | 59 ++++++++++-------- .../google/auth/oauth2/GoogleCredentials.java | 2 +- .../auth/oauth2/RegionalAccessBoundary.java | 16 ++--- .../oauth2/RegionalAccessBoundaryManager.java | 10 +++- .../auth/oauth2/GoogleCredentialsTest.java | 60 +++++++++++++++++++ ...ckExternalAccountCredentialsTransport.java | 10 ++-- .../oauth2/RegionalAccessBoundaryTest.java | 15 +++-- 7 files changed, 125 insertions(+), 47 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 a1b9d336eff6..35e3c003f397 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 @@ -115,10 +115,18 @@ static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration( File certConfig = resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride); if (certConfig == null) { - File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider); - throw new CertificateSourceUnavailableException( - "Certificate configuration file does not exist or is not a file: " - + wellKnownConfig.getAbsolutePath()); + try { + File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider); + throw new CertificateSourceUnavailableException( + "Certificate configuration file does not exist or is not a file: " + + wellKnownConfig.getAbsolutePath()); + } catch (IOException e) { + if (e instanceof CertificateSourceUnavailableException) { + throw (CertificateSourceUnavailableException) e; + } + throw new CertificateSourceUnavailableException( + "Failed to get well-known certificate config file path", e); + } } try (InputStream certConfigStream = new FileInputStream(certConfig)) { return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream); @@ -259,9 +267,9 @@ static File resolveCertificateConfigFile( public static MtlsEndpointUsagePolicy getMtlsEndpointUsagePolicy( EnvironmentProvider envProvider) { String mtlsEndpointUsagePolicy = envProvider.getEnv("GOOGLE_API_USE_MTLS_ENDPOINT"); - if ("never".equals(mtlsEndpointUsagePolicy)) { + if ("never".equalsIgnoreCase(mtlsEndpointUsagePolicy)) { return MtlsEndpointUsagePolicy.NEVER; - } else if ("always".equals(mtlsEndpointUsagePolicy)) { + } else if ("always".equalsIgnoreCase(mtlsEndpointUsagePolicy)) { return MtlsEndpointUsagePolicy.ALWAYS; } return MtlsEndpointUsagePolicy.AUTO; @@ -285,33 +293,36 @@ public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( String certConfigPathOverride) throws IOException { - MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy(envProvider); - try { - if (baseTransportFactory instanceof MtlsHttpTransportFactory) { - // A custom MtlsHttpTransportFactory was already pre-configured by the user. - // Keep using it as-is without re-initializing. - return baseTransportFactory; - } + if (baseTransportFactory == null) { + return null; + } - if (!canBeEnabled(envProvider, propProvider, certConfigPathOverride)) { - return baseTransportFactory; - } + if (baseTransportFactory instanceof MtlsHttpTransportFactory) { + // A custom MtlsHttpTransportFactory was already pre-configured by the user. + // Keep using it as-is without re-initializing. + return baseTransportFactory; + } - if (baseTransportFactory == OAuth2Utils.HTTP_TRANSPORT_FACTORY) { - // This is the default HttpTransportFactory assigned by credentials. - // Automatically discover and load client certificates to construct an mTLS factory. - X509Provider x509Provider = - new X509Provider(envProvider, propProvider, certConfigPathOverride); - KeyStore mtlsKeyStore = x509Provider.getKeyStore(); - return new MtlsHttpTransportFactory(mtlsKeyStore); - } + if (!canBeEnabled(envProvider, propProvider, certConfigPathOverride)) { + return baseTransportFactory; + } + if (baseTransportFactory != OAuth2Utils.HTTP_TRANSPORT_FACTORY) { // A user configured non-mTLS HttpTransportFactory was explicitly injected. // Reject it to avoid bypassing mTLS enforcement or overriding the user's factory. throw new IOException( "mTLS is enabled on the system, but a user configured non-mTLS HttpTransportFactory was provided: " + baseTransportFactory.getClass().getName()); + } + MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy(envProvider); + try { + // This is the default HttpTransportFactory assigned by credentials. + // Automatically discover and load client certificates to construct an mTLS factory. + X509Provider x509Provider = + new X509Provider(envProvider, propProvider, certConfigPathOverride); + KeyStore mtlsKeyStore = x509Provider.getKeyStore(); + return new MtlsHttpTransportFactory(mtlsKeyStore); } catch (Exception e) { if (mtlsPolicy == MtlsEndpointUsagePolicy.ALWAYS) { throw new IOException( 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 c48ee4b63863..23472323be04 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 @@ -405,7 +405,7 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT transportFactory, getEnvironmentProvider(), getPropertyProvider(), null); regionalAccessBoundaryManager.triggerAsyncRefresh( - transportFactory, (RegionalAccessBoundaryProvider) this, token); + transportFactory, (RegionalAccessBoundaryProvider) this, token, getEnvironmentProvider()); } /** 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 e8ea3bc183f2..7455bd290d48 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 @@ -69,7 +69,7 @@ final class RegionalAccessBoundary implements Serializable { static final long TTL_MILLIS = 6 * 60 * 60 * 1000L; // 6 hours static final long REFRESH_THRESHOLD_MILLIS = 1 * 60 * 60 * 1000L; // 1 hour - private static volatile MtlsUtils.MtlsEndpointUsagePolicy userMtlsPolicy = null; + private final String encodedLocations; private final List locations; @@ -184,7 +184,8 @@ static RegionalAccessBoundary refresh( String url, AccessToken accessToken, Clock clock, - int maxRetryElapsedTimeMillis) + int maxRetryElapsedTimeMillis, + EnvironmentProvider envProvider) throws IOException { Preconditions.checkNotNull(accessToken, "The provided access token is null."); if (accessToken.getExpirationTimeMillis() != null @@ -192,16 +193,9 @@ static RegionalAccessBoundary refresh( throw new IllegalArgumentException("The provided access token is expired."); } - if (userMtlsPolicy == null) { - synchronized (RegionalAccessBoundary.class) { - if (userMtlsPolicy == null) { - userMtlsPolicy = - MtlsUtils.getMtlsEndpointUsagePolicy(SystemEnvironmentProvider.getInstance()); - } - } - } + MtlsUtils.MtlsEndpointUsagePolicy policy = MtlsUtils.getMtlsEndpointUsagePolicy(envProvider); if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory - || userMtlsPolicy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { + || policy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { url = url.replace("iamcredentials.googleapis.com", "iamcredentials.mtls.googleapis.com"); } diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java index ee5d9511d755..171c25798cad 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java @@ -177,7 +177,8 @@ void setCachedRAB(RegionalAccessBoundary rab) { void triggerAsyncRefresh( final HttpTransportFactory transportFactory, final RegionalAccessBoundaryProvider provider, - final AccessToken accessToken) { + final AccessToken accessToken, + final EnvironmentProvider envProvider) { if (skipRAB.get() || isCooldownActive()) { return; } @@ -201,7 +202,12 @@ void triggerAsyncRefresh( } RegionalAccessBoundary newRAB = RegionalAccessBoundary.refresh( - transportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis); + transportFactory, + url, + accessToken, + clock, + maxRetryElapsedTimeMillis, + envProvider); cachedRAB.set(newRAB); resetCooldown(); } catch (Throwable e) { 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 5b9652528bc2..84dc26b4c1be 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 @@ -1370,6 +1370,66 @@ private void waitForCooldownActive(GoogleCredentials credentials) throws Interru } } + @Test + public void regionalAccessBoundary_refreshUsesMtlsEndpointWhenConfigured() + throws IOException, InterruptedException { + MockTokenServerTransport transport = new MockTokenServerTransport(); + transport.setRegionalAccessBoundary( + new RegionalAccessBoundary("valid", Collections.singletonList("us-central1"), null)); + + // Configure the environment provider to enable mTLS with ALWAYS policy + EnvironmentProvider envProvider = + new EnvironmentProvider() { + @Override + public String getEnv(String name) { + if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) { + return "true"; + } + if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) { + return "always"; + } + return null; + } + }; + + AccessToken token = + new AccessToken( + "test-token", new java.util.Date(System.currentTimeMillis() + 3600000L)); + + com.google.auth.mtls.MtlsHttpTransportFactory mockMtlsFactory; + try { + java.security.KeyStore dummyKeyStore = + java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType()); + dummyKeyStore.load(null, null); + mockMtlsFactory = + new com.google.auth.mtls.MtlsHttpTransportFactory(dummyKeyStore) { + @Override + public com.google.api.client.http.HttpTransport create() { + return transport; + } + }; + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException(e); + } + + TestRegionalCredentials credentials = + new TestRegionalCredentials(token, envProvider, mockMtlsFactory); + + credentials.getRequestMetadata(URI.create("https://foo.com")); + + // Wait for the async refresh to complete + waitForRegionalAccessBoundary(credentials); + + // Verify a request was made + assertEquals(1, transport.getRegionalAccessBoundaryRequestCount()); + // Verify the request URL used the mTLS subdomain: "iamcredentials.mtls.googleapis.com" + String requestedUrl = transport.getRequest().getUrl(); + assertNotNull(requestedUrl); + assertTrue( + requestedUrl.contains("iamcredentials.mtls.googleapis.com"), + "Expected mTLS URL, but got: " + requestedUrl); + } + private static class TestClock implements Clock { private final AtomicLong currentTime = new AtomicLong(System.currentTimeMillis()); 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 9ae6b67cc294..58f1a0ab0af1 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java @@ -84,7 +84,8 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport { static final String SERVICE_ACCOUNT_IMPERSONATION_URL = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/testn@test.iam.gserviceaccount.com:generateAccessToken"; - static final String IAM_ENDPOINT = "https://iamcredentials.googleapis.com"; + static final String IAM_ENDPOINT = "iamcredentials.googleapis.com"; + static final String MTLS_IAM_ENDPOINT = "iamcredentials.mtls.googleapis.com"; private final Queue responseSequence = new ArrayDeque<>(); private final Queue responseErrorSequence = new ArrayDeque<>(); @@ -202,11 +203,12 @@ public LowLevelHttpResponse execute() throws IOException { .setContent(response.toPrettyString()); } - if (url.contains("iamcredentials.googleapis.com") - || url.contains("iamcredentials.mtls.googleapis.com")) { + if (url.contains(IAM_ENDPOINT) + || url.contains(MTLS_IAM_ENDPOINT)) { if (url.endsWith(REGIONAL_ACCESS_BOUNDARY_URL_END)) { - RegionalAccessBoundary rab = regionalAccessBoundaries.get(url); + String normalizedUrl = url.replace(MTLS_IAM_ENDPOINT, IAM_ENDPOINT); + RegionalAccessBoundary rab = regionalAccessBoundaries.get(normalizedUrl); if (rab == null) { rab = new RegionalAccessBoundary( diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/RegionalAccessBoundaryTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/RegionalAccessBoundaryTest.java index 12d37b633f25..8ee99fd70ca0 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 @@ -150,7 +150,8 @@ public void testRefreshClosesResponse() throws Exception { HttpTransportFactory transportFactory = () -> transport; RegionalAccessBoundary rab = - RegionalAccessBoundary.refresh(transportFactory, url, token, testClock, 1000); + RegionalAccessBoundary.refresh( + transportFactory, url, token, testClock, 1000, SystemEnvironmentProvider.getInstance()); assertEquals("encoded", rab.getEncodedLocations()); assertTrue(mockResponse.isDisconnected(), "Response should have been disconnected"); @@ -182,7 +183,8 @@ public void testManagerTriggersRefreshInGracePeriod() throws InterruptedExceptio RegionalAccessBoundaryManager manager = new RegionalAccessBoundaryManager(testClock); // 1. Let's first get a RAB into the cache - manager.triggerAsyncRefresh(transportFactory, provider, token); + manager.triggerAsyncRefresh( + transportFactory, provider, token, SystemEnvironmentProvider.getInstance()); // Wait for it to be cached int retries = 0; @@ -213,7 +215,8 @@ public void testManagerTriggersRefreshInGracePeriod() throws InterruptedExceptio HttpTransportFactory transportFactory2 = () -> transport2; // 4. Trigger refresh - should start because we are in grace period - manager.triggerAsyncRefresh(transportFactory2, provider, token); + manager.triggerAsyncRefresh( + transportFactory2, provider, token, SystemEnvironmentProvider.getInstance()); // 5. Wait for background refresh to complete // We expect the cached RAB to eventually change to newerEncoded @@ -297,7 +300,8 @@ public int read() throws java.io.IOException { testClock, RegionalAccessBoundaryManager.DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS, testExecutor); - managers[i].triggerAsyncRefresh(transportFactory, provider, token); + managers[i].triggerAsyncRefresh( + transportFactory, provider, token, SystemEnvironmentProvider.getInstance()); } RegionalAccessBoundaryManager extraManager = @@ -307,7 +311,8 @@ public int read() throws java.io.IOException { testExecutor); assertFalse(extraManager.isCooldownActive()); - extraManager.triggerAsyncRefresh(transportFactory, provider, token); + extraManager.triggerAsyncRefresh( + transportFactory, provider, token, SystemEnvironmentProvider.getInstance()); assertTrue( extraManager.isCooldownActive(), From 579f4bff79ea85eeae0e9175c92cdcb0effdbf01 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Tue, 30 Jun 2026 18:56:10 -0700 Subject: [PATCH 14/24] lint fixes. --- .../java/com/google/auth/oauth2/RegionalAccessBoundary.java | 2 -- .../com/google/auth/oauth2/GoogleCredentialsTest.java | 3 +-- .../auth/oauth2/MockExternalAccountCredentialsTransport.java | 3 +-- 3 files changed, 2 insertions(+), 6 deletions(-) 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 7455bd290d48..f22e47e2565b 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 @@ -69,8 +69,6 @@ final class RegionalAccessBoundary implements Serializable { static final long TTL_MILLIS = 6 * 60 * 60 * 1000L; // 6 hours static final long REFRESH_THRESHOLD_MILLIS = 1 * 60 * 60 * 1000L; // 1 hour - - private final String encodedLocations; private final List locations; private final long refreshTime; 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 84dc26b4c1be..4cb19c837d52 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java @@ -1393,8 +1393,7 @@ public String getEnv(String name) { }; AccessToken token = - new AccessToken( - "test-token", new java.util.Date(System.currentTimeMillis() + 3600000L)); + new AccessToken("test-token", new java.util.Date(System.currentTimeMillis() + 3600000L)); com.google.auth.mtls.MtlsHttpTransportFactory mockMtlsFactory; try { 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 58f1a0ab0af1..fa1e8884a401 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java @@ -203,8 +203,7 @@ public LowLevelHttpResponse execute() throws IOException { .setContent(response.toPrettyString()); } - if (url.contains(IAM_ENDPOINT) - || url.contains(MTLS_IAM_ENDPOINT)) { + if (url.contains(IAM_ENDPOINT) || url.contains(MTLS_IAM_ENDPOINT)) { if (url.endsWith(REGIONAL_ACCESS_BOUNDARY_URL_END)) { String normalizedUrl = url.replace(MTLS_IAM_ENDPOINT, IAM_ENDPOINT); From 76a22bb5b6ebb0a3501aa761cc9ab59aaee7851c Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 1 Jul 2026 16:27:31 -0700 Subject: [PATCH 15/24] Changed check to enable mTLS so we only do it once every ~5 hours. --- .../google/auth/oauth2/GoogleCredentials.java | 12 ++++------ .../oauth2/RegionalAccessBoundaryManager.java | 8 +++++-- .../oauth2/RegionalAccessBoundaryTest.java | 24 +++++++++++++++---- 3 files changed, 31 insertions(+), 13 deletions(-) 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 23472323be04..f27d26956478 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 @@ -398,14 +398,12 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT return; } - // Automatically discover certificates or enforce mTLS policy if applicable - // TODO: https://github.com/googleapis/google-cloud-java/issues/13461 - transportFactory = - MtlsUtils.prepareTransportFactoryIfMtlsEnabled( - transportFactory, getEnvironmentProvider(), getPropertyProvider(), null); - regionalAccessBoundaryManager.triggerAsyncRefresh( - transportFactory, (RegionalAccessBoundaryProvider) this, token, getEnvironmentProvider()); + transportFactory, + (RegionalAccessBoundaryProvider) this, + token, + getEnvironmentProvider(), + getPropertyProvider()); } /** diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java index 171c25798cad..68dd87e67583 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java @@ -178,7 +178,8 @@ void triggerAsyncRefresh( final HttpTransportFactory transportFactory, final RegionalAccessBoundaryProvider provider, final AccessToken accessToken, - final EnvironmentProvider envProvider) { + final EnvironmentProvider envProvider, + final PropertyProvider propProvider) { if (skipRAB.get() || isCooldownActive()) { return; } @@ -200,9 +201,12 @@ void triggerAsyncRefresh( skipRAB.set(true); return; } + HttpTransportFactory upgradedTransportFactory = + com.google.auth.mtls.MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + transportFactory, envProvider, propProvider, null); RegionalAccessBoundary newRAB = RegionalAccessBoundary.refresh( - transportFactory, + upgradedTransportFactory, url, accessToken, clock, 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 8ee99fd70ca0..11143eabe928 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 @@ -184,7 +184,11 @@ public void testManagerTriggersRefreshInGracePeriod() throws InterruptedExceptio // 1. Let's first get a RAB into the cache manager.triggerAsyncRefresh( - transportFactory, provider, token, SystemEnvironmentProvider.getInstance()); + transportFactory, + provider, + token, + SystemEnvironmentProvider.getInstance(), + SystemPropertyProvider.getInstance()); // Wait for it to be cached int retries = 0; @@ -216,7 +220,11 @@ public void testManagerTriggersRefreshInGracePeriod() throws InterruptedExceptio // 4. Trigger refresh - should start because we are in grace period manager.triggerAsyncRefresh( - transportFactory2, provider, token, SystemEnvironmentProvider.getInstance()); + transportFactory2, + provider, + token, + SystemEnvironmentProvider.getInstance(), + SystemPropertyProvider.getInstance()); // 5. Wait for background refresh to complete // We expect the cached RAB to eventually change to newerEncoded @@ -301,7 +309,11 @@ public int read() throws java.io.IOException { RegionalAccessBoundaryManager.DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS, testExecutor); managers[i].triggerAsyncRefresh( - transportFactory, provider, token, SystemEnvironmentProvider.getInstance()); + transportFactory, + provider, + token, + SystemEnvironmentProvider.getInstance(), + SystemPropertyProvider.getInstance()); } RegionalAccessBoundaryManager extraManager = @@ -312,7 +324,11 @@ public int read() throws java.io.IOException { assertFalse(extraManager.isCooldownActive()); extraManager.triggerAsyncRefresh( - transportFactory, provider, token, SystemEnvironmentProvider.getInstance()); + transportFactory, + provider, + token, + SystemEnvironmentProvider.getInstance(), + SystemPropertyProvider.getInstance()); assertTrue( extraManager.isCooldownActive(), From 3fed9f750a93acc9e072960528cd86467ec22ff7 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 1 Jul 2026 16:30:17 -0700 Subject: [PATCH 16/24] lint fixes. --- .../java/com/google/auth/oauth2/GoogleCredentials.java | 1 - 1 file changed, 1 deletion(-) 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 f27d26956478..ed9b4d5a7b3f 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,7 +39,6 @@ import com.google.auth.RequestMetadataCallback; import com.google.auth.http.AuthHttpConstants; import com.google.auth.http.HttpTransportFactory; -import com.google.auth.mtls.MtlsUtils; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; From d6331d9d61565c656b8f9730d87bc5954b759fa9 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Tue, 7 Jul 2026 19:00:01 -0700 Subject: [PATCH 17/24] feat(mtls): support custom HttpTransportFactory with mTLS and improve exception wrapping - MtlsUtils: - Trust and preserve developer's custom HttpTransportFactory when mTLS is enabled instead of throwing IOException. - X509Provider: - Move workload certificate configuration resolution inside the try-catch block to properly wrap certificate source errors into IOException. - RegionalAccessBoundary & Manager: - Move subdomain substitution (.mtls.) logic to RegionalAccessBoundaryManager, gating it on MtlsUtils.canBeEnabled. - Simplify RegionalAccessBoundary.refresh signature. - Tests: - Add unit tests verifying custom HttpTransportFactory retention under AUTO and ALWAYS policies. - Add tests validating proper wrapping of JSON parsing failures in X509Provider. --- .../java/com/google/auth/mtls/MtlsUtils.java | 8 +- .../com/google/auth/mtls/X509Provider.java | 25 ++-- .../auth/oauth2/RegionalAccessBoundary.java | 10 +- .../oauth2/RegionalAccessBoundaryManager.java | 12 +- .../com/google/auth/mtls/MtlsUtilsTest.java | 137 ++++++++++++++++++ .../google/auth/mtls/X509ProviderTest.java | 59 ++++++-- .../oauth2/RegionalAccessBoundaryTest.java | 3 +- 7 files changed, 208 insertions(+), 46 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 35e3c003f397..6167f99793b9 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 @@ -308,11 +308,9 @@ public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( } if (baseTransportFactory != OAuth2Utils.HTTP_TRANSPORT_FACTORY) { - // A user configured non-mTLS HttpTransportFactory was explicitly injected. - // Reject it to avoid bypassing mTLS enforcement or overriding the user's factory. - throw new IOException( - "mTLS is enabled on the system, but a user configured non-mTLS HttpTransportFactory was provided: " - + baseTransportFactory.getClass().getName()); + // A user configured HttpTransportFactory was explicitly injected. + // Trust the developer's custom factory and return it as-is. + return baseTransportFactory; } MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy(envProvider); 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 2371682a102d..6e42e14e995e 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 @@ -111,17 +111,22 @@ public X509Provider() { */ @Override public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException { - // Attempt to load from resolved Config File - WorkloadCertificateConfiguration workloadCertConfig = - MtlsUtils.getWorkloadCertificateConfiguration( - envProvider, propProvider, certConfigPathOverride); + try { + // Attempt to load from resolved Config File + WorkloadCertificateConfiguration workloadCertConfig = + MtlsUtils.getWorkloadCertificateConfiguration( + envProvider, propProvider, certConfigPathOverride); - 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); + try (InputStream certStream = + new FileInputStream(new File(workloadCertConfig.getCertPath())); + InputStream privateKeyStream = + new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath())); + SequenceInputStream certAndPrivateKeyStream = + new SequenceInputStream(certStream, privateKeyStream)) { + return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream); + } + } catch (CertificateSourceUnavailableException e) { + throw e; } catch (Exception e) { throw new IOException("X509Provider: Unexpected error loading from config file:", e); } 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 f22e47e2565b..444b35ef0328 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 @@ -45,7 +45,6 @@ import com.google.api.client.util.ExponentialBackOff; import com.google.api.client.util.Key; import com.google.auth.http.HttpTransportFactory; -import com.google.auth.mtls.MtlsUtils; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import java.io.IOException; @@ -182,8 +181,7 @@ static RegionalAccessBoundary refresh( String url, AccessToken accessToken, Clock clock, - int maxRetryElapsedTimeMillis, - EnvironmentProvider envProvider) + int maxRetryElapsedTimeMillis) throws IOException { Preconditions.checkNotNull(accessToken, "The provided access token is null."); if (accessToken.getExpirationTimeMillis() != null @@ -191,12 +189,6 @@ static RegionalAccessBoundary refresh( throw new IllegalArgumentException("The provided access token is expired."); } - MtlsUtils.MtlsEndpointUsagePolicy policy = MtlsUtils.getMtlsEndpointUsagePolicy(envProvider); - if (transportFactory instanceof com.google.auth.mtls.MtlsHttpTransportFactory - || policy == MtlsUtils.MtlsEndpointUsagePolicy.ALWAYS) { - 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/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java index 68dd87e67583..3168d44887a7 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java @@ -201,17 +201,17 @@ void triggerAsyncRefresh( skipRAB.set(true); return; } + if (com.google.auth.mtls.MtlsUtils.canBeEnabled(envProvider, propProvider, null)) { + url = + url.replace( + "iamcredentials.googleapis.com", "iamcredentials.mtls.googleapis.com"); + } HttpTransportFactory upgradedTransportFactory = com.google.auth.mtls.MtlsUtils.prepareTransportFactoryIfMtlsEnabled( transportFactory, envProvider, propProvider, null); RegionalAccessBoundary newRAB = RegionalAccessBoundary.refresh( - upgradedTransportFactory, - url, - accessToken, - clock, - maxRetryElapsedTimeMillis, - envProvider); + upgradedTransportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis); cachedRAB.set(newRAB); resetCooldown(); } catch (Throwable e) { 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 d792cb6de6d3..5f8658eee921 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java @@ -33,7 +33,9 @@ import static org.junit.jupiter.api.Assertions.*; +import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.EnvironmentProvider; +import com.google.auth.oauth2.OAuth2Utils; import com.google.auth.oauth2.PropertyProvider; import java.io.File; import java.io.IOException; @@ -413,6 +415,141 @@ public String getEnv(String name) { assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); } + @Test + void canBeEnabled_autoPolicy_noConfig_returnsFalse() throws IOException { + EnvironmentProvider envProvider = name -> null; + PropertyProvider propProvider = + new PropertyProvider() { + @Override + public String getProperty(String name, String def) { + if ("user.home".equals(name)) return tempDir.toString(); + return def; + } + }; + + assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); + } + + @Test + void canBeEnabled_alwaysPolicy_returnsTrue() throws IOException { + EnvironmentProvider envProvider = + name -> "GOOGLE_API_USE_MTLS_ENDPOINT".equals(name) ? "always" : null; + PropertyProvider propProvider = (name, def) -> def; + + assertTrue(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); + } + + @Test + void getWorkloadCertificateConfiguration_malformedJson_throwsException() throws IOException { + Path configFile = tempDir.resolve("malformed.json"); + Files.write(configFile, "{invalid-json}".getBytes()); + + EnvironmentProvider envProvider = name -> null; + PropertyProvider propProvider = (name, def) -> def; + + assertThrows( + Exception.class, + () -> + MtlsUtils.getWorkloadCertificateConfiguration( + envProvider, propProvider, configFile.toString())); + } + + @Test + void prepareTransportFactoryIfMtlsEnabled_nullInput_returnsNull() throws IOException { + assertNull( + MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + null, name -> null, (name, def) -> def, null)); + } + + @Test + void prepareTransportFactoryIfMtlsEnabled_mtlsFactory_returnsAsIs() + throws java.security.GeneralSecurityException, IOException { + java.security.KeyStore dummyKeyStore = + java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType()); + dummyKeyStore.load(null, null); + MtlsHttpTransportFactory mtlsFactory = new MtlsHttpTransportFactory(dummyKeyStore); + + HttpTransportFactory result = + MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + mtlsFactory, name -> null, (name, def) -> def, null); + + assertSame(mtlsFactory, result); + } + + @Test + void prepareTransportFactoryIfMtlsEnabled_customFactory_mtlsAlways_returnsAsIs() + throws IOException { + HttpTransportFactory customFactory = () -> null; + EnvironmentProvider envProvider = + name -> "GOOGLE_API_USE_MTLS_ENDPOINT".equals(name) ? "always" : null; + PropertyProvider propProvider = (name, def) -> def; + + HttpTransportFactory result = + MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + customFactory, envProvider, propProvider, null); + + assertSame(customFactory, result); + } + + @Test + void prepareTransportFactoryIfMtlsEnabled_customFactory_mtlsAuto_withConfig_returnsAsIs() + throws IOException { + HttpTransportFactory customFactory = () -> null; + EnvironmentProvider envProvider = + name -> + "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) + ? "testresources/mtls/certificate_config.json" + : null; + PropertyProvider propProvider = (name, def) -> def; + + HttpTransportFactory result = + MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + customFactory, envProvider, propProvider, null); + + assertSame(customFactory, result); + } + + @Test + void prepareTransportFactoryIfMtlsEnabled_defaultFactory_mtlsAlways_upgradesToMtlsFactory() + throws IOException { + EnvironmentProvider envProvider = + new EnvironmentProvider() { + @Override + public String getEnv(String name) { + if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) { + return "true"; + } + if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) { + return "always"; + } + if ("GOOGLE_API_CERTIFICATE_CONFIG".equals(name)) { + return "testresources/mtls/certificate_config.json"; + } + return null; + } + }; + PropertyProvider propProvider = (name, def) -> def; + + HttpTransportFactory result = + MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + OAuth2Utils.HTTP_TRANSPORT_FACTORY, envProvider, propProvider, null); + + assertTrue(result instanceof MtlsHttpTransportFactory); + } + + @Test + void prepareTransportFactoryIfMtlsEnabled_defaultFactory_mtlsAuto_noConfig_returnsAsIs() + throws IOException { + EnvironmentProvider envProvider = name -> null; + PropertyProvider propProvider = (name, def) -> def; + + HttpTransportFactory result = + MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + OAuth2Utils.HTTP_TRANSPORT_FACTORY, envProvider, propProvider, null); + + assertSame(OAuth2Utils.HTTP_TRANSPORT_FACTORY, result); + } + private String createJsonConfigString(Path certPath, Path keyPath) { return "{\"cert_configs\":{\"workload\":{\"cert_path\":\"" + certPath.toString().replace("\\", "\\\\") 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 cec775faac0b..c4c82a58398f 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 @@ -50,9 +50,12 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class X509ProviderTest { + @TempDir Path tempDir; + private static final String TEST_CERT_PATH = "testresources/mtls/test_cert.pem"; private static final String TEST_CONFIG_PATH = "testresources/mtls/certificate_config.json"; @@ -70,15 +73,16 @@ void x509Provider_fileDoesntExist_throws() { @Test void x509Provider_emptyFile_throws() throws IOException { - Path emptyConfig = Files.createTempFile("emptyConfig", ".txt"); - emptyConfig.toFile().deleteOnExit(); + Path emptyConfig = tempDir.resolve("emptyConfig.txt"); + Files.createFile(emptyConfig); X509Provider testProvider = new X509Provider(emptyConfig.toString()); String expectedErrorMessage = "no JSON input found"; - IllegalArgumentException exception = - assertThrows(IllegalArgumentException.class, testProvider::getKeyStore); - assertTrue(exception.getMessage().contains(expectedErrorMessage)); + IOException exception = assertThrows(IOException.class, testProvider::getKeyStore); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause() instanceof IllegalArgumentException); + assertTrue(exception.getCause().getMessage().contains(expectedErrorMessage)); } @Test @@ -142,8 +146,8 @@ void x509Provider_succeeds_withWellKnownPath() @Test void x509Provider_succeeds_withWindowsPath() throws IOException, KeyStoreException, CertificateException { - Path windowsTempDir = Files.createTempDirectory("windowsTempDir"); - windowsTempDir.toFile().deleteOnExit(); + Path windowsTempDir = tempDir.resolve("windowsTempDir"); + Files.createDirectory(windowsTempDir); Path gcloudDir = windowsTempDir.resolve("gcloud"); Files.createDirectory(gcloudDir); Path configPath = gcloudDir.resolve("certificate_config.json"); @@ -172,9 +176,8 @@ void x509Provider_succeeds_withWindowsPath() @Test void x509Provider_certFileDoesntExist_throws() throws IOException { - Path tempConfig = Files.createTempFile("config", ".json"); - tempConfig.toFile().deleteOnExit(); - Path nonExistentCert = tempConfig.getParent().resolve("non_existent_cert.pem"); + Path tempConfig = tempDir.resolve("config_no_cert.json"); + Path nonExistentCert = tempDir.resolve("non_existent_cert.pem"); Files.write( tempConfig, @@ -190,10 +193,8 @@ void x509Provider_certFileDoesntExist_throws() throws IOException { @Test void x509Provider_malformedCert_throws() throws IOException { - Path tempConfig = Files.createTempFile("config", ".json"); - tempConfig.toFile().deleteOnExit(); - Path malformedCert = Files.createTempFile("badcert", ".pem"); - malformedCert.toFile().deleteOnExit(); + Path tempConfig = tempDir.resolve("config_malformed_cert.json"); + Path malformedCert = tempDir.resolve("badcert.pem"); Files.write(malformedCert, "This is not a valid certificate".getBytes()); @@ -209,6 +210,36 @@ void x509Provider_malformedCert_throws() throws IOException { assertThrows(Exception.class, testProvider::getKeyStore); } + @Test + void x509Provider_missingCertConfigs_throws() throws IOException { + Path tempConfig = tempDir.resolve("config_missing_configs.json"); + + Files.write(tempConfig, "{}".getBytes()); + + X509Provider testProvider = new X509Provider(tempConfig.toString()); + + IOException exception = assertThrows(IOException.class, testProvider::getKeyStore); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause() instanceof IllegalArgumentException); + assertTrue( + exception.getCause().getMessage().contains("The cert_configs object must be provided")); + } + + @Test + void x509Provider_missingCertPath_throws() throws IOException { + Path tempConfig = tempDir.resolve("config_missing_path.json"); + + Files.write( + tempConfig, "{\"cert_configs\":{\"workload\":{\"key_path\":\"key.pem\"}}}".getBytes()); + + X509Provider testProvider = new X509Provider(tempConfig.toString()); + + IOException exception = assertThrows(IOException.class, testProvider::getKeyStore); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause() instanceof IllegalArgumentException); + assertTrue(exception.getCause().getMessage().contains("The cert_path field must be provided")); + } + // Failure Path: mTLS disabled (allowance = false) throws CertificateSourceUnavailableException @Test void x509Provider_allowanceDisabled_throws() throws Exception { 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 11143eabe928..0d977664d077 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 @@ -150,8 +150,7 @@ public void testRefreshClosesResponse() throws Exception { HttpTransportFactory transportFactory = () -> transport; RegionalAccessBoundary rab = - RegionalAccessBoundary.refresh( - transportFactory, url, token, testClock, 1000, SystemEnvironmentProvider.getInstance()); + RegionalAccessBoundary.refresh(transportFactory, url, token, testClock, 1000); assertEquals("encoded", rab.getEncodedLocations()); assertTrue(mockResponse.isDisconnected(), "Response should have been disconnected"); From 13c69afcca2c87a5f1f6ed54602c539e94363774 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 13 Jul 2026 16:39:58 -0700 Subject: [PATCH 18/24] test fix. --- .../oauth2/ExternalAccountAuthorizedUserCredentialsTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 86d4d95793f4..071500f679ee 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,7 +33,6 @@ 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; @@ -63,6 +62,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -1344,6 +1344,7 @@ private void waitForRegionalAccessBoundary(GoogleCredentials credentials) Assertions.fail("Timed out waiting for regional access boundary refresh"); } } + static GenericJson buildJsonCredentials() { GenericJson json = new GenericJson(); json.put( From ba2d3ca1f7e87376143cfb7030ecd24c4ea89405 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Mon, 13 Jul 2026 18:42:50 -0700 Subject: [PATCH 19/24] Improves regional access boundary cooldown on task submission failures, adds 3-minute token clock skew buffer, and fixes mTLS fallback logic. --- .../google/auth/oauth2/GoogleCredentials.java | 12 +- .../auth/oauth2/RegionalAccessBoundary.java | 4 +- .../oauth2/RegionalAccessBoundaryManager.java | 18 ++- .../oauth2/RegionalAccessBoundaryTest.java | 105 ++++++++++++++++++ 4 files changed, 125 insertions(+), 14 deletions(-) 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 ed9b4d5a7b3f..f69762d066a6 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java @@ -56,8 +56,10 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Executor; import javax.annotation.Nullable; /** Base type for credentials for authorizing calls to Google APIs using OAuth2. */ @@ -379,7 +381,7 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT // Skip refresh for regional endpoints. if (uri != null && uri.getHost() != null) { - String host = uri.getHost().toLowerCase(java.util.Locale.US); + String host = uri.getHost().toLowerCase(Locale.US); if (host.endsWith(".rep.googleapis.com") || host.endsWith(".rep.sandbox.googleapis.com")) { return; } @@ -388,7 +390,7 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT // We need a valid access token for the refresh. if (token == null || (token.getExpirationTimeMillis() != null - && token.getExpirationTimeMillis() < clock.currentTimeMillis())) { + && token.getExpirationTimeMillis() <= clock.currentTimeMillis() + 180_000L)) { return; } @@ -466,9 +468,7 @@ public Map> getRequestMetadata(URI uri) throws IOException */ @Override public void getRequestMetadata( - final URI uri, - final java.util.concurrent.Executor executor, - final RequestMetadataCallback callback) { + final URI uri, final Executor executor, final RequestMetadataCallback callback) { super.getRequestMetadata( uri, executor, @@ -556,7 +556,7 @@ Map> addRegionalAccessBoundaryToRequestMetadata( Preconditions.checkNotNull(requestMetadata); if (uri != null && uri.getHost() != null) { - String host = uri.getHost().toLowerCase(java.util.Locale.US); + String host = uri.getHost().toLowerCase(Locale.US); if (host.endsWith(".rep.googleapis.com") || host.endsWith(".rep.sandbox.googleapis.com")) { return requestMetadata; } diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java index 444b35ef0328..678183192047 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundary.java @@ -185,8 +185,8 @@ static RegionalAccessBoundary refresh( throws IOException { Preconditions.checkNotNull(accessToken, "The provided access token is null."); if (accessToken.getExpirationTimeMillis() != null - && accessToken.getExpirationTimeMillis() < clock.currentTimeMillis()) { - throw new IllegalArgumentException("The provided access token is expired."); + && accessToken.getExpirationTimeMillis() <= clock.currentTimeMillis() + 180_000L) { + throw new IOException("The provided access token is expired or expiring within skew buffer."); } HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java index 3168d44887a7..090b17b38b45 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java @@ -36,7 +36,9 @@ import com.google.api.client.util.Clock; import com.google.api.core.InternalApi; import com.google.auth.http.HttpTransportFactory; +import com.google.auth.mtls.MtlsUtils; import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -68,6 +70,9 @@ final class RegionalAccessBoundaryManager { */ static final int DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS = 60000; + static final String IAM_ENDPOINT = "iamcredentials.googleapis.com"; + static final String MTLS_IAM_ENDPOINT = "iamcredentials.mtls.googleapis.com"; + /** * cachedRAB uses AtomicReference to provide thread-safe, lock-free access to the cached data for * high-concurrency request threads. @@ -201,14 +206,13 @@ void triggerAsyncRefresh( skipRAB.set(true); return; } - if (com.google.auth.mtls.MtlsUtils.canBeEnabled(envProvider, propProvider, null)) { - url = - url.replace( - "iamcredentials.googleapis.com", "iamcredentials.mtls.googleapis.com"); - } HttpTransportFactory upgradedTransportFactory = - com.google.auth.mtls.MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + MtlsUtils.prepareTransportFactoryIfMtlsEnabled( transportFactory, envProvider, propProvider, null); + if (MtlsUtils.canBeEnabled(envProvider, propProvider, null) + && upgradedTransportFactory != OAuth2Utils.HTTP_TRANSPORT_FACTORY) { + url = url.replace(IAM_ENDPOINT, MTLS_IAM_ENDPOINT); + } RegionalAccessBoundary newRAB = RegionalAccessBoundary.refresh( upgradedTransportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis); @@ -227,6 +231,8 @@ void triggerAsyncRefresh( } catch (Exception | Error e) { // If scheduling fails (e.g., RejectedExecutionException, OutOfMemoryError for threads), // the task's finally block will never execute. We must release the lock here. + handleRefreshFailure( + new IOException("Failed to submit background refresh task: " + e.getMessage(), e)); log( LOGGER_PROVIDER, Level.FINE, diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/RegionalAccessBoundaryTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/RegionalAccessBoundaryTest.java index 0d977664d077..8c8c33541d67 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 @@ -35,6 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.api.client.testing.http.MockHttpTransport; @@ -156,6 +157,23 @@ public void testRefreshClosesResponse() throws Exception { assertTrue(mockResponse.isDisconnected(), "Response should have been disconnected"); } + @Test + public void testRefreshThrowsIOExceptionOnExpiringToken() { + final String url = "https://example.com/rab"; + final AccessToken token = + new AccessToken( + "token", new java.util.Date(testClock.currentTimeMillis() + 120_000L)); // 2 minutes, within 3 min skew + + MockHttpTransport transport = new MockHttpTransport.Builder().build(); + HttpTransportFactory transportFactory = () -> transport; + + assertThrows( + IOException.class, + () -> { + RegionalAccessBoundary.refresh(transportFactory, url, token, testClock, 1000); + }); + } + @Test public void testManagerTriggersRefreshInGracePeriod() throws InterruptedException { final String url = @@ -410,4 +428,91 @@ public boolean isDisconnected() { // Verify that MtlsHttpTransportFactory.create() was called to retrieve the mTLS transport Mockito.verify(mockMtlsFactory, Mockito.times(2)).create(); } + + @Test + public void + regionalAccessBoundary_withMtlsEnabledButInitializationFailed_shouldFallbackToNonMtlsEndpoint() + throws IOException, InterruptedException { + + MockExternalAccountCredentialsTransport transport = + new MockExternalAccountCredentialsTransport(); + + // Configure the environment provider to enable mTLS. + // X509Provider will use the invalid certificate config. + TestEnvironmentProvider testEnvProvider = new TestEnvironmentProvider(); + testEnvProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true"); + testEnvProvider.setEnv( + "GOOGLE_API_CERTIFICATE_CONFIG", + new File("testresources/mtls/invalid_certificate_config.json").getAbsolutePath()); + + final String url = + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/default:allowedLocations"; + final AccessToken token = + new AccessToken( + "token", new java.util.Date(System.currentTimeMillis() + 10 * 3600000L)); + + RegionalAccessBoundaryProvider provider = () -> url; + + // Use default OAuth2Utils.HTTP_TRANSPORT_FACTORY + HttpTransportFactory transportFactory = OAuth2Utils.HTTP_TRANSPORT_FACTORY; + + RegionalAccessBoundaryManager manager = + new RegionalAccessBoundaryManager( + testClock, + RegionalAccessBoundaryManager.DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS, + com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService()); + + // Mock static RegionalAccessBoundary.refresh method to intercept the call + try (org.mockito.MockedStatic rabMock = + org.mockito.Mockito.mockStatic(RegionalAccessBoundary.class)) { + + RegionalAccessBoundary mockRab = + new RegionalAccessBoundary( + "fallback-encoded", Arrays.asList("fallback-loc"), testClock.currentTimeMillis(), testClock); + + rabMock + .when( + () -> + RegionalAccessBoundary.refresh( + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.anyInt())) + .thenReturn(mockRab); + + // Trigger refresh + manager.triggerAsyncRefresh( + transportFactory, + provider, + token, + testEnvProvider, + SystemPropertyProvider.getInstance()); + + // Verify it was cached + assertEquals("fallback-encoded", manager.getCachedRAB().getEncodedLocations()); + + final org.mockito.ArgumentCaptor urlCaptor = + org.mockito.ArgumentCaptor.forClass(String.class); + final org.mockito.ArgumentCaptor factoryCaptor = + org.mockito.ArgumentCaptor.forClass(HttpTransportFactory.class); + + // Verify static call and capture arguments + rabMock.verify( + () -> + RegionalAccessBoundary.refresh( + factoryCaptor.capture(), + urlCaptor.capture(), + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.anyInt()), + org.mockito.Mockito.times(1)); + + // Verify the URL passed to refresh did NOT get changed to the mTLS endpoint + assertEquals(url, urlCaptor.getValue()); + // Verify that the transport factory used is the default one + assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, factoryCaptor.getValue()); + } + } } + From bcc2932cc222982bb0f1b6e93454fcdca79f869b Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 15 Jul 2026 15:53:58 -0700 Subject: [PATCH 20/24] removed test which needed static mocking. --- .../oauth2/RegionalAccessBoundaryTest.java | 91 +------------------ 1 file changed, 3 insertions(+), 88 deletions(-) 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 8c8c33541d67..a43da03c41e6 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/RegionalAccessBoundaryTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/RegionalAccessBoundaryTest.java @@ -162,7 +162,9 @@ public void testRefreshThrowsIOExceptionOnExpiringToken() { final String url = "https://example.com/rab"; final AccessToken token = new AccessToken( - "token", new java.util.Date(testClock.currentTimeMillis() + 120_000L)); // 2 minutes, within 3 min skew + "token", + new java.util.Date( + testClock.currentTimeMillis() + 120_000L)); // 2 minutes, within 3 min skew MockHttpTransport transport = new MockHttpTransport.Builder().build(); HttpTransportFactory transportFactory = () -> transport; @@ -428,91 +430,4 @@ public boolean isDisconnected() { // Verify that MtlsHttpTransportFactory.create() was called to retrieve the mTLS transport Mockito.verify(mockMtlsFactory, Mockito.times(2)).create(); } - - @Test - public void - regionalAccessBoundary_withMtlsEnabledButInitializationFailed_shouldFallbackToNonMtlsEndpoint() - throws IOException, InterruptedException { - - MockExternalAccountCredentialsTransport transport = - new MockExternalAccountCredentialsTransport(); - - // Configure the environment provider to enable mTLS. - // X509Provider will use the invalid certificate config. - TestEnvironmentProvider testEnvProvider = new TestEnvironmentProvider(); - testEnvProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true"); - testEnvProvider.setEnv( - "GOOGLE_API_CERTIFICATE_CONFIG", - new File("testresources/mtls/invalid_certificate_config.json").getAbsolutePath()); - - final String url = - "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/default:allowedLocations"; - final AccessToken token = - new AccessToken( - "token", new java.util.Date(System.currentTimeMillis() + 10 * 3600000L)); - - RegionalAccessBoundaryProvider provider = () -> url; - - // Use default OAuth2Utils.HTTP_TRANSPORT_FACTORY - HttpTransportFactory transportFactory = OAuth2Utils.HTTP_TRANSPORT_FACTORY; - - RegionalAccessBoundaryManager manager = - new RegionalAccessBoundaryManager( - testClock, - RegionalAccessBoundaryManager.DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS, - com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService()); - - // Mock static RegionalAccessBoundary.refresh method to intercept the call - try (org.mockito.MockedStatic rabMock = - org.mockito.Mockito.mockStatic(RegionalAccessBoundary.class)) { - - RegionalAccessBoundary mockRab = - new RegionalAccessBoundary( - "fallback-encoded", Arrays.asList("fallback-loc"), testClock.currentTimeMillis(), testClock); - - rabMock - .when( - () -> - RegionalAccessBoundary.refresh( - org.mockito.ArgumentMatchers.any(), - org.mockito.ArgumentMatchers.any(), - org.mockito.ArgumentMatchers.any(), - org.mockito.ArgumentMatchers.any(), - org.mockito.ArgumentMatchers.anyInt())) - .thenReturn(mockRab); - - // Trigger refresh - manager.triggerAsyncRefresh( - transportFactory, - provider, - token, - testEnvProvider, - SystemPropertyProvider.getInstance()); - - // Verify it was cached - assertEquals("fallback-encoded", manager.getCachedRAB().getEncodedLocations()); - - final org.mockito.ArgumentCaptor urlCaptor = - org.mockito.ArgumentCaptor.forClass(String.class); - final org.mockito.ArgumentCaptor factoryCaptor = - org.mockito.ArgumentCaptor.forClass(HttpTransportFactory.class); - - // Verify static call and capture arguments - rabMock.verify( - () -> - RegionalAccessBoundary.refresh( - factoryCaptor.capture(), - urlCaptor.capture(), - org.mockito.ArgumentMatchers.any(), - org.mockito.ArgumentMatchers.any(), - org.mockito.ArgumentMatchers.anyInt()), - org.mockito.Mockito.times(1)); - - // Verify the URL passed to refresh did NOT get changed to the mTLS endpoint - assertEquals(url, urlCaptor.getValue()); - // Verify that the transport factory used is the default one - assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, factoryCaptor.getValue()); - } - } } - From 99d19a08d3c4d47c9b4a116bce599f40ca7e4454 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 15 Jul 2026 16:46:52 -0700 Subject: [PATCH 21/24] Added double check when refreshing RAB to ensure no duplicated refreshes are queued. --- .../google/auth/oauth2/RegionalAccessBoundaryManager.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java index 090b17b38b45..d3a86766ac2c 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java @@ -198,6 +198,11 @@ void triggerAsyncRefresh( // this thread "won the race" and is responsible for starting the background task. // All other concurrent threads will return false and exit immediately. if (isRefreshing.compareAndSet(false, true)) { + currentRab = cachedRAB.get(); + if (currentRab != null && !currentRab.shouldRefresh()) { + isRefreshing.set(false); + return; + } Runnable refreshTask = () -> { try { From 161f28811aef3564ee9f0ea9b1b854036da19a29 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 16 Jul 2026 13:28:35 -0700 Subject: [PATCH 22/24] decouple mTLS endpoint selection and certificate availability checks per AIP-4114, removing fail-fast exceptions when client certificates are unavailable under always policy. --- .../java/com/google/auth/mtls/MtlsUtils.java | 47 ++++++++++++------ .../oauth2/RegionalAccessBoundaryManager.java | 3 +- .../com/google/auth/mtls/MtlsUtilsTest.java | 49 ++++++++++++++++--- 3 files changed, 76 insertions(+), 23 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 6167f99793b9..c8cb0083a83a 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java @@ -173,28 +173,47 @@ public static boolean canBeEnabled( // Check if client certificate usage is allowed String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE"); - MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider); if ("false".equalsIgnoreCase(useClientCertificate)) { - if (policy == MtlsEndpointUsagePolicy.ALWAYS) { - throw new CertificateSourceUnavailableException( - "mTLS is configured to ALWAYS, but client certificate usage was explicitly disabled via GOOGLE_API_USE_CLIENT_CERTIFICATE=false."); - } return false; } + MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider); if (policy == MtlsEndpointUsagePolicy.NEVER) { return false; } - if (policy == MtlsEndpointUsagePolicy.ALWAYS) { - return true; - } - File certConfigFile = resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride); return certConfigFile != null; } + /** + * Returns whether the mutual TLS (mTLS) endpoint should be used. + * + * @param envProvider the environment provider to use for resolving environment variables + * @param propProvider the property provider to use for resolving system properties + * @param certConfigPathOverride optional override path for the configuration file + * @return true if the mTLS endpoint should be used, false otherwise + */ + public static boolean shouldMtlsEndpointBeUsed( + EnvironmentProvider envProvider, + PropertyProvider propProvider, + String certConfigPathOverride) { + MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider); + if (policy == MtlsEndpointUsagePolicy.ALWAYS) { + return true; + } + if (policy == MtlsEndpointUsagePolicy.NEVER) { + return false; + } + // policy is AUTO: use mTLS endpoint if client certificate can be enabled + try { + return canBeEnabled(envProvider, propProvider, certConfigPathOverride); + } catch (IOException e) { + return false; + } + } + /** * Resolves the mutual TLS (mTLS) certificate configuration file. * @@ -313,7 +332,6 @@ public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( return baseTransportFactory; } - MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy(envProvider); try { // This is the default HttpTransportFactory assigned by credentials. // Automatically discover and load client certificates to construct an mTLS factory. @@ -322,11 +340,10 @@ public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( KeyStore mtlsKeyStore = x509Provider.getKeyStore(); return new MtlsHttpTransportFactory(mtlsKeyStore); } catch (Exception e) { - if (mtlsPolicy == MtlsEndpointUsagePolicy.ALWAYS) { - throw new IOException( - "mTLS is configured to ALWAYS, but initialization failed: " + e.getMessage(), e); - } - // Graceful fallback to standard transport if mTLS initialization fails under AUTO policy + LOGGER.warning( + "mTLS transport factory initialization failed, falling back to non-mTLS transport: " + + e.getMessage()); + // Graceful fallback to standard transport if mTLS initialization fails return baseTransportFactory; } } diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java index d3a86766ac2c..db64deaf8d56 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java @@ -214,8 +214,7 @@ void triggerAsyncRefresh( HttpTransportFactory upgradedTransportFactory = MtlsUtils.prepareTransportFactoryIfMtlsEnabled( transportFactory, envProvider, propProvider, null); - if (MtlsUtils.canBeEnabled(envProvider, propProvider, null) - && upgradedTransportFactory != OAuth2Utils.HTTP_TRANSPORT_FACTORY) { + if (MtlsUtils.shouldMtlsEndpointBeUsed(envProvider, propProvider, null)) { url = url.replace(IAM_ENDPOINT, MTLS_IAM_ENDPOINT); } RegionalAccessBoundary newRAB = 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 5f8658eee921..34e6891a26df 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java @@ -353,7 +353,7 @@ public String getProperty(String name, String def) { } @Test - void canBeEnabled_alwaysPolicy_clientCertDisabled_throwsException() { + void canBeEnabled_alwaysPolicy_clientCertDisabled_returnsFalse() throws IOException { EnvironmentProvider envProvider = name -> { if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) { @@ -366,9 +366,8 @@ void canBeEnabled_alwaysPolicy_clientCertDisabled_throwsException() { }; PropertyProvider propProvider = (name, def) -> def; - assertThrows( - CertificateSourceUnavailableException.class, - () -> MtlsUtils.canBeEnabled(envProvider, propProvider, null)); + assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); + assertTrue(MtlsUtils.shouldMtlsEndpointBeUsed(envProvider, propProvider, null)); } @Test @@ -431,12 +430,13 @@ public String getProperty(String name, String def) { } @Test - void canBeEnabled_alwaysPolicy_returnsTrue() throws IOException { + void canBeEnabled_alwaysPolicy_returnsFalse() throws IOException { EnvironmentProvider envProvider = name -> "GOOGLE_API_USE_MTLS_ENDPOINT".equals(name) ? "always" : null; PropertyProvider propProvider = (name, def) -> def; - assertTrue(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); + assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); + assertTrue(MtlsUtils.shouldMtlsEndpointBeUsed(envProvider, propProvider, null)); } @Test @@ -537,6 +537,43 @@ public String getEnv(String name) { assertTrue(result instanceof MtlsHttpTransportFactory); } + @Test + void + prepareTransportFactoryIfMtlsEnabled_defaultFactory_mtlsAlways_clientCertDisabled_returnsAsIs() + throws IOException { + EnvironmentProvider envProvider = + name -> { + if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) { + return "false"; + } + if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) { + return "always"; + } + return null; + }; + PropertyProvider propProvider = (name, def) -> def; + + HttpTransportFactory result = + MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + OAuth2Utils.HTTP_TRANSPORT_FACTORY, envProvider, propProvider, null); + + assertSame(OAuth2Utils.HTTP_TRANSPORT_FACTORY, result); + } + + @Test + void prepareTransportFactoryIfMtlsEnabled_defaultFactory_mtlsAlways_missingConfig_returnsAsIs() + throws IOException { + EnvironmentProvider envProvider = + name -> "GOOGLE_API_USE_MTLS_ENDPOINT".equals(name) ? "always" : null; + PropertyProvider propProvider = (name, def) -> def; + + HttpTransportFactory result = + MtlsUtils.prepareTransportFactoryIfMtlsEnabled( + OAuth2Utils.HTTP_TRANSPORT_FACTORY, envProvider, propProvider, null); + + assertSame(OAuth2Utils.HTTP_TRANSPORT_FACTORY, result); + } + @Test void prepareTransportFactoryIfMtlsEnabled_defaultFactory_mtlsAuto_noConfig_returnsAsIs() throws IOException { From dfbe8149235dacb053aaebeca6efd1d42bcb8a79 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 16 Jul 2026 13:59:00 -0700 Subject: [PATCH 23/24] restore X509Provider.isAvailable to check certificate store availability rather than configuration flags, and add unit tests. --- .../com/google/auth/mtls/X509Provider.java | 7 ++++- .../google/auth/mtls/X509ProviderTest.java | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) 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 6e42e14e995e..8e4d6969a1e1 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java @@ -134,6 +134,11 @@ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOEx @Override public boolean isAvailable() throws IOException { - return MtlsUtils.canBeEnabled(envProvider, propProvider, certConfigPathOverride); + try { + this.getKeyStore(); + } catch (CertificateSourceUnavailableException e) { + return false; + } + return true; } } 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 c4c82a58398f..6a2693a62273 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/X509ProviderTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/X509ProviderTest.java @@ -32,6 +32,7 @@ package com.google.auth.mtls; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -250,4 +251,34 @@ void x509Provider_allowanceDisabled_throws() throws Exception { null); assertThrows(CertificateSourceUnavailableException.class, provider::getKeyStore); } + + @Test + void x509Provider_isAvailable_succeeds() throws IOException { + X509Provider testProvider = new X509Provider(TEST_CONFIG_PATH); + assertTrue(testProvider.isAvailable()); + } + + @Test + void x509Provider_isAvailable_missingConfig_returnsFalse() throws IOException { + X509Provider testProvider = new X509Provider("badfile.json"); + assertFalse(testProvider.isAvailable()); + } + + @Test + void x509Provider_isAvailable_unaffectedByMtlsFlags() throws IOException { + X509Provider provider = + new X509Provider( + name -> { + if ("GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name)) { + return "false"; + } + if ("GOOGLE_API_USE_MTLS_ENDPOINT".equals(name)) { + return "never"; + } + return null; + }, + (name, def) -> def, + TEST_CONFIG_PATH); + assertTrue(provider.isAvailable()); + } } From 863e9646a744ad21d48b3885d304b135d39bb888 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Fri, 17 Jul 2026 15:32:04 -0700 Subject: [PATCH 24/24] Added check in canBeEnabled to check for cert & key file presence. --- .../java/com/google/auth/mtls/MtlsUtils.java | 14 +++++++++++++- .../com/google/auth/mtls/MtlsUtilsTest.java | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) 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 c8cb0083a83a..cdf67965a36b 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 @@ -184,7 +184,19 @@ public static boolean canBeEnabled( File certConfigFile = resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride); - return certConfigFile != null; + if (certConfigFile == null) { + return false; + } + + try { + WorkloadCertificateConfiguration config = + getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride); + File certFile = new File(config.getCertPath()); + File keyFile = new File(config.getPrivateKeyPath()); + return certFile.isFile() && keyFile.isFile(); + } catch (IOException e) { + return false; + } } /** 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 34e6891a26df..1f536b218db4 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/mtls/MtlsUtilsTest.java @@ -439,6 +439,25 @@ void canBeEnabled_alwaysPolicy_returnsFalse() throws IOException { assertTrue(MtlsUtils.shouldMtlsEndpointBeUsed(envProvider, propProvider, null)); } + @Test + void shouldMtlsEndpointBeUsed_autoPolicy_withMissingCertFiles_returnsFalse() throws IOException { + Path configFile = tempDir.resolve("config.json"); + Path nonExistentCert = tempDir.resolve("non_existent_cert.pem"); + Files.write( + configFile, + ("{\"cert_configs\":{\"workload\":{\"cert_path\":\"" + + nonExistentCert.toString().replace("\\", "\\\\") + + "\",\"key_path\":\"key.pem\"}}}") + .getBytes()); + + EnvironmentProvider envProvider = + name -> "GOOGLE_API_CERTIFICATE_CONFIG".equals(name) ? configFile.toString() : null; + PropertyProvider propProvider = (name, def) -> def; + + assertFalse(MtlsUtils.canBeEnabled(envProvider, propProvider, null)); + assertFalse(MtlsUtils.shouldMtlsEndpointBeUsed(envProvider, propProvider, null)); + } + @Test void getWorkloadCertificateConfiguration_malformedJson_throwsException() throws IOException { Path configFile = tempDir.resolve("malformed.json");