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 479e644b1d47..4f0d4c034b6d 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
@@ -131,16 +131,23 @@ static void setGkeCredentialBundlePathForTesting(@Nullable String path) {
gkeCredentialBundlePath = path != null ? path : GKE_CREDENTIAL_BUNDLE_PATH;
}
+ private static boolean isUseClientCertificateExplicitlyDisabled(
+ @Nullable String useClientCertificate) {
+ return !Strings.isNullOrEmpty(useClientCertificate)
+ && !"true".equalsIgnoreCase(useClientCertificate);
+ }
+
/**
* Returns if mutual TLS client certificate should be used. Returns true if valid workload
* certificates are configured or if GOOGLE_API_USE_CLIENT_CERTIFICATE is explicitly set to true
* (e.g. for Enterprise Certificate Proxy or custom MtlsProviders), unless explicitly disabled via
- * GOOGLE_API_USE_CLIENT_CERTIFICATE=false.
+ * GOOGLE_API_USE_CLIENT_CERTIFICATE (set to a non-empty value other than "true").
*/
public static boolean useMtlsClientCertificate(
EnvironmentProvider envProvider, PropertyProvider propProvider) {
- String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
- if ("false".equalsIgnoreCase(useClientCertificate)) {
+ String rawVal = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
+ String useClientCertificate = rawVal != null ? rawVal.trim() : null;
+ if (isUseClientCertificateExplicitlyDisabled(useClientCertificate)) {
return false;
}
if (getWorkloadCertPath(envProvider, propProvider) != null) {
@@ -167,17 +174,18 @@ public static boolean useMtlsClientCertificate(
* configuration file is missing, unreadable, malformed, or references missing/unreadable
* certificate or private key files. This is treated as an unrecoverable misconfiguration.
*
{@code null} (Safe fallback / fail open): Client certificates are explicitly
- * disabled via {@code GOOGLE_API_USE_CLIENT_CERTIFICATE=false}, no explicit configuration
- * is set and neither the default well-known configuration file nor a readable GKE
- * credential bundle exists on disk, or the configuration specifies an non-workload source
- * (e.g., ECP/PKCS11 without a {@code workload} section). Callers can proceed without
- * workload certificate file polling.
+ * disabled via {@code GOOGLE_API_USE_CLIENT_CERTIFICATE} (any non-empty value other than
+ * {@code "true"}), no explicit configuration is set and neither the default well-known
+ * configuration file nor a readable GKE credential bundle exists on disk, or the
+ * configuration specifies an non-workload source (e.g., ECP/PKCS11 without a {@code
+ * workload} section). Callers can proceed without workload certificate file polling.
*
*/
public static @Nullable String getWorkloadCertPath(
EnvironmentProvider envProvider, PropertyProvider propProvider) {
- String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
- if ("false".equalsIgnoreCase(useClientCertificate)) {
+ String rawVal = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
+ String useClientCertificate = rawVal != null ? rawVal.trim() : null;
+ if (isUseClientCertificateExplicitlyDisabled(useClientCertificate)) {
return null;
}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityCacheUtils.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityCacheUtils.java
index eacad1065247..0dfa9d63c8a4 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityCacheUtils.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityCacheUtils.java
@@ -352,19 +352,22 @@ static boolean tryUpdateCache(
}
/**
- * Checks if a file exists and is readable, throwing {@link AccessDeniedException} if permission
- * is denied.
+ * Checks if a file exists, is a non-empty regular file, and is readable, throwing {@link
+ * AccessDeniedException} if permission is denied.
*/
static boolean checkExistsOrAccessDenied(final Path path) throws AccessDeniedException {
try {
- Files.readAttributes(path, BasicFileAttributes.class);
+ BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
+ if (!attrs.isRegularFile()) {
+ return false;
+ }
if (!Files.isReadable(path)) {
if (!Files.exists(path)) {
return false; // Deleted mid-rotation; let the caller retry.
}
throw new AccessDeniedException(path.toString());
}
- return true;
+ return attrs.size() > 0;
} catch (AccessDeniedException e) {
throw e;
} catch (IOException e) {
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java
index 49aa3033424e..2bbe599df301 100644
--- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java
@@ -286,14 +286,36 @@ private static boolean isMtlsExplicitlyDisabled() {
return !Strings.isNullOrEmpty(useClientCert) && !"true".equalsIgnoreCase(useClientCert);
}
+ /**
+ * Resolves symlinks on existing ancestor directories of {@code path} (for example, {@code
+ * /var/run} to {@code /run} before startup files are created), preserving any trailing
+ * non-existent path elements.
+ */
+ private static Path resolveRealPathAllowingMissing(final Path path) {
+ Path normalized = path.toAbsolutePath().normalize();
+ Path current = normalized;
+ while (current != null) {
+ try {
+ Path realCurrent = current.toRealPath();
+ Path relative = current.relativize(normalized);
+ return relative.toString().isEmpty()
+ ? realCurrent
+ : realCurrent.resolve(relative).normalize();
+ } catch (IOException e) {
+ current = current.getParent();
+ }
+ }
+ return normalized;
+ }
+
/** Checks whether the given path resides within the well-known certificate directory. */
private static boolean isPathInWellKnownDir(final @Nullable String pathStr) {
if (Strings.isNullOrEmpty(pathStr)) {
return false;
}
try {
- Path path = Paths.get(pathStr).toAbsolutePath().normalize();
- Path wellKnown = Paths.get(wellKnownDir).toAbsolutePath().normalize();
+ Path path = resolveRealPathAllowingMissing(Paths.get(pathStr));
+ Path wellKnown = resolveRealPathAllowingMissing(Paths.get(wellKnownDir));
return path.startsWith(wellKnown);
} catch (Exception e) {
return false;
@@ -686,7 +708,7 @@ private static ResolvedCertAndKeyPaths getPathsFromConfigWithRetry(
lastParseException);
}
- /** Searches for certificates at well-known locations with retry logic. */
+ /** Searches for certificates at well-known locations. */
private static ResolvedCertAndKeyPaths getWellKnownCertificatePathWithRetry(
final @Nullable CachedAgentIdentityInfo initialCached) throws IOException {
if (isMtlsExplicitlyDisabled()) {
@@ -694,11 +716,11 @@ private static ResolvedCertAndKeyPaths getWellKnownCertificatePathWithRetry(
return new ResolvedCertAndKeyPaths(null, null, false);
}
- // Unlike the Cloud Run well-known files below (which may be delivered asynchronously and are
- // polled for up to 30s on initial startup), GKE mounts the Pod Certificate volume before the
- // container starts, so we perform a single immediate check per call without startup polling.
- // When found, we mark initialStartupCompleted = true so subsequent refreshes during rotation do
- // not enter the 30s startup polling loop.
+ // Startup polling is only performed when GOOGLE_API_CERTIFICATE_CONFIG is set and points to the
+ // well-known directory (see getPathsFromConfigWithRetry). When GOOGLE_API_CERTIFICATE_CONFIG is
+ // unset, GKE mounts the Pod Certificate volume before the container starts (and on GCE an empty
+ // well-known directory may exist without any certificates ever being delivered), so we perform
+ // a single immediate check without startup polling.
// The GKE credential bundle is only used when no well-known gcloud certificate configuration
// file exists (it would otherwise be the transport's mTLS certificate source).
boolean gkeBundleAllowed =
@@ -732,52 +754,31 @@ private static ResolvedCertAndKeyPaths getWellKnownCertificatePathWithRetry(
String certOnlyPath = Paths.get(wellKnownDir, "certificates.pem").toString();
String keyOnlyPath = Paths.get(wellKnownDir, "private_key.pem").toString();
- boolean shouldPoll = !initialStartupCompleted;
- int maxCycles = shouldPoll ? TOTAL_POLL_CYCLES : 1;
-
- boolean warned = false;
- for (int cycle = 0; cycle < maxCycles; cycle++) {
- try {
- if (AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(bundlePath))) {
- initialStartupCompleted = true;
- // Combined bundle file contains both certificate chain and private key
- return new ResolvedCertAndKeyPaths(bundlePath, bundlePath, false);
- }
- if (AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(certOnlyPath))
- && AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(keyOnlyPath))) {
- initialStartupCompleted = true;
- return new ResolvedCertAndKeyPaths(certOnlyPath, keyOnlyPath, false);
- }
- } catch (AccessDeniedException e) {
+ try {
+ if (AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(bundlePath))) {
initialStartupCompleted = true;
- LoggingUtils.log(
- LOGGER_PROVIDER,
- Level.FINE,
- Collections.emptyMap(),
- "Permission denied reading well-known certificates. Falling back to unbound token.");
- return new ResolvedCertAndKeyPaths(null, null, false);
- } catch (Exception e) {
- // Fall through to retry
+ // Combined bundle file contains both certificate chain and private key
+ return new ResolvedCertAndKeyPaths(bundlePath, bundlePath, false);
}
-
- if (cycle + 1 < maxCycles) {
- if (shouldPoll && !warned) {
- LoggingUtils.log(
- LOGGER_PROVIDER,
- Level.FINE,
- Collections.emptyMap(),
- String.format(
- "Well-known certificate file not found at %s. Retrying for up to %d seconds.",
- wellKnownDir, TOTAL_TIMEOUT_MS / 1000));
- warned = true;
- }
- try {
- timeService.sleep(getSleepIntervalMs(cycle));
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new IOException("Interrupted while waiting for well-known certificate files.", e);
- }
+ if (AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(certOnlyPath))
+ && AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(keyOnlyPath))) {
+ initialStartupCompleted = true;
+ return new ResolvedCertAndKeyPaths(certOnlyPath, keyOnlyPath, false);
}
+ } catch (AccessDeniedException e) {
+ initialStartupCompleted = true;
+ LoggingUtils.log(
+ LOGGER_PROVIDER,
+ Level.FINE,
+ Collections.emptyMap(),
+ "Permission denied reading well-known certificates. Falling back to unbound token.");
+ return new ResolvedCertAndKeyPaths(null, null, false);
+ } catch (Exception e) {
+ LoggingUtils.log(
+ LOGGER_PROVIDER,
+ Level.FINE,
+ Collections.emptyMap(),
+ "Unexpected error checking well-known certificates: " + e.getMessage());
}
initialStartupCompleted = true;
@@ -792,12 +793,7 @@ private static ResolvedCertAndKeyPaths getWellKnownCertificatePathWithRetry(
fallbackCached.certMetadata.getPath(), fallbackCached.keyMetadata.getPath(), false);
}
- throw new IOException(
- String.format(
- "Unable to find well-known Agent Identity certificate file at %s for bound token"
- + " request. Token binding protection is failing. You can turn off this"
- + " protection by setting %s to false to fall back to unbound tokens.",
- wellKnownDir, GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN));
+ return new ResolvedCertAndKeyPaths(null, null, 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 546dfbd2c220..1e85e6b11b08 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
@@ -823,6 +823,17 @@ void getWorkloadCertPath_useClientCertFalse_ignoresGkeBundle() throws IOExceptio
assertFalse(MtlsUtils.useMtlsClientCertificate(envProvider, homeInTempDir()));
}
+ @Test
+ void getWorkloadCertPath_useClientCertNonTrueValues_disablesMtls() throws IOException {
+ writeGkeBundle();
+ for (String disabledVal : new String[] {"0", "no", "FALSE", "off", "random"}) {
+ EnvironmentProvider envProvider =
+ name -> "GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name) ? disabledVal : null;
+ assertNull(MtlsUtils.getWorkloadCertPath(envProvider, homeInTempDir()));
+ assertFalse(MtlsUtils.useMtlsClientCertificate(envProvider, homeInTempDir()));
+ }
+ }
+
@Test
void getWorkloadCertPath_defaultConfigWithoutWorkload_ignoresGkeBundle() throws IOException {
writeGkeBundle();
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java
index d6d25407a394..26680489dd25 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java
@@ -71,7 +71,6 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@@ -590,7 +589,7 @@ public void getAgentIdentityCertInfo_mismatch_throwsIOExceptionAfterRetries() th
@Test
public void
- getAgentIdentityCertInfo_wellKnownDirExistsNoFiles_explicitlyEnabled_retriesAndThrowsIOException()
+ getAgentIdentityCertInfo_wellKnownDirExistsNoFiles_explicitlyEnabled_returnsNullWithoutPolling()
throws IOException {
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
FakeTimeService fakeTime = new FakeTimeService();
@@ -598,8 +597,23 @@ public void getAgentIdentityCertInfo_mismatch_throwsIOExceptionAfterRetries() th
envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null);
envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true");
- assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo);
- assertTrue(fakeTime.getSleepCount() > 0);
+ assertNull(AgentIdentityUtils.getAgentIdentityCertInfo());
+ assertEquals(0, fakeTime.getSleepCount());
+ }
+
+ @Test
+ public void
+ getAgentIdentityCertInfo_wellKnownDirMissing_explicitlyEnabled_returnsNullWithoutPolling()
+ throws IOException {
+ AgentIdentityUtils.setWellKnownDir(
+ tempDir.resolve("non_existent_well_known").toAbsolutePath().toString() + "/");
+ FakeTimeService fakeTime = new FakeTimeService();
+ AgentIdentityUtils.setTimeService(fakeTime);
+ envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null);
+ envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true");
+
+ assertNull(AgentIdentityUtils.getAgentIdentityCertInfo());
+ assertEquals(0, fakeTime.getSleepCount());
}
@Test
@@ -1004,13 +1018,15 @@ public void getAgentIdentityCertInfo_partialJsonDuringStartupPoll_retriesAndSucc
@Test
public void
- getAgentIdentityCertInfo_startupPollTimeout_subsequentRefreshDoesNotPollAndThrowsIOException()
+ getAgentIdentityCertInfo_configInWellKnownDirTimeout_subsequentRefreshDoesNotPollAndThrowsIOException()
throws Exception {
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
FakeTimeService fakeTime = new FakeTimeService();
AgentIdentityUtils.setTimeService(fakeTime);
- envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null);
- envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true");
+ envProvider.setEnv(
+ "GOOGLE_API_CERTIFICATE_CONFIG",
+ tempDir.resolve("missing_config.json").toAbsolutePath().toString());
+ envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", null);
// Initial call polls for all 100 cycles (99 sleeps) and fails closed with IOException
assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo);
@@ -1022,24 +1038,86 @@ public void getAgentIdentityCertInfo_partialJsonDuringStartupPoll_retriesAndSucc
}
@Test
- public void
- getAgentIdentityCertInfo_configInWellKnownDirTimeout_subsequentRefreshDoesNotPollAndThrowsIOException()
- throws Exception {
+ public void getAgentIdentityCertInfo_zeroByteFilesDuringStartupPoll_waitsUntilNonEmpty()
+ throws Exception {
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
+ Path targetCertPath = tempDir.resolve("certificates.pem");
+ Path targetKeyPath = tempDir.resolve("private_key.pem");
+ // Create 0-byte files simulating CSI driver creating file entries before writing content
+ Files.createFile(targetCertPath);
+ Files.createFile(targetKeyPath);
+
+ Path configFile = tempDir.resolve("credentials.json");
+ writeWorkloadConfig(configFile, targetCertPath, targetKeyPath);
+ envProvider.setEnv(AgentIdentityUtils.GOOGLE_API_CERTIFICATE_CONFIG, configFile.toString());
+
+ // Populate after sleep 4 and 5 so loadAndVerifyCredentials's 3 attempts (2 sleeps) cannot
+ // mask a broken 0-byte startup poll check.
FakeTimeService fakeTime = new FakeTimeService();
+ fakeTime.setOnSleepCallback(
+ () -> {
+ if (fakeTime.getSleepCount() == 4) {
+ try {
+ // First populate only cert; key is still 0 bytes
+ copyResource("agent/agent_spiffe_cert.pem", targetCertPath);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ } else if (fakeTime.getSleepCount() == 5) {
+ try {
+ // Then populate key
+ copyResource("agent/agent_spiffe_key.pem", targetKeyPath);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
AgentIdentityUtils.setTimeService(fakeTime);
+
+ AgentIdentityUtils.CertInfo info = AgentIdentityUtils.getAgentIdentityCertInfo();
+ assertNotNull(info);
+ assertEquals(5, fakeTime.getSleepCount());
+ }
+
+ @Test
+ public void getAgentIdentityCertInfo_symlinkedWellKnownDir_pollsForStartupFiles()
+ throws Exception {
+ Path realWellKnown = Files.createDirectory(tempDir.resolve("real_run_secrets"));
+ Path symlinkWellKnown = tempDir.resolve("var_run_secrets");
+ try {
+ Files.createSymbolicLink(symlinkWellKnown, realWellKnown);
+ } catch (UnsupportedOperationException | IOException e) {
+ Assumptions.assumeTrue(false, "Symbolic links not supported: " + e.getMessage());
+ }
+
+ // Configure wellKnownDir via symlink path (/var/run/...) while GOOGLE_API_CERTIFICATE_CONFIG
+ // uses the real path (/run/...) for a file that does not exist yet at startup.
+ AgentIdentityUtils.setWellKnownDir(symlinkWellKnown.toAbsolutePath().toString() + "/");
+ Path configViaRealPath = realWellKnown.resolve("credentials.json");
+ Path certViaRealPath = realWellKnown.resolve("certificates.pem");
+ Path keyViaRealPath = realWellKnown.resolve("private_key.pem");
envProvider.setEnv(
- "GOOGLE_API_CERTIFICATE_CONFIG",
- tempDir.resolve("missing_config.json").toAbsolutePath().toString());
- envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", null);
+ AgentIdentityUtils.GOOGLE_API_CERTIFICATE_CONFIG,
+ configViaRealPath.toAbsolutePath().toString());
- // Initial call polls for all 100 cycles (99 sleeps) and fails closed with IOException
- assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo);
- assertEquals(99, fakeTime.getSleepCount());
+ FakeTimeService fakeTime = new FakeTimeService();
+ fakeTime.setOnSleepCallback(
+ () -> {
+ if (fakeTime.getSleepCount() == 1) {
+ try {
+ copyResource("agent/agent_spiffe_cert.pem", certViaRealPath);
+ copyResource("agent/agent_spiffe_key.pem", keyViaRealPath);
+ writeWorkloadConfig(configViaRealPath, certViaRealPath, keyViaRealPath);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ AgentIdentityUtils.setTimeService(fakeTime);
- // Subsequent token refresh fails closed immediately without sleeping
- assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo);
- assertEquals(99, fakeTime.getSleepCount());
+ AgentIdentityUtils.CertInfo info = AgentIdentityUtils.getAgentIdentityCertInfo();
+ assertNotNull(info);
+ assertEquals(1, fakeTime.getSleepCount());
}
@Test
@@ -1140,8 +1218,9 @@ public void getAgentIdentityCertInfo_implicitDiscovery_bundleWithoutPrivateKey_t
}
@Test
- public void getAgentIdentityCertInfo_explicitMtlsEnabled_missingPrivateKey_throwsIOException()
- throws Exception {
+ public void
+ getAgentIdentityCertInfo_explicitMtlsEnabled_missingPrivateKey_returnsNullWithoutPolling()
+ throws Exception {
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
URL certUrl = getClass().getClassLoader().getResource("agent/agent_spiffe_cert.pem");
assertNotNull(certUrl);
@@ -1153,10 +1232,8 @@ public void getAgentIdentityCertInfo_explicitMtlsEnabled_missingPrivateKey_throw
FakeTimeService fakeTime = new FakeTimeService();
AgentIdentityUtils.setTimeService(fakeTime);
- IOException e = assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo);
- assertTrue(
- e.getMessage().contains("Unable to find well-known Agent Identity certificate file"));
- assertEquals(99, fakeTime.getSleepCount());
+ assertNull(AgentIdentityUtils.getAgentIdentityCertInfo());
+ assertEquals(0, fakeTime.getSleepCount());
}
@Test
@@ -1944,25 +2021,16 @@ public void getAgentIdentityCertInfo_gkeBundle_useClientCertTrue_isNotPolledFor(
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
envProvider.setEnv(AgentIdentityUtils.GOOGLE_API_USE_CLIENT_CERTIFICATE, "true");
FakeTimeService fakeTime = new FakeTimeService();
- AtomicBoolean written = new AtomicBoolean(false);
- fakeTime.setOnSleepCallback(
- () -> {
- if (written.compareAndSet(false, true)) {
- try {
- writeAgentGkeBundle();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
- });
AgentIdentityUtils.setTimeService(fakeTime);
- // The bundle appears during startup polling, but only the Cloud Run files are polled for.
- assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo);
- assertTrue(written.get());
+ // Without GOOGLE_API_CERTIFICATE_CONFIG, no startup polling occurs when the bundle is absent.
+ assertNull(AgentIdentityUtils.getAgentIdentityCertInfo());
+ assertEquals(0, fakeTime.getSleepCount());
- // After startup, the bundle is found by the single up-front check.
+ // Once the bundle is written, it is found by the single up-front check.
+ writeAgentGkeBundle();
assertNotNull(AgentIdentityUtils.getAgentIdentityCertInfo());
+ assertEquals(0, fakeTime.getSleepCount());
}
@Test
@@ -2027,7 +2095,7 @@ public void getAgentIdentityCertInfo_gkeBundleRemovedAfterCaching_configAppears_
writeDefaultGcloudCertificateConfig();
// No other well-known files exist and the cached GKE credential is not allowed anymore.
- assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo);
+ assertNull(AgentIdentityUtils.getAgentIdentityCertInfo());
assertEquals(0, fakeTime.getSleepCount());
}
diff --git a/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java b/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java
index f31ac845fdcb..91299357da10 100644
--- a/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java
+++ b/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java
@@ -46,7 +46,6 @@
import com.google.auth.ApiKeyCredentials;
import com.google.auth.Credentials;
import com.google.auth.mtls.CertificateSourceUnavailableException;
-import com.google.auth.mtls.DefaultMtlsProviderFactory;
import com.google.auth.mtls.MtlsProvider;
import com.google.auth.oauth2.ComputeEngineCredentials;
import com.google.auth.oauth2.SecureSessionAgent;
@@ -1408,18 +1407,23 @@ public InstantiatingGrpcChannelProvider build() {
if (mtlsProvider == null) {
// Attempt to create default MtlsProvider from environment.
try {
- mtlsProvider = DefaultMtlsProviderFactory.create();
+ mtlsProvider = certificateBasedAccess.createDefaultMtlsProvider();
} catch (CertificateSourceUnavailableException e) {
// This is okay. Leave mtlsProvider as null so that we will not auto-upgrade
// to mTLS endpoints. See https://google.aip.dev/auth/4114.
} catch (IOException e) {
- LOG.log(
- Level.WARNING,
- "DefaultMtlsProviderFactory encountered unexpected IOException: " + e.getMessage());
- LOG.log(
- Level.WARNING,
- "mTLS configuration was detected on the device, but mTLS failed to initialize."
- + " Falling back to non-mTLS channel.");
+ mtlsProvider =
+ new MtlsProvider() {
+ @Override
+ public boolean isAvailable() throws IOException {
+ throw e;
+ }
+
+ @Override
+ public KeyStore getKeyStore() throws IOException {
+ throw e;
+ }
+ };
}
}
}
diff --git a/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java b/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java
index a7b28c7d709e..5ffb6943a4fe 100644
--- a/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java
+++ b/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProviderTest.java
@@ -52,6 +52,7 @@
import com.google.auth.ApiKeyCredentials;
import com.google.auth.Credentials;
import com.google.auth.http.AuthHttpConstants;
+import com.google.auth.mtls.CertificateSourceUnavailableException;
import com.google.auth.mtls.MtlsProvider;
import com.google.auth.oauth2.CloudShellCredentials;
import com.google.auth.oauth2.ComputeEngineCredentials;
@@ -1443,6 +1444,50 @@ void createChannelBuilder_whenMtlsActiveAndKeyStoreNull_throwsIOException() {
assertThat(thrown).hasMessageThat().contains("Failed to initialize mTLS channel credentials");
}
+ @Test
+ void createChannelBuilder_whenDefaultMtlsProviderThrowsIOException_propagatesIOException()
+ throws Exception {
+ CertificateBasedAccess mtlsCertificateBasedAccess =
+ mock(CertificateBasedAccess.class, Mockito.withSettings().withoutAnnotations());
+ Mockito.when(mtlsCertificateBasedAccess.useMtlsClientCertificate()).thenReturn(true);
+ IOException expected = new IOException("Corrupt PEM file");
+ Mockito.when(mtlsCertificateBasedAccess.createDefaultMtlsProvider()).thenThrow(expected);
+
+ InstantiatingGrpcChannelProvider provider =
+ InstantiatingGrpcChannelProvider.newBuilder()
+ .setCertificateBasedAccess(mtlsCertificateBasedAccess)
+ .setAttemptDirectPath(false)
+ .setEndpoint(DEFAULT_ENDPOINT)
+ .setHeaderProvider(
+ mock(HeaderProvider.class, Mockito.withSettings().withoutAnnotations()))
+ .build();
+
+ IOException thrown = assertThrows(IOException.class, provider::createChannelBuilder);
+ assertThat(thrown).isSameInstanceAs(expected);
+ }
+
+ @Test
+ void
+ createChannelBuilder_whenDefaultMtlsProviderThrowsCertificateSourceUnavailable_fallsBackWithoutMtls()
+ throws Exception {
+ CertificateBasedAccess mtlsCertificateBasedAccess =
+ mock(CertificateBasedAccess.class, Mockito.withSettings().withoutAnnotations());
+ Mockito.when(mtlsCertificateBasedAccess.useMtlsClientCertificate()).thenReturn(true);
+ Mockito.when(mtlsCertificateBasedAccess.createDefaultMtlsProvider())
+ .thenThrow(new CertificateSourceUnavailableException("No certificates on disk"));
+
+ InstantiatingGrpcChannelProvider provider =
+ InstantiatingGrpcChannelProvider.newBuilder()
+ .setCertificateBasedAccess(mtlsCertificateBasedAccess)
+ .setAttemptDirectPath(false)
+ .setEndpoint(DEFAULT_ENDPOINT)
+ .setHeaderProvider(
+ mock(HeaderProvider.class, Mockito.withSettings().withoutAnnotations()))
+ .build();
+
+ assertThat(provider.createChannelBuilder()).isNotNull();
+ }
+
private static class FakeLogHandler extends Handler {
List records = new ArrayList<>();
diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java
index d1c2ea0f1c97..0766a8032df1 100644
--- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java
+++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProvider.java
@@ -40,7 +40,6 @@
import com.google.api.gax.rpc.mtls.CertificateBasedAccess;
import com.google.auth.Credentials;
import com.google.auth.mtls.CertificateSourceUnavailableException;
-import com.google.auth.mtls.DefaultMtlsProviderFactory;
import com.google.auth.mtls.MtlsProvider;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
@@ -51,7 +50,6 @@
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
-import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
@@ -453,18 +451,23 @@ public InstantiatingHttpJsonChannelProvider build() {
if (mtlsProvider == null) {
// Attempt to create default MtlsProvider from environment.
try {
- mtlsProvider = DefaultMtlsProviderFactory.create();
+ mtlsProvider = certificateBasedAccess.createDefaultMtlsProvider();
} catch (CertificateSourceUnavailableException e) {
// This is okay. Leave mtlsProvider as null so that we will not auto-upgrade
// to mTLS endpoints. See https://google.aip.dev/auth/4114.
} catch (IOException e) {
- LOG.log(
- Level.WARNING,
- "DefaultMtlsProviderFactory encountered unexpected IOException: " + e.getMessage());
- LOG.log(
- Level.WARNING,
- "mTLS configuration was detected on the device, but mTLS failed to initialize."
- + " Falling back to non-mTLS channel.");
+ mtlsProvider =
+ new MtlsProvider() {
+ @Override
+ public boolean isAvailable() throws IOException {
+ throw e;
+ }
+
+ @Override
+ public KeyStore getKeyStore() throws IOException {
+ throw e;
+ }
+ };
}
}
}
diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java
index 749876c4a005..74a578da24cb 100644
--- a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java
+++ b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/InstantiatingHttpJsonChannelProviderTest.java
@@ -41,6 +41,7 @@
import com.google.api.gax.rpc.mtls.AbstractMtlsTransportChannelTest;
import com.google.api.gax.rpc.mtls.CertificateBasedAccess;
import com.google.api.gax.rpc.testing.FakeMtlsProvider;
+import com.google.auth.mtls.CertificateSourceUnavailableException;
import com.google.auth.mtls.MtlsProvider;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -479,6 +480,50 @@ void getTransportChannel_whenMtlsKeyStoreThrowsRuntimeException_propagatesUnwrap
assertThat(thrown).isSameInstanceAs(failure);
}
+ @Test
+ void getTransportChannel_whenDefaultMtlsProviderThrowsIOException_propagatesIOException()
+ throws Exception {
+ Mockito.when(certificateBasedAccess.useMtlsClientCertificate()).thenReturn(true);
+ IOException expected = new IOException("Corrupt PEM file");
+ Mockito.when(certificateBasedAccess.createDefaultMtlsProvider()).thenThrow(expected);
+
+ InstantiatingHttpJsonChannelProvider provider =
+ InstantiatingHttpJsonChannelProvider.newBuilder()
+ .setEndpoint(DEFAULT_ENDPOINT)
+ .setCertificateBasedAccess(certificateBasedAccess)
+ .build();
+ InstantiatingHttpJsonChannelProvider finalProvider =
+ (InstantiatingHttpJsonChannelProvider) provider.withHeaders(DEFAULT_HEADER_MAP);
+
+ IOException thrown = assertThrows(IOException.class, finalProvider::getTransportChannel);
+ assertThat(thrown).isSameInstanceAs(expected);
+ }
+
+ @Test
+ void
+ getTransportChannel_whenDefaultMtlsProviderThrowsCertificateSourceUnavailable_fallsBackWithoutMtls()
+ throws Exception {
+ Mockito.when(certificateBasedAccess.useMtlsClientCertificate()).thenReturn(true);
+ Mockito.when(certificateBasedAccess.createDefaultMtlsProvider())
+ .thenThrow(new CertificateSourceUnavailableException("No certificates on disk"));
+
+ InstantiatingHttpJsonChannelProvider provider =
+ InstantiatingHttpJsonChannelProvider.newBuilder()
+ .setEndpoint(DEFAULT_ENDPOINT)
+ .setCertificateBasedAccess(certificateBasedAccess)
+ .build();
+ InstantiatingHttpJsonChannelProvider finalProvider =
+ (InstantiatingHttpJsonChannelProvider) provider.withHeaders(DEFAULT_HEADER_MAP);
+
+ HttpTransport transport = finalProvider.createHttpTransport();
+ assertThat(transport).isInstanceOf(NetHttpTransport.class);
+ assertThat(((NetHttpTransport) transport).isMtls()).isFalse();
+
+ HttpJsonTransportChannel channel = finalProvider.getTransportChannel();
+ assertThat(channel).isNotNull();
+ channel.shutdownNow();
+ }
+
@Test
void createHttpTransport_withMtlsKeyStore_returnsMtlsTransport()
throws IOException, GeneralSecurityException {
diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java
index 0c0ef9a69b9c..2fdb3974087f 100644
--- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java
+++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java
@@ -34,7 +34,6 @@
import com.google.api.gax.rpc.mtls.CertificateBasedAccess;
import com.google.auth.Credentials;
import com.google.auth.mtls.CertificateSourceUnavailableException;
-import com.google.auth.mtls.DefaultMtlsProviderFactory;
import com.google.auth.mtls.MtlsProvider;
import com.google.auth.oauth2.ComputeEngineCredentials;
import com.google.auto.value.AutoValue;
@@ -42,7 +41,6 @@
import com.google.common.base.Strings;
import com.google.common.net.HostAndPort;
import java.io.IOException;
-import java.util.logging.Level;
import java.util.logging.Logger;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@@ -65,7 +63,11 @@ public abstract class EndpointContext {
// static block initialization for exception handling
static {
try {
- INSTANCE = EndpointContext.newBuilder().setServiceName("").build();
+ INSTANCE =
+ EndpointContext.newBuilder()
+ .setServiceName("")
+ .setCertificateBasedAccess(new CertificateBasedAccess(name -> "false"))
+ .build();
} catch (IOException e) {
throw new RuntimeException("Unable to create a default empty EndpointContext", e);
}
@@ -309,16 +311,9 @@ private String determineEndpoint() throws IOException {
if (certificateBasedAccess.useMtlsClientCertificate()) {
if (mtlsProvider == null) {
try {
- mtlsProvider = DefaultMtlsProviderFactory.create();
+ mtlsProvider = certificateBasedAccess.createDefaultMtlsProvider();
} catch (CertificateSourceUnavailableException e) {
// This is okay. Leave mtlsProvider as null;
- } catch (IOException e) {
- LOG.log(
- Level.WARNING,
- "DefaultMtlsProviderFactory encountered unexpected IOException: " + e.getMessage());
- LOG.log(
- Level.WARNING,
- "mTLS configuration was detected on the device, but mTLS failed to initialize. Falling back to non-mTLS channel.");
}
}
}
diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/mtls/CertificateBasedAccess.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/mtls/CertificateBasedAccess.java
index d77378ad9612..59a2b674404c 100644
--- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/mtls/CertificateBasedAccess.java
+++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/mtls/CertificateBasedAccess.java
@@ -32,8 +32,11 @@
import com.google.api.core.InternalApi;
import com.google.api.gax.rpc.internal.EnvironmentProvider;
+import com.google.auth.mtls.DefaultMtlsProviderFactory;
+import com.google.auth.mtls.MtlsProvider;
import com.google.auth.mtls.MtlsUtils;
import com.google.auth.oauth2.PropertyProvider;
+import java.io.IOException;
/**
* Utility class for handling certificate-based access configurations.
@@ -96,4 +99,9 @@ public MtlsEndpointUsagePolicy getMtlsEndpointUsagePolicy() {
public String getWorkloadCertPath() {
return MtlsUtils.getWorkloadCertPath(getAuthEnvProvider(), getAuthPropertyProvider());
}
+
+ /** Creates the default {@link MtlsProvider} for the environment. */
+ public MtlsProvider createDefaultMtlsProvider() throws IOException {
+ return DefaultMtlsProviderFactory.create();
+ }
}
diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java
index 950b9f9dd7a7..e388676a8c89 100644
--- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java
+++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java
@@ -38,6 +38,7 @@
import com.google.api.gax.rpc.mtls.CertificateBasedAccess;
import com.google.api.gax.rpc.testing.FakeMtlsProvider;
import com.google.auth.Credentials;
+import com.google.auth.mtls.CertificateSourceUnavailableException;
import com.google.auth.mtls.MtlsProvider;
import com.google.auth.oauth2.ComputeEngineCredentials;
import com.google.common.truth.Truth;
@@ -743,4 +744,39 @@ void getUrlDomain_nonGDUUniverseDomain() throws IOException {
defaultEndpointContextBuilder.setUniverseDomain("random.com").build();
Truth.assertThat(endpointContext.getUrlDomain()).isEqualTo("test.random.com");
}
+
+ @Test
+ void endpointContextBuild_whenDefaultMtlsProviderThrowsIOException_propagatesIOException()
+ throws Exception {
+ CertificateBasedAccess certificateBasedAccess = Mockito.mock(CertificateBasedAccess.class);
+ Mockito.when(certificateBasedAccess.useMtlsClientCertificate()).thenReturn(true);
+ IOException expected = new IOException("Corrupt PEM file");
+ Mockito.when(certificateBasedAccess.createDefaultMtlsProvider()).thenThrow(expected);
+
+ EndpointContext.Builder builder =
+ defaultEndpointContextBuilder
+ .setMtlsProvider(null)
+ .setCertificateBasedAccess(certificateBasedAccess);
+ IOException actual = assertThrows(IOException.class, builder::build);
+ Truth.assertThat(actual).isSameInstanceAs(expected);
+ }
+
+ @Test
+ void
+ endpointContextBuild_whenDefaultMtlsProviderThrowsCertificateSourceUnavailable_fallsBackToRegularEndpoint()
+ throws Exception {
+ CertificateBasedAccess certificateBasedAccess = Mockito.mock(CertificateBasedAccess.class);
+ Mockito.when(certificateBasedAccess.useMtlsClientCertificate()).thenReturn(true);
+ Mockito.when(certificateBasedAccess.getMtlsEndpointUsagePolicy())
+ .thenReturn(CertificateBasedAccess.MtlsEndpointUsagePolicy.AUTO);
+ Mockito.when(certificateBasedAccess.createDefaultMtlsProvider())
+ .thenThrow(new CertificateSourceUnavailableException("No certificates on disk"));
+
+ EndpointContext endpointContext =
+ defaultEndpointContextBuilder
+ .setMtlsProvider(null)
+ .setCertificateBasedAccess(certificateBasedAccess)
+ .build();
+ Truth.assertThat(endpointContext.resolvedEndpoint()).isEqualTo(DEFAULT_ENDPOINT);
+ }
}