Skip to content

Commit b083558

Browse files
committed
Made changes to polling intervals for certificate load. Docstring changes.
1 parent 445498c commit b083558

File tree

3 files changed

+87
-49
lines changed

3 files changed

+87
-49
lines changed

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

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@
4848
import java.security.cert.CertificateFactory;
4949
import java.security.cert.CertificateParsingException;
5050
import java.security.cert.X509Certificate;
51-
import java.util.Collection;
52-
import java.util.List;
53-
import java.util.Map;
51+
import java.util.*;
5452
import java.util.logging.Level;
5553
import java.util.logging.Logger;
5654
import java.util.regex.Pattern;
@@ -67,15 +65,34 @@ final class AgentIdentityUtils {
6765
ImmutableList.of(
6866
Pattern.compile("^agents\\.global\\.org-\\d+\\.system\\.id\\.goog$"),
6967
Pattern.compile("^agents\\.global\\.proj-\\d+\\.system\\.id\\.goog$"));
68+
private static final int SAN_URI_TYPE = 6;
69+
private static final String SPIFFE_SCHEME_PREFIX = "spiffe://";
7070

7171
// Polling configuration
72-
private static final long TOTAL_TIMEOUT_MS = 30000; // 30 seconds
73-
private static final long FAST_POLL_DURATION_MS = 5000; // 5 seconds
72+
private static final int FAST_POLL_CYCLES = 50;
7473
private static final long FAST_POLL_INTERVAL_MS = 100; // 0.1 seconds
7574
private static final long SLOW_POLL_INTERVAL_MS = 500; // 0.5 seconds
75+
private static final long TOTAL_TIMEOUT_MS = 30000; // 30 seconds
76+
private static final List<Long> POLLING_INTERVALS;
7677

77-
private static final int SAN_URI_TYPE = 6;
78-
private static final String SPIFFE_SCHEME_PREFIX = "spiffe://";
78+
// Pre-calculates the sequence of polling intervals
79+
static {
80+
List<Long> intervals = new ArrayList<>();
81+
82+
for (int i = 0; i < FAST_POLL_CYCLES; i++) {
83+
intervals.add(FAST_POLL_INTERVAL_MS);
84+
}
85+
86+
long remainingTime = TOTAL_TIMEOUT_MS - (FAST_POLL_CYCLES * FAST_POLL_INTERVAL_MS);
87+
// Integer division is sufficient here as we want full cycles
88+
int slowPollCycles = (int) (remainingTime / SLOW_POLL_INTERVAL_MS);
89+
90+
for (int i = 0; i < slowPollCycles; i++) {
91+
intervals.add(SLOW_POLL_INTERVAL_MS);
92+
}
93+
94+
POLLING_INTERVALS = Collections.unmodifiableList(intervals);
95+
}
7996

8097
// Interface to allow mocking System.getenv for tests without exposing it publicly.
8198
interface EnvReader {
@@ -111,7 +128,8 @@ public void sleep(long millis) throws InterruptedException {
111128
private AgentIdentityUtils() {}
112129

113130
/**
114-
* Gets the Agent Identity certificate if available and enabled.
131+
* Gets the Agent Identity certificate if certificate is available and agent token sharing is not
132+
* disabled.
115133
*
116134
* @return The X509Certificate if found and Agent Identities are enabled, null otherwise.
117135
* @throws IOException If there is an error reading the certificate file after retries.
@@ -130,39 +148,46 @@ static X509Certificate getAgentIdentityCertificate() throws IOException {
130148
return parseCertificate(certPath);
131149
}
132150

133-
/** Checks if the user has opted out of Agent Token sharing. */
151+
/**
152+
* Checks if Agent Identity token sharing is disabled via an environment variable.
153+
*
154+
* @return {@code true} if the {@link #GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES}
155+
* variable is set to {@code "false"}, otherwise returns {@code false}.
156+
*/
134157
private static boolean isOptedOut() {
135158
String optOut = envReader.getEnv(GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES);
136159
return optOut != null && "false".equalsIgnoreCase(optOut);
137160
}
138161

139-
/** Polls for the certificate config file and the certificate file it references. */
162+
/**
163+
* Polls for the certificate config file and the certificate file it references, and returns the
164+
* certificate's path.
165+
*
166+
* <p>This method will retry for a total of {@link #TOTAL_TIMEOUT_MS} milliseconds before failing.
167+
*
168+
* @param certConfigPath The path to the certificate configuration JSON file.
169+
* @return The path to the certificate file extracted from the config.
170+
* @throws IOException If the files cannot be found after the timeout, or if the thread is
171+
* interrupted while waiting.
172+
*/
140173
private static String getCertificatePathWithRetry(String certConfigPath) throws IOException {
141-
long startTime = timeService.currentTimeMillis();
142174
boolean warned = false;
143175

144-
while (true) {
176+
// Deterministic polling loop based on pre-calculated intervals.
177+
for (long sleepInterval : POLLING_INTERVALS) {
145178
try {
146179
if (Files.exists(Paths.get(certConfigPath))) {
147180
String certPath = extractCertPathFromConfig(certConfigPath);
148181
if (!Strings.isNullOrEmpty(certPath) && Files.exists(Paths.get(certPath))) {
149182
return certPath;
150183
}
151184
}
152-
} catch (Exception e) {
153-
// Ignore exceptions during polling and retry
154-
LOGGER.log(Level.FINE, "Error while polling for certificate files");
155-
}
156-
157-
long elapsedTime = timeService.currentTimeMillis() - startTime;
158-
if (elapsedTime >= TOTAL_TIMEOUT_MS) {
159-
throw new IOException(
160-
"Certificate config or certificate file not found after multiple retries. "
161-
+ "Token binding protection is failing. You can turn off this protection by setting "
162-
+ GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES
163-
+ " to false to fall back to unbound tokens.");
185+
} catch (IOException e) {
186+
// Do not log here to prevent noise in the logs per iteration.
187+
// Fall through to the sleep logic to retry.
164188
}
165189

190+
// If we are here, we failed to find the certificate, log a warning only once.
166191
if (!warned) {
167192
LOGGER.warning(
168193
String.format(
@@ -172,17 +197,32 @@ private static String getCertificatePathWithRetry(String certConfigPath) throws
172197
warned = true;
173198
}
174199

200+
// Sleep before the next attempt.
175201
try {
176-
long sleepTime =
177-
elapsedTime < FAST_POLL_DURATION_MS ? FAST_POLL_INTERVAL_MS : SLOW_POLL_INTERVAL_MS;
178-
timeService.sleep(sleepTime);
202+
timeService.sleep(sleepInterval);
179203
} catch (InterruptedException e) {
180204
Thread.currentThread().interrupt();
181-
throw new IOException("Interrupted while waiting for certificate files", e);
205+
throw new IOException(
206+
"Interrupted while waiting for Agent Identity certificate files for bound token request.",
207+
e);
182208
}
183209
}
210+
211+
// If the loop completes without returning, we have timed out.
212+
throw new IOException(
213+
"Unable to find Agent Identity certificate config or file for bound token request after multiple retries. "
214+
+ "Token binding protection is failing. You can turn off this protection by setting "
215+
+ GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES
216+
+ " to false to fall back to unbound tokens.");
184217
}
185218

219+
/**
220+
* Parses the certificate configuration JSON file and extracts the path to the certificate.
221+
*
222+
* @param certConfigPath The path to the certificate configuration JSON file.
223+
* @return The certificate file path, or {@code null} if not found in the config.
224+
* @throws IOException If the configuration file cannot be read.
225+
*/
186226
@SuppressWarnings("unchecked")
187227
private static String extractCertPathFromConfig(String certConfigPath) throws IOException {
188228
try (InputStream stream = new FileInputStream(certConfigPath)) {
@@ -199,12 +239,20 @@ private static String extractCertPathFromConfig(String certConfigPath) throws IO
199239
return null;
200240
}
201241

242+
/**
243+
* Parses an X.509 certificate from the given file path.
244+
*
245+
* @param certPath The path to the certificate file.
246+
* @return The parsed {@link X509Certificate}.
247+
* @throws IOException If the certificate file cannot be read or parsed.
248+
*/
202249
private static X509Certificate parseCertificate(String certPath) throws IOException {
203250
try (InputStream stream = new FileInputStream(certPath)) {
204251
CertificateFactory cf = CertificateFactory.getInstance("X.509");
205252
return (X509Certificate) cf.generateCertificate(stream);
206253
} catch (GeneralSecurityException e) {
207-
throw new IOException("Failed to parse certificate", e);
254+
throw new IOException(
255+
"Failed to parse Agent Identity certificate for bound token request.", e);
208256
}
209257
}
210258

@@ -255,7 +303,7 @@ static String calculateCertificateFingerprint(X509Certificate cert) throws IOExc
255303
byte[] digest = md.digest();
256304
return BaseEncoding.base64Url().omitPadding().encode(digest);
257305
} catch (GeneralSecurityException e) {
258-
throw new IOException("Failed to calculate certificate fingerprint", e);
306+
throw new IOException("Failed to calculate fingerprint for Agent Identity certificate.", e);
259307
}
260308
}
261309

oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -347,20 +347,13 @@ private String getUniverseDomainFromMetadata() throws IOException {
347347
public AccessToken refreshAccessToken() throws IOException {
348348
String tokenUrl = createTokenUrlWithScopes();
349349

350-
try {
351-
X509Certificate cert = AgentIdentityUtils.getAgentIdentityCertificate();
352-
if (cert != null && AgentIdentityUtils.shouldRequestBoundToken(cert)) {
353-
String fingerprint = AgentIdentityUtils.calculateCertificateFingerprint(cert);
354-
GenericUrl url = new GenericUrl(tokenUrl);
355-
url.set("bindCertificateFingerprint", fingerprint);
356-
tokenUrl = url.build();
357-
}
358-
} catch (IOException e) {
359-
LOGGER.log(
360-
Level.WARNING,
361-
"Failed to process Agent Identity certificate for bound token request.",
362-
e);
363-
throw e;
350+
// Checks whether access token has to be bound to certificate for agent identity.
351+
X509Certificate cert = AgentIdentityUtils.getAgentIdentityCertificate();
352+
if (cert != null && AgentIdentityUtils.shouldRequestBoundToken(cert)) {
353+
String fingerprint = AgentIdentityUtils.calculateCertificateFingerprint(cert);
354+
GenericUrl url = new GenericUrl(tokenUrl);
355+
url.set("bindCertificateFingerprint", fingerprint);
356+
tokenUrl = url.build();
364357
}
365358

366359
HttpResponse response = getMetadataResponse(tokenUrl, RequestType.ACCESS_TOKEN_REQUEST, true);

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,13 +1258,11 @@ public void refreshAccessToken_withAgentCert_optedOut_requestsNormalToken() thro
12581258
public void refreshAccessToken_agentConfigMissingFile_throws() throws IOException {
12591259

12601260
// Point config to a non-existent file.
1261-
12621261
envProvider.setEnv(
12631262
AgentIdentityUtils.GOOGLE_API_CERTIFICATE_CONFIG,
12641263
tempDir.resolve("missing_config.json").toAbsolutePath().toString());
12651264

12661265
// Use a mock TimeService to avoid actual sleeping and control time flow.
1267-
12681266
final AtomicLong currentTime = new AtomicLong(0);
12691267

12701268
AgentIdentityUtils.setTimeService(
@@ -1284,17 +1282,16 @@ public void sleep(long millis) {
12841282
});
12851283

12861284
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
1287-
12881285
transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL);
1289-
12901286
ComputeEngineCredentials credentials =
12911287
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
12921288

12931289
IOException e = assertThrows(IOException.class, credentials::refreshAccessToken);
12941290

12951291
assertTrue(
12961292
e.getMessage()
1297-
.contains("Certificate config or certificate file not found after multiple retries"));
1293+
.contains(
1294+
"Unable to find Agent Identity certificate config or file for bound token request after multiple retries."));
12981295
}
12991296

13001297
private void setupCertConfig(String certResourceName) throws IOException {

0 commit comments

Comments
 (0)