Skip to content

Commit 73fa344

Browse files
committed
fix(oauth2): address PR 13873 review findings for agent identity token binding
- Prevent 30-second polling delay and IOException on standard GCE/container environments when well-known credentials directory exists without certificate files unless mTLS is explicitly enabled. - Fail fast on malformed certificate config JSON without retrying. - Fix URI resource path decoding in AgentIdentityUtilsTest to prevent FileNotFoundException when workspace paths contain spaces. - Copy matching private key in well-known fallback test to verify full key pair loading. - Clean up static wellKnownDir state and temporary directories in ComputeEngineCredentialsTest. - Correct opt-out environment variable value in test setup from 'true' to 'false'.
1 parent 0649e49 commit 73fa344

5 files changed

Lines changed: 199 additions & 23 deletions

File tree

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ public final class AgentIdentityUtils {
7171
static final String GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES =
7272
"GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES";
7373

74+
/** Javadoc. */
75+
static final String GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE";
76+
7477
/** Javadoc. */
7578
private static final List<Pattern> AGENT_IDENTITY_SPIFFE_PATTERNS =
7679
ImmutableList.of(
@@ -399,6 +402,10 @@ && checkExistsOrAccessDenied(Paths.get(paths.getCertPath()))) {
399402
+ " token.");
400403
return null;
401404
} catch (IOException e) {
405+
if (e.getMessage() != null
406+
&& e.getMessage().contains("Failed to parse Agent Identity config JSON")) {
407+
throw e; // Fail fast on malformed JSON syntax errors
408+
}
402409
// Fall through to retry
403410
}
404411
if (!warned) {
@@ -435,6 +442,42 @@ private static String getWellKnownCertificatePathWithRetry() throws IOException
435442
String bundlePath = Paths.get(wellKnownDir, "credentialbundle.pem").toString();
436443
String certOnlyPath = Paths.get(wellKnownDir, "certificates.pem").toString();
437444

445+
// 1) First check immediately without sleeping:
446+
try {
447+
if (checkExistsOrAccessDenied(Paths.get(bundlePath))) {
448+
return bundlePath;
449+
}
450+
if (checkExistsOrAccessDenied(Paths.get(certOnlyPath))) {
451+
return certOnlyPath;
452+
}
453+
} catch (java.nio.file.AccessDeniedException e) {
454+
Slf4jUtils.log(
455+
LOGGER,
456+
org.slf4j.event.Level.WARN,
457+
Collections.emptyMap(),
458+
"Permission denied reading well-known certificates. Falling back to unbound"
459+
+ " token.");
460+
return null;
461+
} catch (Exception e) {
462+
// Fall through
463+
}
464+
465+
// 2) If not found immediately, only enter retry loop if mTLS was explicitly enabled:
466+
String useClientCert = envReader.getEnv(GOOGLE_API_USE_CLIENT_CERTIFICATE);
467+
if (!"true".equalsIgnoreCase(useClientCert)) {
468+
Slf4jUtils.log(
469+
LOGGER,
470+
org.slf4j.event.Level.DEBUG,
471+
Collections.emptyMap(),
472+
String.format(
473+
"Well-known certificate file not found at %s and %s is not"
474+
+ " explicitly enabled; falling back to unbound token without"
475+
+ " retrying.",
476+
wellKnownDir, GOOGLE_API_USE_CLIENT_CERTIFICATE));
477+
return null;
478+
}
479+
480+
// 3) Retry loop for rotation/transient absence when explicitly enabled:
438481
boolean warned = false;
439482
for (long sleepInterval : POLLING_INTERVALS) {
440483
try {
@@ -461,20 +504,28 @@ private static String getWellKnownCertificatePathWithRetry() throws IOException
461504
org.slf4j.event.Level.WARN,
462505
Collections.emptyMap(),
463506
String.format(
464-
"Well-known certificate file not found at %s. Retrying for up to %d" + " seconds.",
507+
"Well-known certificate file not found at %s. Retrying for up to"
508+
+ " %d seconds.",
465509
wellKnownDir, TOTAL_TIMEOUT_MS / 1000));
466510
warned = true;
467511
}
468512
try {
469513
timeService.sleep(sleepInterval);
470514
} catch (InterruptedException e) {
471515
Thread.currentThread().interrupt();
472-
throw new IOException("Interrupted while waiting for well-known certificate files.", e);
516+
throw new IOException(
517+
"Interrupted while waiting for well-known certificate files.", e);
473518
}
474519
}
475-
throw new IOException(
476-
"Unable to find well-known certificate file for bound token request after multiple"
477-
+ " retries.");
520+
Slf4jUtils.log(
521+
LOGGER,
522+
org.slf4j.event.Level.WARN,
523+
Collections.emptyMap(),
524+
String.format(
525+
"Unable to find well-known certificate file at %s after retrying;"
526+
+ " falling back to unbound token.",
527+
wellKnownDir));
528+
return null;
478529
}
479530

480531
/** Reads the full certificate chain from the specified path as a string. */
@@ -530,7 +581,7 @@ static PrivateKey readPrivateKey(final String keyPath, final String algorithm)
530581
*/
531582
static boolean shouldEnableMtls(final boolean certsPresent, final boolean configExists)
532583
throws IOException {
533-
String useClientCert = envReader.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
584+
String useClientCert = envReader.getEnv(GOOGLE_API_USE_CLIENT_CERTIFICATE);
534585

535586
// Case 1: Explicitly enabled via environment variable
536587
if ("true".equalsIgnoreCase(useClientCert)) {

google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java

Lines changed: 117 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.Collection;
5656
import java.util.Collections;
5757
import java.util.List;
58+
import java.util.concurrent.atomic.AtomicInteger;
5859
import java.util.concurrent.atomic.AtomicLong;
5960
import org.junit.jupiter.api.AfterEach;
6061
import org.junit.jupiter.api.BeforeEach;
@@ -150,22 +151,47 @@ private X509Certificate mockCertWithSanUri(String uri) throws CertificateExcepti
150151

151152
@Test
152153
public void getAgentIdentityCertificate_optedOut_returnsNullImmediately() throws IOException {
153-
envProvider.setEnv("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES", "true");
154+
envProvider.setEnv("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false");
154155
envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", "/non/existent/path");
155156
assertNull(AgentIdentityUtils.getAgentIdentityCertInfo());
156157
}
157158

159+
@Test
160+
public void getAgentIdentityCertificate_preventTokenSharingTrue_doesNotOptOut()
161+
throws Exception {
162+
envProvider.setEnv("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES", "true");
163+
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
164+
165+
URL certUrl = getClass().getClassLoader().getResource("agent/agent_spiffe_cert.pem");
166+
assertNotNull(certUrl, "Test resource agent/agent_spiffe_cert.pem not found");
167+
String certPath = Paths.get(certUrl.toURI()).toAbsolutePath().toString();
168+
Files.copy(Paths.get(certPath), tempDir.resolve("certificates.pem"));
169+
170+
URL keyUrl = getClass().getClassLoader().getResource("agent/agent_spiffe_key.pem");
171+
assertNotNull(keyUrl, "Test resource agent/agent_spiffe_key.pem not found");
172+
String keyPath = Paths.get(keyUrl.toURI()).toAbsolutePath().toString();
173+
Files.copy(Paths.get(keyPath), tempDir.resolve("private_key.pem"));
174+
175+
envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null);
176+
177+
AgentIdentityUtils.CertInfo info = AgentIdentityUtils.getAgentIdentityCertInfo();
178+
assertNotNull(info);
179+
assertEquals(
180+
new String(Files.readAllBytes(tempDir.resolve("certificates.pem")), StandardCharsets.UTF_8),
181+
info.getCertContent());
182+
}
183+
158184
@Test
159185
public void getAgentIdentityCertificate_noConfigEnvVar_returnsNull() throws IOException {
160186
AgentIdentityUtils.setTimeService(new FakeTimeService());
161187
assertNull(AgentIdentityUtils.getAgentIdentityCertInfo());
162188
}
163189

164190
@Test
165-
public void getAgentIdentityCertificate_happyPath_loadsCertificate() throws IOException {
191+
public void getAgentIdentityCertificate_happyPath_loadsCertificate() throws Exception {
166192
URL certUrl = getClass().getClassLoader().getResource("x509_leaf_certificate.pem");
167193
assertNotNull(certUrl, "Test resource x509_leaf_certificate.pem not found");
168-
String certPath = new File(certUrl.getFile()).getAbsolutePath();
194+
String certPath = Paths.get(certUrl.toURI()).toAbsolutePath().toString();
169195
File configFile = tempDir.resolve("config.json").toFile();
170196
String configJson =
171197
"{"
@@ -209,6 +235,26 @@ public void getAgentIdentityCertInfo_malformedJson_throwsIOException() throws IO
209235
}
210236
envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configFile.getAbsolutePath());
211237
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
238+
FakeTimeService fakeTime = new FakeTimeService();
239+
AgentIdentityUtils.setTimeService(fakeTime);
240+
241+
IOException e = assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo);
242+
assertTrue(e.getMessage().contains("Failed to parse Agent Identity config JSON"));
243+
assertEquals(0, fakeTime.getSleepCount());
244+
}
245+
246+
@Test
247+
public void getAgentIdentityCertInfo_configExists_certMissing_throwsIOExceptionAfterRetries()
248+
throws IOException {
249+
File configFile = tempDir.resolve("config.json").toFile();
250+
try (FileOutputStream fos = new FileOutputStream(configFile)) {
251+
String json =
252+
"{ \"cert_configs\": { \"workload\": { \"cert_path\": \"/non/existent/cert.pem\","
253+
+ " \"key_path\": \"/non/existent/key.pem\" } } }";
254+
fos.write(json.getBytes(StandardCharsets.UTF_8));
255+
}
256+
envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configFile.getAbsolutePath());
257+
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
212258
AgentIdentityUtils.setTimeService(new FakeTimeService());
213259

214260
IOException e = assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo);
@@ -244,15 +290,43 @@ public void shouldEnableMtls_unset_certsPresent_returnsTrue() throws IOException
244290
}
245291

246292
@Test
247-
public void getAgentIdentityCertInfo_fallbackPath_loadsCertificate() throws IOException {
248-
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
293+
public void shouldEnableMtls_true_noCertsNoConfig_returnsFalse() throws IOException {
294+
envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true");
295+
assertFalse(AgentIdentityUtils.shouldEnableMtls(false, false));
296+
}
249297

250-
URL certUrl = getClass().getClassLoader().getResource("x509_leaf_certificate.pem");
251-
assertNotNull(certUrl, "Test resource x509_leaf_certificate.pem not found");
252-
String certPath = new File(certUrl.getFile()).getAbsolutePath();
298+
@Test
299+
public void shouldEnableMtls_false_noCertsNoConfig_returnsFalse() throws IOException {
300+
envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false");
301+
assertFalse(AgentIdentityUtils.shouldEnableMtls(false, false));
302+
}
253303

304+
@Test
305+
public void shouldEnableMtls_unset_noCertsNoConfig_returnsFalse() throws IOException {
306+
envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", null);
307+
assertFalse(AgentIdentityUtils.shouldEnableMtls(false, false));
308+
}
309+
310+
@Test
311+
public void shouldEnableMtls_unset_certsMissing_configExists_throwsIOException() {
312+
envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", null);
313+
assertThrows(IOException.class, () -> AgentIdentityUtils.shouldEnableMtls(false, true));
314+
}
315+
316+
@Test
317+
public void getAgentIdentityCertInfo_fallbackPath_loadsCertificate() throws Exception {
318+
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
319+
320+
URL certUrl = getClass().getClassLoader().getResource("agent/agent_spiffe_cert.pem");
321+
assertNotNull(certUrl, "Test resource agent/agent_spiffe_cert.pem not found");
322+
String certPath = Paths.get(certUrl.toURI()).toAbsolutePath().toString();
254323
Files.copy(Paths.get(certPath), tempDir.resolve("certificates.pem"));
255324

325+
URL keyUrl = getClass().getClassLoader().getResource("agent/agent_spiffe_key.pem");
326+
assertNotNull(keyUrl, "Test resource agent/agent_spiffe_key.pem not found");
327+
String keyPath = Paths.get(keyUrl.toURI()).toAbsolutePath().toString();
328+
Files.copy(Paths.get(keyPath), tempDir.resolve("private_key.pem"));
329+
256330
envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null);
257331

258332
AgentIdentityUtils.CertInfo info = AgentIdentityUtils.getAgentIdentityCertInfo();
@@ -289,9 +363,9 @@ public void verifyKeyPair_mismatch_returnsFalse() throws Exception {
289363

290364
@Test
291365
public void getAgentIdentityCertInfo_mismatch_throwsIOExceptionAfterRetries() throws Exception {
292-
URL certUrl = getClass().getClassLoader().getResource("x509_leaf_certificate.pem");
293-
assertNotNull(certUrl, "Test resource x509_leaf_certificate.pem not found");
294-
String certPath = new File(certUrl.getFile()).getAbsolutePath();
366+
URL certUrl = getClass().getClassLoader().getResource("agent/agent_spiffe_cert.pem");
367+
assertNotNull(certUrl, "Test resource agent/agent_spiffe_cert.pem not found");
368+
String certPath = Paths.get(certUrl.toURI()).toAbsolutePath().toString();
295369

296370
// Generate a random key that won't match the cert
297371
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
@@ -339,8 +413,35 @@ public void getAgentIdentityCertInfo_mismatch_throwsIOExceptionAfterRetries() th
339413
assertEquals(200, fakeTime.currentTimeMillis()); // 2 retries * 100ms
340414
}
341415

416+
@Test
417+
public void getAgentIdentityCertInfo_wellKnownDirExistsNoFiles_notExplicitlyEnabled_returnsNullImmediately()
418+
throws IOException {
419+
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
420+
FakeTimeService fakeTime = new FakeTimeService();
421+
AgentIdentityUtils.setTimeService(fakeTime);
422+
envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null);
423+
envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", null);
424+
425+
assertNull(AgentIdentityUtils.getAgentIdentityCertInfo());
426+
assertEquals(0, fakeTime.getSleepCount());
427+
}
428+
429+
@Test
430+
public void getAgentIdentityCertInfo_wellKnownDirExistsNoFiles_explicitlyEnabled_retriesAndReturnsNull()
431+
throws IOException {
432+
AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/");
433+
FakeTimeService fakeTime = new FakeTimeService();
434+
AgentIdentityUtils.setTimeService(fakeTime);
435+
envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null);
436+
envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true");
437+
438+
assertNull(AgentIdentityUtils.getAgentIdentityCertInfo());
439+
assertTrue(fakeTime.getSleepCount() > 0);
440+
}
441+
342442
private static class FakeTimeService implements AgentIdentityUtils.TimeService {
343443
private final AtomicLong currentTime = new AtomicLong(0);
444+
private final AtomicInteger sleepCount = new AtomicInteger(0);
344445

345446
@Override
346447
public long currentTimeMillis() {
@@ -349,8 +450,13 @@ public long currentTimeMillis() {
349450

350451
@Override
351452
public void sleep(long millis) throws InterruptedException {
453+
sleepCount.incrementAndGet();
352454
currentTime.addAndGet(millis);
353455
}
456+
457+
int getSleepCount() {
458+
return sleepCount.get();
459+
}
354460
}
355461

356462
private static class TestEnvironmentProvider {

google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,21 @@
8282
import org.junit.jupiter.api.AfterEach;
8383
import org.junit.jupiter.api.BeforeEach;
8484
import org.junit.jupiter.api.Test;
85+
import org.junit.jupiter.api.io.TempDir;
8586

8687
/** Test case for {@link ComputeEngineCredentials}. */
8788
class ComputeEngineCredentialsTest extends BaseSerializationTest {
8889

8990
private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
9091

9192
private TestEnvironmentProvider envProvider;
92-
private Path tempDir;
93+
@TempDir private Path tempDir;
9394

9495
@BeforeEach
9596
void setUp() throws IOException {
9697
envProvider = new TestEnvironmentProvider();
9798
// Inject our test environment reader into AgentIdentityUtils
9899
AgentIdentityUtils.setEnvReader(envProvider::getEnv);
99-
tempDir = Files.createTempDirectory("compute_engine_creds_test");
100100

101101
// Speed up polling in tests by using a fake time service that advances time immediately
102102
final AtomicLong currentTime = new AtomicLong(0);
@@ -120,6 +120,7 @@ public void sleep(long millis) {
120120
void tearDown() {
121121
// Reset the mocks
122122
AgentIdentityUtils.resetTimeService();
123+
AgentIdentityUtils.setWellKnownDir("/var/run/secrets/workload-spiffe-credentials/");
123124
AgentIdentityUtils.setEnvReader(System::getenv);
124125
}
125126

google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,11 @@
6262
import com.google.gson.JsonParser;
6363
import com.google.gson.JsonSyntaxException;
6464
import java.io.IOException;
65+
import java.util.ArrayList;
6566
import java.util.Arrays;
6667
import java.util.List;
6768
import java.util.Map;
69+
import org.junit.jupiter.api.AfterAll;
6870
import org.junit.jupiter.api.AfterEach;
6971
import org.junit.jupiter.api.BeforeAll;
7072
import org.junit.jupiter.api.BeforeEach;
@@ -86,22 +88,33 @@ void setUp() {
8688
AgentIdentityUtils.setEnvReader(
8789
name -> {
8890
if ("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES".equals(name)) {
89-
return "true";
91+
return "false";
9092
}
9193
return null;
9294
});
9395
}
9496

97+
private final List<ch.qos.logback.classic.Logger> modifiedLoggers = new ArrayList<>();
98+
private final List<TestAppender> addedAppenders = new ArrayList<>();
99+
95100
@AfterEach
96101
void tearDown() {
97102
AgentIdentityUtils.setEnvReader(System::getenv);
103+
for (int i = 0; i < modifiedLoggers.size(); i++) {
104+
modifiedLoggers.get(i).detachAppender(addedAppenders.get(i));
105+
}
106+
modifiedLoggers.clear();
107+
addedAppenders.clear();
98108
}
99109

100110
private TestAppender setupTestLogger(Class<?> clazz) {
101111
TestAppender testAppender = new TestAppender();
102112
testAppender.start();
103-
Logger logger = LoggerFactory.getLogger(clazz);
104-
((ch.qos.logback.classic.Logger) logger).addAppender(testAppender);
113+
ch.qos.logback.classic.Logger logger =
114+
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger(clazz);
115+
logger.addAppender(testAppender);
116+
modifiedLoggers.add(logger);
117+
addedAppenders.add(testAppender);
105118
return testAppender;
106119
}
107120

@@ -113,6 +126,11 @@ static void setup() {
113126
LoggingUtils.setEnvironmentProvider(testEnvironmentProvider);
114127
}
115128

129+
@AfterAll
130+
static void tearDownAll() {
131+
LoggingUtils.setEnvironmentProvider(LoggingUtils.SystemEnvironmentProvider.getInstance());
132+
}
133+
116134
@Test
117135
void userCredentials_getRequestMetadata_fromRefreshToken_hasAccessToken() throws IOException {
118136
TestAppender testAppender = setupTestLogger(UserCredentials.class);

0 commit comments

Comments
 (0)