From 302038f7179253d9a6747bb43027c2f9e745bb72 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 5 Nov 2025 23:48:10 -0800 Subject: [PATCH 1/6] Cloud run with tests. --- .../auth/oauth2/AgentIdentityUtils.java | 245 ++ .../auth/oauth2/ComputeEngineCredentials.java | 23 +- .../auth/oauth2/AgentIdentityUtilsTest.java | 243 ++ .../oauth2/ComputeEngineCredentialsTest.java | 2330 +++++++++-------- oauth2_http/testresources/agent_cert.pem | 19 + .../testresources/agent_spiffe_cert.pem | 19 + 6 files changed, 1801 insertions(+), 1078 deletions(-) create mode 100644 oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java create mode 100644 oauth2_http/testresources/agent_cert.pem create mode 100644 oauth2_http/testresources/agent_spiffe_cert.pem diff --git a/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java b/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java new file mode 100644 index 000000000..c490843d0 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java @@ -0,0 +1,245 @@ +/* + * Copyright 2025 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.oauth2; + +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonObjectParser; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.io.BaseEncoding; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** Internal utility class for handling Agent Identity certificate-bound access tokens. */ +final class AgentIdentityUtils { + private static final Logger LOGGER = Logger.getLogger(AgentIdentityUtils.class.getName()); + + static final String GOOGLE_API_CERTIFICATE_CONFIG = "GOOGLE_API_CERTIFICATE_CONFIG"; + static final String GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES = + "GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES"; + + private static final List AGENT_IDENTITY_SPIFFE_PATTERNS = + ImmutableList.of( + Pattern.compile("^agents\\.global\\.org-\\d+\\.system\\.id\\.goog$"), + Pattern.compile("^agents\\.global\\.proj-\\d+\\.system\\.id\\.goog$")); + + // Polling configuration + static long TOTAL_TIMEOUT_MS = 30000; // 30 seconds + static long FAST_POLL_DURATION_MS = 5000; // 5 seconds + static long FAST_POLL_INTERVAL_MS = 100; // 0.1 seconds + static long SLOW_POLL_INTERVAL_MS = 500; // 0.5 seconds + + private static final int SAN_URI_TYPE = 6; + private static final String SPIFFE_SCHEME_PREFIX = "spiffe://"; + + // Interface to allow mocking System.getenv for tests without exposing it publicly. + interface EnvReader { + String getEnv(String name); + } + + private static EnvReader envReader = System::getenv; + + private AgentIdentityUtils() {} + + /** + * Gets the Agent Identity certificate if available and enabled. + * + * @return The X509Certificate if found and Agent Identities are enabled, null otherwise. + * @throws IOException If there is an error reading the certificate file after retries. + */ + static X509Certificate getAgentIdentityCertificate() throws IOException { + if (isOptedOut()) { + return null; + } + + String certConfigPath = envReader.getEnv(GOOGLE_API_CERTIFICATE_CONFIG); + if (Strings.isNullOrEmpty(certConfigPath)) { + return null; + } + + String certPath = getCertificatePathWithRetry(certConfigPath); + return parseCertificate(certPath); + } + + /** Checks if the user has opted out of Agent Token sharing. */ + private static boolean isOptedOut() { + String optOut = envReader.getEnv(GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES); + return optOut != null && "false".equalsIgnoreCase(optOut); + } + + /** Polls for the certificate config file and the certificate file it references. */ + private static String getCertificatePathWithRetry(String certConfigPath) throws IOException { + long startTime = System.currentTimeMillis(); + boolean warned = false; + + while (true) { + try { + if (Files.exists(Paths.get(certConfigPath))) { + String certPath = extractCertPathFromConfig(certConfigPath); + if (!Strings.isNullOrEmpty(certPath) && Files.exists(Paths.get(certPath))) { + return certPath; + } + } + } catch (Exception e) { + // Ignore exceptions during polling and retry + LOGGER.log(Level.FINE, "Error while polling for certificate files", e); + } + + long elapsedTime = System.currentTimeMillis() - startTime; + if (elapsedTime >= TOTAL_TIMEOUT_MS) { + throw new IOException( + "Certificate config or certificate file not found after multiple retries. " + + "Token binding protection is failing. You can turn off this protection by setting " + + GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES + + " to false to fall back to unbound tokens."); + } + + if (!warned) { + LOGGER.warning( + String.format( + "Certificate config file not found at %s (from %s environment variable). " + + "Retrying for up to %d seconds.", + certConfigPath, + GOOGLE_API_CERTIFICATE_CONFIG, + TOTAL_TIMEOUT_MS / 1000)); + warned = true; + } + + try { + long sleepTime = + elapsedTime < FAST_POLL_DURATION_MS ? FAST_POLL_INTERVAL_MS : SLOW_POLL_INTERVAL_MS; + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for certificate files", e); + } + } + } + + @SuppressWarnings("unchecked") + private static String extractCertPathFromConfig(String certConfigPath) throws IOException { + try (InputStream stream = new FileInputStream(certConfigPath)) { + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); + GenericJson config = + parser.parseAndClose(stream, StandardCharsets.UTF_8, GenericJson.class); + Map certConfigs = (Map) config.get("cert_configs"); + if (certConfigs != null) { + Map workload = (Map) certConfigs.get("workload"); + if (workload != null) { + return (String) workload.get("cert_path"); + } + } + } + return null; + } + + private static X509Certificate parseCertificate(String certPath) throws IOException { + try (InputStream stream = new FileInputStream(certPath)) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(stream); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to parse certificate", e); + } + } + + /** Checks if the certificate belongs to an Agent Identity by inspecting SANs. */ + static boolean shouldRequestBoundToken(X509Certificate cert) { + try { + Collection> sans = cert.getSubjectAlternativeNames(); + if (sans == null) { + return false; + } + for (List san : sans) { + // SAN entry is a list where first element is the type (Integer) and second is value (mostly + // String) + if (san.size() >= 2 + && san.get(0) instanceof Integer + && (Integer) san.get(0) == SAN_URI_TYPE) { + Object value = san.get(1); + if (value instanceof String) { + String uri = (String) value; + if (uri.startsWith(SPIFFE_SCHEME_PREFIX)) { + // Extract trust domain: spiffe:///... + String withoutScheme = uri.substring(SPIFFE_SCHEME_PREFIX.length()); + int slashIndex = withoutScheme.indexOf('/'); + String trustDomain = + (slashIndex == -1) ? withoutScheme : withoutScheme.substring(0, slashIndex); + + for (Pattern pattern : AGENT_IDENTITY_SPIFFE_PATTERNS) { + if (pattern.matcher(trustDomain).matches()) { + return true; + } + } + } + } + } + } + } catch (CertificateParsingException e) { + LOGGER.log(Level.WARNING, "Failed to parse Subject Alternative Names from certificate", e); + } + return false; + } + + /** Calculates the SHA-256 fingerprint of the certificate, Base64Url encoded without padding. */ + static String calculateCertificateFingerprint(X509Certificate cert) throws IOException { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] der = cert.getEncoded(); + md.update(der); + byte[] digest = md.digest(); + return BaseEncoding.base64Url().omitPadding().encode(digest); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to calculate certificate fingerprint", e); + } + } + + @VisibleForTesting + static void setEnvReader(EnvReader reader) { + envReader = reader; + } +} \ No newline at end of file diff --git a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index 48a6fbe6b..300ef4939 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -62,6 +62,7 @@ import java.io.ObjectInputStream; import java.net.SocketTimeoutException; import java.net.UnknownHostException; +import java.security.cert.X509Certificate; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -344,8 +345,26 @@ private String getUniverseDomainFromMetadata() throws IOException { /** Refresh the access token by getting it from the GCE metadata server */ @Override public AccessToken refreshAccessToken() throws IOException { - HttpResponse response = - getMetadataResponse(createTokenUrlWithScopes(), RequestType.ACCESS_TOKEN_REQUEST, true); + String tokenUrl = createTokenUrlWithScopes(); + + try { + X509Certificate cert = + AgentIdentityUtils.getAgentIdentityCertificate(); + if (cert != null && AgentIdentityUtils.shouldRequestBoundToken(cert)) { + String fingerprint = AgentIdentityUtils.calculateCertificateFingerprint(cert); + GenericUrl url = new GenericUrl(tokenUrl); + url.set("bindCertificateFingerprint", fingerprint); + tokenUrl = url.build(); + } + } catch (IOException e) { + LOGGER.log( + Level.WARNING, + "Failed to process Agent Identity certificate for bound token request.", + e); + throw e; + } + + HttpResponse response = getMetadataResponse(tokenUrl, RequestType.ACCESS_TOKEN_REQUEST, true); int statusCode = response.getStatusCode(); if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { throw new IOException( diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java new file mode 100644 index 000000000..475d9afe2 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java @@ -0,0 +1,243 @@ +package com.google.auth.oauth2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AgentIdentityUtilsTest { + + private static final String VALID_SPIFFE_ORG = + "spiffe://agents.global.org-12345.system.id.goog/path/to/resource"; + private static final String VALID_SPIFFE_PROJ = + "spiffe://agents.global.proj-98765.system.id.goog/another/path"; + private static final String INVALID_SPIFFE_DOMAIN = "spiffe://example.com/workload"; + private static final String INVALID_SPIFFE_FORMAT = + "spiffe://agents.global.org-INVALID.system.id.goog/path"; + + // A minimal, valid self-signed X.509 certificate (PEM format) for testing loading. + // Generated for testing purposes. + private static final String TEST_CERT_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIDWTCCAkGgAwIBAgIUX5/1aT1uuxgj1+F7Q/r+5Q9y4JQwDQYJKoZIhvcNAQEL\n" + + "BQAwHTEbMBkGA1UEAwwSdGVzdC5leGFtcGxlLmNvbTAeFw0yNDAxMDEwMDAwMDBa\n" + + "Fw0zNDAxMDEwMDAwMDBaMB0xGzAZBgNVBAMMEnRlc3QuZXhhbXBsZS5jb20wggEi\n" + + "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDV/8Q/5+8+X9Y+5+6+7+8+9+0+\n" + + "A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/\n" + + "g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B\n" + + "/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h\n" + + "/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/\n" + + "D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/\n" + + "j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E\n" + + "AgMBAAGjUzBRMB0GA1UdDgQWBBS/1/2/3/4/5/6/7/8/9/+A/B/C/DAfBgNVHSME\n" + + "GDAWgBS/1/2/3/4/5/6/7/8/9/+A/B/C/DAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\n" + + "SIb3DQEBCwUAA4IBAQDV/8Q/5+8+X9Y+5+6+7+8+9+0+A/B/C/D/E/F/G/H/I/J/\n" + + "K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/\n" + + "q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E/F/G/H/I/J/K/L\n" + + "/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r\n" + + "/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E/F/G/H/I/J/K/L/M/\n" + + "N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/\n" + + "t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E\n" + + "-----END CERTIFICATE-----"; + + private TestEnvironmentProvider envProvider; + private Path tempDir; + + @Before + public void setUp() throws IOException { + envProvider = new TestEnvironmentProvider(); + // Inject our test environment reader + AgentIdentityUtils.setEnvReader(envProvider::getEnv); + tempDir = Files.createTempDirectory("agent_identity_test"); + } + + @After + public void tearDown() throws IOException { + // Reset polling constants to defaults after each test to avoid side effects + AgentIdentityUtils.TOTAL_TIMEOUT_MS = 30000; + AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 100; + AgentIdentityUtils.FAST_POLL_DURATION_MS = 5000; + AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 500; + + // Clean up temp files + if (tempDir != null) { + Files.walk(tempDir) + .sorted(java.util.Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } + + // --- 1. SPIFFE ID Validation Tests --- + + @Test + public void shouldRequestBoundToken_validOrgSpiffe_returnsTrue() throws CertificateException { + assertTrue(AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(VALID_SPIFFE_ORG))); + } + + @Test + public void shouldRequestBoundToken_validProjSpiffe_returnsTrue() throws CertificateException { + assertTrue(AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(VALID_SPIFFE_PROJ))); + } + + @Test + public void shouldRequestBoundToken_invalidDomain_returnsFalse() throws CertificateException { + assertFalse( + AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(INVALID_SPIFFE_DOMAIN))); + } + + @Test + public void shouldRequestBoundToken_invalidFormat_returnsFalse() throws CertificateException { + assertFalse( + AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(INVALID_SPIFFE_FORMAT))); + } + + @Test + public void shouldRequestBoundToken_noSan_returnsFalse() throws CertificateException { + X509Certificate mockCert = mock(X509Certificate.class); + when(mockCert.getSubjectAlternativeNames()).thenReturn(null); + assertFalse(AgentIdentityUtils.shouldRequestBoundToken(mockCert)); + } + + // Helper to create a mock cert with a specific URI in SAN + private X509Certificate mockCertWithSanUri(String uri) throws CertificateException { + X509Certificate mockCert = mock(X509Certificate.class); + // SAN entry type 6 is URI + List spiffeEntry = Arrays.asList(6, uri); + Collection> sans = Collections.singletonList(spiffeEntry); + when(mockCert.getSubjectAlternativeNames()).thenReturn(sans); + return mockCert; + } + + // --- 2. Fingerprint Calculation Tests --- + + @Test + public void calculateCertificateFingerprint_knownInput_returnsExpectedOutput() throws Exception { + // We mock the getEncoded() method to return a fixed byte array to guarantee a stable hash + // regardless of the actual certificate implementation details. + X509Certificate mockCert = mock(X509Certificate.class); + byte[] fakeDer = new byte[] {0x01, 0x02, 0x03, 0x04, (byte) 0xFF}; + when(mockCert.getEncoded()).thenReturn(fakeDer); + + // SHA-256 of {0x01, 0x02, 0x03, 0x04, 0xFF} is: + // fc402e5e4d71483c6d537984a30c2b4c8b065539a4bd1b026c6112926ba52793 + // Base64Url (no padding) of that hash is: + // _EAuXk1xSDxtU3mEowwrTIsGVTmkvRsCbGESkmulJ5M + String expectedFingerprint = "_EAuXk1xSDxtU3mEowwrTIsGVTmkvRsCbGESkmulJ5M"; + + String actualFingerprint = AgentIdentityUtils.calculateCertificateFingerprint(mockCert); + assertEquals(expectedFingerprint, actualFingerprint); + } + + // --- 3. Environmental Control Tests --- + + @Test + public void getAgentIdentityCertificate_optedOut_returnsNullImmediately() throws IOException { + envProvider.setEnv("GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); + // Set config to a non-existent path; if it tried to load it, it would fail or retry. + // Returning null immediately proves it respected the opt-out. + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", "/non/existent/path"); + + assertNull(AgentIdentityUtils.getAgentIdentityCertificate()); + } + + @Test + public void getAgentIdentityCertificate_noConfigEnvVar_returnsNull() throws IOException { + // Default opt-in is true, but no config env var is set. + assertNull(AgentIdentityUtils.getAgentIdentityCertificate()); + } + + // --- 4. Certificate Loading & Polling Tests --- + + @Test + public void getAgentIdentityCertificate_happyPath_loadsCertificate() throws IOException { + // Setup: Get the absolute path of the test resource. + URL certUrl = getClass().getClassLoader().getResource("agent_cert.pem"); + assertNotNull("Test resource agent_cert.pem not found", certUrl); + String certPath = new File(certUrl.getFile()).getAbsolutePath(); + + // Create config file pointing to the cert. + // We still need a temp file for the config because it must contain the absolute path + // to the certificate, which varies by machine. + File configFile = tempDir.resolve("config.json").toFile(); + String configJson = + "{" + + " \"cert_configs\": {" + + " \"workload\": {" + + " \"cert_path\": \"" + + certPath.replace("\\", "\\\\") + + "\"" + + " }" + + " }" + + "}"; + try (FileOutputStream fos = new FileOutputStream(configFile)) { + fos.write(configJson.getBytes(StandardCharsets.UTF_8)); + } + + // Configure environment + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configFile.getAbsolutePath()); + + // Execute + X509Certificate cert = AgentIdentityUtils.getAgentIdentityCertificate(); + + // Verify + assertNotNull(cert); + // Basic verification that it loaded OUR cert (checking issuer from agent_cert.pem) + assertTrue(cert.getIssuerDN().getName().contains("unit-tests")); + } + + @Test + public void getAgentIdentityCertificate_timeout_throwsIOException() { + // Setup: Set config path to something that doesn't exist + envProvider.setEnv( + "GOOGLE_API_CERTIFICATE_CONFIG", tempDir.resolve("missing.json").toAbsolutePath().toString()); + + // Reduce timeout to make test fast (e.g., 100ms total) + AgentIdentityUtils.TOTAL_TIMEOUT_MS = 100; + AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 10; + AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 10; + AgentIdentityUtils.FAST_POLL_DURATION_MS = 50; + + // Execute & Verify + IOException e = assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertificate); + assertTrue(e.getMessage().contains("Certificate config or certificate file not found after multiple retries")); + } + + // A helper class to mock System.getenv for testing purposes within this file. + private static class TestEnvironmentProvider { + private final java.util.Map env = new java.util.HashMap<>(); + + void setEnv(String key, String value) { + env.put(key, value); + } + + String getEnv(String key) { + return env.get(key); + } + } +} \ No newline at end of file diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index 4b1f9c1ca..4f5ddc0e9 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -57,19 +57,28 @@ import com.google.auth.TestUtils; import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.DefaultCredentialsProviderTest.MockRequestCountingTransportFactory; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -78,1082 +87,1251 @@ @RunWith(JUnit4.class) public class ComputeEngineCredentialsTest extends BaseSerializationTest { - private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo"); - - private static final String TOKEN_URL = - "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"; - - // Id Token which includes basic default claims - public static final String STANDARD_ID_TOKEN = - "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyO" - + "TNhZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Zvby5iYXIiL" - + "CJhenAiOiIxMDIxMDE1NTA4MzQyMDA3MDg1NjgiLCJleHAiOjE1NjQ0NzUwNTEsImlhdCI6MTU2NDQ3MTQ1MSwi" - + "aXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwNzA4NTY4In0" - + ".redacted"; - - // Id Token which includes GCE extended claims - public static final String FULL_ID_TOKEN = - "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyOTNh" - + "ZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Zvby5iYXIiLCJhe" - + "nAiOiIxMTIxNzkwNjI3MjAzOTEzMDU4ODUiLCJlbWFpbCI6IjEwNzEyODQxODQ0MzYtY29tcHV0ZUBkZXZlbG9wZ" - + "XIuZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NjQ1MTk0OTYsImdvb" - + "2dsZSI6eyJjb21wdXRlX2VuZ2luZSI6eyJpbnN0YW5jZV9jcmVhdGlvbl90aW1lc3RhbXAiOjE1NjMyMzA5MDcsI" - + "mluc3RhbmNlX2lkIjoiMzQ5Nzk3NDM5MzQ0MTE3OTI0MyIsImluc3RhbmNlX25hbWUiOiJpYW0iLCJwcm9qZWN0X" - + "2lkIjoibWluZXJhbC1taW51dGlhLTgyMCIsInByb2plY3RfbnVtYmVyIjoxMDcxMjg0MTg0NDM2LCJ6b25lIjoid" - + "XMtY2VudHJhbDEtYSJ9fSwiaWF0IjoxNTY0NTE1ODk2LCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb" - + "20iLCJzdWIiOiIxMTIxNzkwNjI3MjAzOTEzMDU4ODUifQ.redacted"; - - // Id Token which includes GCE extended claims and any VM License data (if applicable) - public static final String FULL_ID_TOKEN_WITH_LICENSES = - "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOG" - + "I3OTIyOTNhZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.ew0KICAiYXVkIjogImh0dHBzOi8" - + "vZm9vLmJhciIsDQogICJhenAiOiAiMTEyMTc5MDYyNzIwMzkxMzA1ODg1IiwNCiAgImVtYWlsIjogIjEyMzQ1Ni1" - + "jb21wdXRlQGRldmVsb3Blci5nc2VydmljZWFjY291bnQuY29tIiwNCiAgImVtYWlsX3ZlcmlmaWVkIjogdHJ1ZSw" - + "NCiAgImV4cCI6IDE1NjQ1MTk0OTYsDQogICJnb29nbGUiOiB7DQogICAgImNvbXB1dGVfZW5naW5lIjogew0KICA" - + "gICAgImluc3RhbmNlX2NyZWF0aW9uX3RpbWVzdGFtcCI6IDE1NjMyMzA5MDcsDQogICAgICAiaW5zdGFuY2VfaWQ" - + "iOiAiMzQ5Nzk3NDM5MzQ0MTE3OTI0MyIsDQogICAgICAiaW5zdGFuY2VfbmFtZSI6ICJpYW0iLA0KICAgICAgImx" - + "pY2Vuc2VfaWQiOiBbDQogICAgICAgICIxMDAxMDAwIiwNCiAgICAgICAgIjEwMDEwMDEiLA0KICAgICAgICAiMTA" - + "wMTAwOCINCiAgICAgIF0sDQogICAgICAicHJvamVjdF9pZCI6ICJmb28tYmFyLTgyMCIsDQogICAgICAicHJvamV" - + "jdF9udW1iZXIiOiAxMDcxMjg0MTg0NDM2LA0KICAgICAgInpvbmUiOiAidXMtY2VudHJhbDEtYSINCiAgICB9DQo" - + "gIH0sDQogICJpYXQiOiAxNTY0NTE1ODk2LA0KICAiaXNzIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSI" - + "sDQogICJzdWIiOiAiMTEyMTc5MDYyNzIwMzkxMzA1ODg1Ig0KfQ.redacted"; - private static final String ACCESS_TOKEN = "1/MkSJoj1xsli0AccessToken_NKPY2"; - private static final List SCOPES = Arrays.asList("foo", "bar"); - private static final String ACCESS_TOKEN_WITH_SCOPES = "1/MkSJoj1xsli0AccessTokenScoped_NKPY2"; - private static final Map SCOPE_TO_ACCESS_TOKEN_MAP = - Stream.of( - new String[][] { - {"default", ACCESS_TOKEN}, - {SCOPES.toString().replaceAll("\\s", ""), ACCESS_TOKEN_WITH_SCOPES}, - }) - .collect(Collectors.toMap(data -> data[0], data -> data[1])); - - @Test - public void buildTokenUrlWithScopes_null_scopes() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setScopes(null).build(); - Collection scopes = credentials.getScopes(); - String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL, tokenUrlWithScopes); - assertTrue(scopes.isEmpty()); - } - - @Test - public void buildTokenUrlWithScopes_empty_scopes() { - ComputeEngineCredentials.Builder builder = - ComputeEngineCredentials.newBuilder().setScopes(Collections.emptyList()); - ComputeEngineCredentials credentials = builder.build(); - Collection scopes = credentials.getScopes(); - String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL, tokenUrlWithScopes); - assertTrue(scopes.isEmpty()); - assertTrue(builder.getScopes().isEmpty()); - } - - @Test - public void buildTokenUrlWithScopes_single_scope() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setScopes(Arrays.asList("foo")).build(); - String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); - Collection scopes = credentials.getScopes(); - - assertEquals(TOKEN_URL + "?scopes=foo", tokenUrlWithScopes); - assertEquals(1, scopes.size()); - assertEquals("foo", scopes.toArray()[0]); - } - - @Test - public void buildTokenUrlWithScopes_multiple_scopes() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setScopes(Arrays.asList(null, "foo", "", "bar")) - .build(); - Collection scopes = credentials.getScopes(); - String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?scopes=foo,bar", tokenUrlWithScopes); - assertEquals(2, scopes.size()); - assertEquals("foo", scopes.toArray()[0]); - assertEquals("bar", scopes.toArray()[1]); - } - - @Test - public void buildTokenUrlWithScopes_defaultScopes() { - ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().build(); - credentials = - (ComputeEngineCredentials) - credentials.createScoped(null, Arrays.asList(null, "foo", "", "bar")); - Collection scopes = credentials.getScopes(); - String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?scopes=foo,bar", tokenUrlWithScopes); - assertEquals(2, scopes.size()); - assertEquals("foo", scopes.toArray()[0]); - assertEquals("bar", scopes.toArray()[1]); - } - - @Test - public void buildTokenUrl_nullTransport() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(null) - .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) - .build(); - String tokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?binding-enforcement=on", tokenUrl); - } - - @Test - public void buildTokenUrl_nullBindingEnforcement() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) - .setBindingEnforcement(null) - .build(); - String tokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?transport=mtls", tokenUrl); - } - - @Test - public void buildTokenUrl_nullTransport_nullBindingEnforcement() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(null) - .setBindingEnforcement(null) - .build(); - String softBoundTokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL, softBoundTokenUrl); - } - - @Test - public void buildTokenUrl_mtls_transport() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) - .build(); - String tokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?transport=mtls", tokenUrl); - } - - @Test - public void buildTokenUrl_iam_enforcement() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.IAM_POLICY) - .build(); - String tokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?binding-enforcement=iam-policy", tokenUrl); - } - - @Test - public void buildTokenUrlSoftMtlsBound_mtls_transport_iam_enforcement() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) - .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.IAM_POLICY) - .build(); - String softBoundTokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?transport=mtls&binding-enforcement=iam-policy", softBoundTokenUrl); - } - - @Test - public void buildTokenUrl_always_enforced() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) - .build(); - String tokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?binding-enforcement=on", tokenUrl); - } - - @Test - public void buildTokenUrlHardMtlsBound_mtls_transport_always_enforced() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) - .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) - .build(); - String hardBoundTokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?transport=mtls&binding-enforcement=on", hardBoundTokenUrl); - } - - @Test - public void buildTokenUrlHardDirectPathBound_alts_transport() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.ALTS) - .build(); - String hardBoundTokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?transport=alts", hardBoundTokenUrl); - } - - @Test - public void buildScoped_scopesPresent() throws IOException { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setScopes(null).build(); - ComputeEngineCredentials scopedCredentials = - (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); - Collection scopes = scopedCredentials.getScopes(); - - assertEquals(1, scopes.size()); - assertEquals("foo", scopes.toArray()[0]); - } - - @Test - public void buildScoped_correctMargins() throws IOException { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setScopes(null).build(); - ComputeEngineCredentials scopedCredentials = - (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); - - assertEquals( - ComputeEngineCredentials.COMPUTE_EXPIRATION_MARGIN, - scopedCredentials.getExpirationMargin()); - assertEquals( - ComputeEngineCredentials.COMPUTE_REFRESH_MARGIN, scopedCredentials.getRefreshMargin()); - } - - @Test - public void buildScoped_explicitUniverse() throws IOException { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setScopes(null) - .setUniverseDomain("some-universe") - .build(); - ComputeEngineCredentials scopedCredentials = - (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); - - assertEquals("some-universe", scopedCredentials.getUniverseDomain()); - assertTrue(scopedCredentials.isExplicitUniverseDomain()); - } - - @Test - public void createScoped_defaultScopes() { - GoogleCredentials credentials = - ComputeEngineCredentials.create().createScoped(null, Arrays.asList("foo")); - Collection scopes = ((ComputeEngineCredentials) credentials).getScopes(); - - assertEquals(1, scopes.size()); - assertEquals("foo", scopes.toArray()[0]); - } - - @Test - public void buildScoped_quotaProjectId() throws IOException { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setScopes(null) - .setQuotaProjectId("some-project-id") - .build(); - ComputeEngineCredentials scopedCredentials = - (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); - - assertEquals("some-project-id", scopedCredentials.getQuotaProjectId()); - } - - @Test - public void buildDefaultScoped_explicitUniverse() throws IOException { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setScopes(null) - .setUniverseDomain("some-universe") - .build(); - ComputeEngineCredentials scopedCredentials = - (ComputeEngineCredentials) credentials.createScoped(null, Arrays.asList("foo")); - - assertEquals("some-universe", scopedCredentials.getUniverseDomain()); - assertTrue(scopedCredentials.isExplicitUniverseDomain()); - } - - @Test - public void create_scoped_correctMargins() { - GoogleCredentials credentials = - ComputeEngineCredentials.create().createScoped(null, Arrays.asList("foo")); - - assertEquals( - ComputeEngineCredentials.COMPUTE_EXPIRATION_MARGIN, credentials.getExpirationMargin()); - assertEquals(ComputeEngineCredentials.COMPUTE_REFRESH_MARGIN, credentials.getRefreshMargin()); - } - - @Test - public void getRequestMetadata_hasAccessToken() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - Map> metadata = credentials.getRequestMetadata(CALL_URI); - - TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); - // verify metrics header added and other header intact - Map> requestHeaders = transportFactory.transport.getRequest().getHeaders(); - com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "at", "mds"); - assertTrue(requestHeaders.containsKey("metadata-flavor")); - assertTrue(requestHeaders.get("metadata-flavor").contains("Google")); - } - - @Test - public void getRequestMetadata_shouldInvalidateAccessTokenWhenScoped_newAccessTokenFromRefresh() - throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - Map> metadata = credentials.getRequestMetadata(CALL_URI); - - TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); - - assertNotNull(credentials.getAccessToken()); - ComputeEngineCredentials scopedCredentialCopy = - (ComputeEngineCredentials) credentials.createScoped(SCOPES); - assertNull(scopedCredentialCopy.getAccessToken()); - Map> metadataForCopiedCredentials = - scopedCredentialCopy.getRequestMetadata(CALL_URI); - TestUtils.assertContainsBearerToken(metadataForCopiedCredentials, ACCESS_TOKEN_WITH_SCOPES); - TestUtils.assertNotContainsBearerToken(metadataForCopiedCredentials, ACCESS_TOKEN); - } - - @Test - public void getRequestMetadata_missingServiceAccount_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND); - transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - try { - credentials.getRequestMetadata(CALL_URI); - fail("Expected error refreshing token."); - } catch (IOException expected) { - String message = expected.getMessage(); - assertTrue(message.contains(Integer.toString(HttpStatusCodes.STATUS_CODE_NOT_FOUND))); - // Message should mention scopes are missing on the VM. - assertTrue(message.contains("scope")); - } - } - - @Test - public void getRequestMetadata_serverError_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR); - transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - try { - credentials.getRequestMetadata(CALL_URI); - fail("Expected error refreshing token."); - } catch (IOException expected) { - String message = expected.getMessage(); - assertTrue(message.contains(Integer.toString(HttpStatusCodes.STATUS_CODE_SERVER_ERROR))); - assertTrue(message.contains("Unexpected")); - } - } - - @Test - public void equals_true() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - ComputeEngineCredentials explicitUniverseCredentials = - ComputeEngineCredentials.newBuilder() - .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) - .setHttpTransportFactory(transportFactory) - .build(); - ComputeEngineCredentials otherCredentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, otherCredentials.getUniverseDomain()); - assertFalse(explicitUniverseCredentials.equals(otherCredentials)); - assertFalse(otherCredentials.equals(explicitUniverseCredentials)); - ComputeEngineCredentials otherExplicitUniverseCredentials = - ComputeEngineCredentials.newBuilder() - .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) - .setHttpTransportFactory(transportFactory) - .build(); - assertTrue(explicitUniverseCredentials.equals(otherExplicitUniverseCredentials)); - assertTrue(otherExplicitUniverseCredentials.equals(explicitUniverseCredentials)); - } - - @Test - public void equals_false_transportFactory() throws IOException { - MockHttpTransportFactory httpTransportFactory = new MockHttpTransportFactory(); - MockMetadataServerTransportFactory serverTransportFactory = - new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(serverTransportFactory) - .build(); - ComputeEngineCredentials otherCredentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(httpTransportFactory).build(); - assertFalse(credentials.equals(otherCredentials)); - assertFalse(otherCredentials.equals(credentials)); - } - - @Test - public void toString_explicit_containsFields() throws IOException { - MockMetadataServerTransportFactory serverTransportFactory = - new MockMetadataServerTransportFactory(); - String expectedToString = - String.format( - "ComputeEngineCredentials{quotaProjectId=%s, universeDomain=%s, isExplicitUniverseDomain=%s, transportFactoryClassName=%s, scopes=%s}", - "some-project", - "some-domain", - true, - MockMetadataServerTransportFactory.class.getName(), - "[some scope]"); - GoogleCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(serverTransportFactory) - .setQuotaProjectId("some-project") - .setUniverseDomain("some-domain") - .build(); - credentials = credentials.createScoped("some scope"); - assertEquals(expectedToString, credentials.toString()); - } - - @Test - public void hashCode_equals() throws IOException { - MockMetadataServerTransportFactory serverTransportFactory = - new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(serverTransportFactory) - .build(); - ComputeEngineCredentials otherCredentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(serverTransportFactory) - .build(); - assertEquals(credentials.hashCode(), otherCredentials.hashCode()); - } - - @Test - public void toBuilder() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(new MockMetadataServerTransportFactory()) - .setQuotaProjectId("quota-project") - .build(); - - ComputeEngineCredentials secondCredentials = credentials.toBuilder().build(); - - assertEquals(credentials, secondCredentials); - } - - @Test - public void serialize() throws IOException, ClassNotFoundException { - MockMetadataServerTransportFactory serverTransportFactory = - new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(serverTransportFactory) - .build(); - GoogleCredentials deserializedCredentials = serializeAndDeserialize(credentials); - assertEquals(credentials, deserializedCredentials); - assertEquals(credentials.hashCode(), deserializedCredentials.hashCode()); - assertEquals(credentials.toString(), deserializedCredentials.toString()); - assertSame(deserializedCredentials.clock, Clock.SYSTEM); - credentials = ComputeEngineCredentials.newBuilder().build(); - deserializedCredentials = serializeAndDeserialize(credentials); - assertEquals(credentials, deserializedCredentials); - assertEquals(credentials.hashCode(), deserializedCredentials.hashCode()); - assertEquals(credentials.toString(), deserializedCredentials.toString()); - assertSame(deserializedCredentials.clock, Clock.SYSTEM); - } - - @Test - public void getAccount_sameAs() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - assertEquals(defaultAccountEmail, credentials.getAccount()); - - // metric headers are not supported for getAccount() - Map> headers = transportFactory.transport.getRequest().getHeaders(); - assertFalse(headers.containsKey(MetricsUtils.API_CLIENT_HEADER)); - } - - @Test - public void getAccount_missing_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - if (isGetServiceAccountsUrl(url)) { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND) - .setContent(""); - } - }; - } - return super.buildRequest(method, url); - } - }; - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - try { - credentials.getAccount(); - fail("Fetching default service account should have failed"); - } catch (RuntimeException e) { - assertEquals("Failed to get service account", e.getMessage()); - assertNotNull(e.getCause()); - assertTrue(e.getCause().getMessage().contains("404")); - } - } - - @Test - public void getAccount_emptyContent_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - if (isGetServiceAccountsUrl(url)) { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_OK); - } - }; - } - return super.buildRequest(method, url); - } - }; - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - try { - credentials.getAccount(); - fail("Fetching default service account should have failed"); - } catch (RuntimeException e) { - assertEquals("Failed to get service account", e.getMessage()); - assertNotNull(e.getCause()); - assertTrue(e.getCause().getMessage().contains("Empty content")); - } - } - - @Test - public void sign_sameAs() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; - - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - transportFactory.transport.setSignature(expectedSignature); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - assertArrayEquals(expectedSignature, credentials.sign(expectedSignature)); - } - - @Test - public void sign_getUniverseException() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - - String defaultAccountEmail = "mail@mail.com"; - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - transportFactory.transport.setStatusCode(501); - Assert.assertThrows(IOException.class, credentials::getUniverseDomain); - - byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; - SigningException signingException = - Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature)); - assertEquals("Failed to sign: Error obtaining universe domain", signingException.getMessage()); - } - - @Test - public void sign_getAccountFails() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; - - transportFactory.transport.setSignature(expectedSignature); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - SigningException exception = - Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature)); - assertNotNull(exception.getMessage()); - assertNotNull(exception.getCause()); - } - - @Test - public void sign_accessDenied_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - if (isSignRequestUrl(url)) { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_FORBIDDEN) - .setContent(TestUtils.errorJson("Sign Error")); - } - }; - } - return super.buildRequest(method, url); - } - }; - - transportFactory.transport.setAccessToken(ACCESS_TOKEN); - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - byte[] bytes = {0xD, 0xE, 0xA, 0xD}; - - SigningException exception = - Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); - assertEquals("Failed to sign the provided bytes", exception.getMessage()); - assertNotNull(exception.getCause()); - assertTrue(exception.getCause().getMessage().contains("403")); - } - - @Test - public void sign_serverError_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - if (isSignRequestUrl(url)) { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) - .setContent(TestUtils.errorJson("Sign Error")); - } - }; + private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo"); + + private static final String TOKEN_URL = + "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"; + + // Id Token which includes basic default claims + public static final String STANDARD_ID_TOKEN = + "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyO" + + "TNhZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Zvby5iYXIiL" + + "CJhenAiOiIxMDIxMDE1NTA4MzQyMDA3MDg1NjgiLCJleHAiOjE1NjQ0NzUwNTEsImlhdCI6MTU2NDQ3MTQ1MSwi" + + "aXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwNzA4NTY4In0" + + ".redacted"; + + // Id Token which includes GCE extended claims + public static final String FULL_ID_TOKEN = + "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyOTNh" + + "ZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Zvby5iYXIiLCJhe" + + "nAiOiIxMTIxNzkwNjI3MjAzOTEzMDU4ODUiLCJlbWFpbCI6IjEwNzEyODQxODQ0MzYtY29tcHV0ZUBkZXZlbG9wZ" + + "XIuZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NjQ1MTk0OTYsImdvb" + + "2dsZSI6eyJjb21wdXRlX2VuZ2luZSI6eyJpbnN0YW5jZV9jcmVhdGlvbl90aW1lc3RhbXAiOjE1NjMyMzA5MDcsI" + + "mluc3RhbmNlX2lkIjoiMzQ5Nzk3NDM5MzQ0MTE3OTI0MyIsImluc3RhbmNlX25hbWUiOiJpYW0iLCJwcm9qZWN0X" + + "2lkIjoibWluZXJhbC1taW51dGlhLTgyMCIsInByb2plY3RfbnVtYmVyIjoxMDcxMjg0MTg0NDM2LCJ6b25lIjoid" + + "XMtY2VudHJhbDEtYSJ9fSwiaWF0IjoxNTY0NTE1ODk2LCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb" + + "20iLCJzdWIiOiIxMTIxNzkwNjI3MjAzOTEzMDU4ODUifQ.redacted"; + + // Id Token which includes GCE extended claims and any VM License data (if applicable) + public static final String FULL_ID_TOKEN_WITH_LICENSES = + "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOG" + + "I3OTIyOTNhZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.ew0KICAiYXVkIjogImh0dHBzOi8" + + "vZm9vLmJhciIsDQogICJhenAiOiAiMTEyMTc5MDYyNzIwMzkxMzA1ODg1IiwNCiAgImVtYWlsIjogIjEyMzQ1Ni1" + + "jb21wdXRlQGRldmVsb3Blci5nc2VydmljZWFjY291bnQuY29tIiwNCiAgImVtYWlsX3ZlcmlmaWVkIjogdHJ1ZSw" + + "NCiAgImV4cCI6IDE1NjQ1MTk0OTYsDQogICJnb29nbGUiOiB7DQogICAgImNvbXB1dGVfZW5naW5lIjogew0KICA" + + "gICAgImluc3RhbmNlX2NyZWF0aW9uX3RpbWVzdGFtcCI6IDE1NjMyMzA5MDcsDQogICAgICAiaW5zdGFuY2VfaWQ" + + "iOiAiMzQ5Nzk3NDM5MzQ0MTE3OTI0MyIsDQogICAgICAiaW5zdGFuY2VfbmFtZSI6ICJpYW0iLA0KICAgICAgImx" + + "pY2Vuc2VfaWQiOiBbDQogICAgICAgICIxMDAxMDAwIiwNCiAgICAgICAgIjEwMDEwMDEiLA0KICAgICAgICAiMTA" + + "wMTAwOCINCiAgICAgIF0sDQogICAgICAicHJvamVjdF9pZCI6ICJmb28tYmFyLTgyMCIsDQogICAgICAicHJvamV" + + "jdF9udW1iZXIiOiAxMDcxMjg0MTg0NDM2LA0KICAgICAgInpvbmUiOiAidXMtY2VudHJhbDEtYSINCiAgICB9DQo" + + "gIH0sDQogICJpYXQiOiAxNTY0NTE1ODk2LA0KICAiaXNzIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSI" + + "sDQogICJzdWIiOiAiMTEyMTc5MDYyNzIwMzkxMzA1ODg1Ig0KfQ.redacted"; + private static final String ACCESS_TOKEN = "1/MkSJoj1xsli0AccessToken_NKPY2"; + private static final List SCOPES = Arrays.asList("foo", "bar"); + private static final String ACCESS_TOKEN_WITH_SCOPES = "1/MkSJoj1xsli0AccessTokenScoped_NKPY2"; + private static final Map SCOPE_TO_ACCESS_TOKEN_MAP = + Stream.of( + new String[][] { + {"default", ACCESS_TOKEN}, + {SCOPES.toString().replaceAll("\\s", ""), ACCESS_TOKEN_WITH_SCOPES}, + }) + .collect(Collectors.toMap(data -> data[0], data -> data[1])); + + private TestEnvironmentProvider envProvider; + private Path tempDir; + + @Before + public void setUp() throws IOException { + envProvider = new TestEnvironmentProvider(); + // Inject our test environment reader into AgentIdentityUtils + AgentIdentityUtils.setEnvReader(envProvider::getEnv); + tempDir = Files.createTempDirectory("compute_engine_creds_test"); + } + + @After + public void tearDown() throws IOException { + // Reset polling constants to defaults after each test + AgentIdentityUtils.TOTAL_TIMEOUT_MS = 30000; + AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 100; + AgentIdentityUtils.FAST_POLL_DURATION_MS = 5000; + AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 500; + + if (tempDir != null) { + Files.walk(tempDir) + .sorted(java.util.Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } + + @Test + public void buildTokenUrlWithScopes_null_scopes() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setScopes(null).build(); + Collection scopes = credentials.getScopes(); + String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL, tokenUrlWithScopes); + assertTrue(scopes.isEmpty()); + } + + @Test + public void buildTokenUrlWithScopes_empty_scopes() { + ComputeEngineCredentials.Builder builder = + ComputeEngineCredentials.newBuilder().setScopes(Collections.emptyList()); + ComputeEngineCredentials credentials = builder.build(); + Collection scopes = credentials.getScopes(); + String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL, tokenUrlWithScopes); + assertTrue(scopes.isEmpty()); + assertTrue(builder.getScopes().isEmpty()); + } + + @Test + public void buildTokenUrlWithScopes_single_scope() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setScopes(Arrays.asList("foo")).build(); + String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); + Collection scopes = credentials.getScopes(); + + assertEquals(TOKEN_URL + "?scopes=foo", tokenUrlWithScopes); + assertEquals(1, scopes.size()); + assertEquals("foo", scopes.toArray()[0]); + } + + @Test + public void buildTokenUrlWithScopes_multiple_scopes() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setScopes(Arrays.asList(null, "foo", "", "bar")) + .build(); + Collection scopes = credentials.getScopes(); + String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?scopes=foo,bar", tokenUrlWithScopes); + assertEquals(2, scopes.size()); + assertEquals("foo", scopes.toArray()[0]); + assertEquals("bar", scopes.toArray()[1]); + } + + @Test + public void buildTokenUrlWithScopes_defaultScopes() { + ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().build(); + credentials = + (ComputeEngineCredentials) + credentials.createScoped(null, Arrays.asList(null, "foo", "", "bar")); + Collection scopes = credentials.getScopes(); + String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?scopes=foo,bar", tokenUrlWithScopes); + assertEquals(2, scopes.size()); + assertEquals("foo", scopes.toArray()[0]); + assertEquals("bar", scopes.toArray()[1]); + } + + @Test + public void buildTokenUrl_nullTransport() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(null) + .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) + .build(); + String tokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?binding-enforcement=on", tokenUrl); + } + + @Test + public void buildTokenUrl_nullBindingEnforcement() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) + .setBindingEnforcement(null) + .build(); + String tokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?transport=mtls", tokenUrl); + } + + @Test + public void buildTokenUrl_nullTransport_nullBindingEnforcement() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(null) + .setBindingEnforcement(null) + .build(); + String softBoundTokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL, softBoundTokenUrl); + } + + @Test + public void buildTokenUrl_mtls_transport() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) + .build(); + String tokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?transport=mtls", tokenUrl); + } + + @Test + public void buildTokenUrl_iam_enforcement() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.IAM_POLICY) + .build(); + String tokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?binding-enforcement=iam-policy", tokenUrl); + } + + @Test + public void buildTokenUrlSoftMtlsBound_mtls_transport_iam_enforcement() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) + .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.IAM_POLICY) + .build(); + String softBoundTokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?transport=mtls&binding-enforcement=iam-policy", softBoundTokenUrl); + } + + @Test + public void buildTokenUrl_always_enforced() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) + .build(); + String tokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?binding-enforcement=on", tokenUrl); + } + + @Test + public void buildTokenUrlHardMtlsBound_mtls_transport_always_enforced() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) + .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) + .build(); + String hardBoundTokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?transport=mtls&binding-enforcement=on", hardBoundTokenUrl); + } + + @Test + public void buildTokenUrlHardDirectPathBound_alts_transport() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.ALTS) + .build(); + String hardBoundTokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?transport=alts", hardBoundTokenUrl); + } + + @Test + public void buildScoped_scopesPresent() throws IOException { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setScopes(null).build(); + ComputeEngineCredentials scopedCredentials = + (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); + Collection scopes = scopedCredentials.getScopes(); + + assertEquals(1, scopes.size()); + assertEquals("foo", scopes.toArray()[0]); + } + + @Test + public void buildScoped_correctMargins() throws IOException { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setScopes(null).build(); + ComputeEngineCredentials scopedCredentials = + (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); + + assertEquals( + ComputeEngineCredentials.COMPUTE_EXPIRATION_MARGIN, + scopedCredentials.getExpirationMargin()); + assertEquals( + ComputeEngineCredentials.COMPUTE_REFRESH_MARGIN, scopedCredentials.getRefreshMargin()); + } + + @Test + public void buildScoped_explicitUniverse() throws IOException { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setScopes(null) + .setUniverseDomain("some-universe") + .build(); + ComputeEngineCredentials scopedCredentials = + (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); + + assertEquals("some-universe", scopedCredentials.getUniverseDomain()); + assertTrue(scopedCredentials.isExplicitUniverseDomain()); + } + + @Test + public void createScoped_defaultScopes() { + GoogleCredentials credentials = + ComputeEngineCredentials.create().createScoped(null, Arrays.asList("foo")); + Collection scopes = ((ComputeEngineCredentials) credentials).getScopes(); + + assertEquals(1, scopes.size()); + assertEquals("foo", scopes.toArray()[0]); + } + + @Test + public void buildScoped_quotaProjectId() throws IOException { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setScopes(null) + .setQuotaProjectId("some-project-id") + .build(); + ComputeEngineCredentials scopedCredentials = + (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); + + assertEquals("some-project-id", scopedCredentials.getQuotaProjectId()); + } + + @Test + public void buildDefaultScoped_explicitUniverse() throws IOException { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setScopes(null) + .setUniverseDomain("some-universe") + .build(); + ComputeEngineCredentials scopedCredentials = + (ComputeEngineCredentials) credentials.createScoped(null, Arrays.asList("foo")); + + assertEquals("some-universe", scopedCredentials.getUniverseDomain()); + assertTrue(scopedCredentials.isExplicitUniverseDomain()); + } + + @Test + public void create_scoped_correctMargins() { + GoogleCredentials credentials = + ComputeEngineCredentials.create().createScoped(null, Arrays.asList("foo")); + + assertEquals( + ComputeEngineCredentials.COMPUTE_EXPIRATION_MARGIN, credentials.getExpirationMargin()); + assertEquals(ComputeEngineCredentials.COMPUTE_REFRESH_MARGIN, credentials.getRefreshMargin()); + } + + @Test + public void getRequestMetadata_hasAccessToken() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + Map> metadata = credentials.getRequestMetadata(CALL_URI); + + TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); + // verify metrics header added and other header intact + Map> requestHeaders = transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "at", "mds"); + assertTrue(requestHeaders.containsKey("metadata-flavor")); + assertTrue(requestHeaders.get("metadata-flavor").contains("Google")); + } + + @Test + public void getRequestMetadata_shouldInvalidateAccessTokenWhenScoped_newAccessTokenFromRefresh() + throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + Map> metadata = credentials.getRequestMetadata(CALL_URI); + + TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); + + assertNotNull(credentials.getAccessToken()); + ComputeEngineCredentials scopedCredentialCopy = + (ComputeEngineCredentials) credentials.createScoped(SCOPES); + assertNull(scopedCredentialCopy.getAccessToken()); + Map> metadataForCopiedCredentials = + scopedCredentialCopy.getRequestMetadata(CALL_URI); + TestUtils.assertContainsBearerToken(metadataForCopiedCredentials, ACCESS_TOKEN_WITH_SCOPES); + TestUtils.assertNotContainsBearerToken(metadataForCopiedCredentials, ACCESS_TOKEN); + } + + @Test + public void getRequestMetadata_missingServiceAccount_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND); + transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + try { + credentials.getRequestMetadata(CALL_URI); + fail("Expected error refreshing token."); + } catch (IOException expected) { + String message = expected.getMessage(); + assertTrue(message.contains(Integer.toString(HttpStatusCodes.STATUS_CODE_NOT_FOUND))); + // Message should mention scopes are missing on the VM. + assertTrue(message.contains("scope")); + } + } + + @Test + public void getRequestMetadata_serverError_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR); + transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + try { + credentials.getRequestMetadata(CALL_URI); + fail("Expected error refreshing token."); + } catch (IOException expected) { + String message = expected.getMessage(); + assertTrue(message.contains(Integer.toString(HttpStatusCodes.STATUS_CODE_SERVER_ERROR))); + assertTrue(message.contains("Unexpected")); + } + } + + @Test + public void equals_true() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + ComputeEngineCredentials explicitUniverseCredentials = + ComputeEngineCredentials.newBuilder() + .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) + .setHttpTransportFactory(transportFactory) + .build(); + ComputeEngineCredentials otherCredentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, otherCredentials.getUniverseDomain()); + assertFalse(explicitUniverseCredentials.equals(otherCredentials)); + assertFalse(otherCredentials.equals(explicitUniverseCredentials)); + ComputeEngineCredentials otherExplicitUniverseCredentials = + ComputeEngineCredentials.newBuilder() + .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) + .setHttpTransportFactory(transportFactory) + .build(); + assertTrue(explicitUniverseCredentials.equals(otherExplicitUniverseCredentials)); + assertTrue(otherExplicitUniverseCredentials.equals(explicitUniverseCredentials)); + } + + @Test + public void equals_false_transportFactory() throws IOException { + MockHttpTransportFactory httpTransportFactory = new MockHttpTransportFactory(); + MockMetadataServerTransportFactory serverTransportFactory = + new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(serverTransportFactory) + .build(); + ComputeEngineCredentials otherCredentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(httpTransportFactory).build(); + assertFalse(credentials.equals(otherCredentials)); + assertFalse(otherCredentials.equals(credentials)); + } + + @Test + public void toString_explicit_containsFields() throws IOException { + MockMetadataServerTransportFactory serverTransportFactory = + new MockMetadataServerTransportFactory(); + String expectedToString = + String.format( + "ComputeEngineCredentials{quotaProjectId=%s, universeDomain=%s, isExplicitUniverseDomain=%s, transportFactoryClassName=%s, scopes=%s}", + "some-project", + "some-domain", + true, + MockMetadataServerTransportFactory.class.getName(), + "[some scope]"); + GoogleCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(serverTransportFactory) + .setQuotaProjectId("some-project") + .setUniverseDomain("some-domain") + .build(); + credentials = credentials.createScoped("some scope"); + assertEquals(expectedToString, credentials.toString()); + } + + @Test + public void hashCode_equals() throws IOException { + MockMetadataServerTransportFactory serverTransportFactory = + new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(serverTransportFactory) + .build(); + ComputeEngineCredentials otherCredentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(serverTransportFactory) + .build(); + assertEquals(credentials.hashCode(), otherCredentials.hashCode()); + } + + @Test + public void toBuilder() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(new MockMetadataServerTransportFactory()) + .setQuotaProjectId("quota-project") + .build(); + + ComputeEngineCredentials secondCredentials = credentials.toBuilder().build(); + + assertEquals(credentials, secondCredentials); + } + + @Test + public void serialize() throws IOException, ClassNotFoundException { + MockMetadataServerTransportFactory serverTransportFactory = + new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(serverTransportFactory) + .build(); + GoogleCredentials deserializedCredentials = serializeAndDeserialize(credentials); + assertEquals(credentials, deserializedCredentials); + assertEquals(credentials.hashCode(), deserializedCredentials.hashCode()); + assertEquals(credentials.toString(), deserializedCredentials.toString()); + assertSame(deserializedCredentials.clock, Clock.SYSTEM); + credentials = ComputeEngineCredentials.newBuilder().build(); + deserializedCredentials = serializeAndDeserialize(credentials); + assertEquals(credentials, deserializedCredentials); + assertEquals(credentials.hashCode(), deserializedCredentials.hashCode()); + assertEquals(credentials.toString(), deserializedCredentials.toString()); + assertSame(deserializedCredentials.clock, Clock.SYSTEM); + } + + @Test + public void getAccount_sameAs() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + assertEquals(defaultAccountEmail, credentials.getAccount()); + + // metric headers are not supported for getAccount() + Map> headers = transportFactory.transport.getRequest().getHeaders(); + assertFalse(headers.containsKey(MetricsUtils.API_CLIENT_HEADER)); + } + + @Test + public void getAccount_missing_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (isGetServiceAccountsUrl(url)) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND) + .setContent(""); + } + }; + } + return super.buildRequest(method, url); + } + }; + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + try { + credentials.getAccount(); + fail("Fetching default service account should have failed"); + } catch (RuntimeException e) { + assertEquals("Failed to get service account", e.getMessage()); + assertNotNull(e.getCause()); + assertTrue(e.getCause().getMessage().contains("404")); + } + } + + @Test + public void getAccount_emptyContent_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (isGetServiceAccountsUrl(url)) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_OK); + } + }; + } + return super.buildRequest(method, url); + } + }; + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + try { + credentials.getAccount(); + fail("Fetching default service account should have failed"); + } catch (RuntimeException e) { + assertEquals("Failed to get service account", e.getMessage()); + assertNotNull(e.getCause()); + assertTrue(e.getCause().getMessage().contains("Empty content")); + } + } + + @Test + public void sign_sameAs() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; + + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + transportFactory.transport.setSignature(expectedSignature); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + assertArrayEquals(expectedSignature, credentials.sign(expectedSignature)); + } + + @Test + public void sign_getUniverseException() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + + String defaultAccountEmail = "mail@mail.com"; + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + transportFactory.transport.setStatusCode(501); + Assert.assertThrows(IOException.class, credentials::getUniverseDomain); + + byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; + SigningException signingException = + Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature)); + assertEquals("Failed to sign: Error obtaining universe domain", signingException.getMessage()); + } + + @Test + public void sign_getAccountFails() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; + + transportFactory.transport.setSignature(expectedSignature); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature)); + assertNotNull(exception.getMessage()); + assertNotNull(exception.getCause()); + } + + @Test + public void sign_accessDenied_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (isSignRequestUrl(url)) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_FORBIDDEN) + .setContent(TestUtils.errorJson("Sign Error")); + } + }; + } + return super.buildRequest(method, url); + } + }; + + transportFactory.transport.setAccessToken(ACCESS_TOKEN); + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + byte[] bytes = {0xD, 0xE, 0xA, 0xD}; + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); + assertEquals("Failed to sign the provided bytes", exception.getMessage()); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause().getMessage().contains("403")); + } + + @Test + public void sign_serverError_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (isSignRequestUrl(url)) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(TestUtils.errorJson("Sign Error")); + } + }; + } + return super.buildRequest(method, url); + } + }; + + transportFactory.transport.setAccessToken(ACCESS_TOKEN); + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + byte[] bytes = {0xD, 0xE, 0xA, 0xD}; + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); + assertEquals("Failed to sign the provided bytes", exception.getMessage()); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause().getMessage().contains("500")); + } + + @Test + public void refresh_503_retryable_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE) + .setContent(TestUtils.errorJson("Some error")); + } + }; + } + }; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + IOException exception = + Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken()); + assertTrue(exception.getCause().getMessage().contains("503")); + assertTrue(exception instanceof GoogleAuthException); + assertTrue(((GoogleAuthException) exception).isRetryable()); + } + + @Test + public void refresh_non503_ioexception_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + final Queue responseSequence = new ArrayDeque<>(); + IntStream.rangeClosed(400, 600).forEach(i -> responseSequence.add(i)); + + while (!responseSequence.isEmpty()) { + if (responseSequence.peek() == 503) { + responseSequence.poll(); + continue; } - return super.buildRequest(method, url); - } - }; - - transportFactory.transport.setAccessToken(ACCESS_TOKEN); - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - byte[] bytes = {0xD, 0xE, 0xA, 0xD}; - - SigningException exception = - Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); - assertEquals("Failed to sign the provided bytes", exception.getMessage()); - assertNotNull(exception.getCause()); - assertTrue(exception.getCause().getMessage().contains("500")); - } - - @Test - public void refresh_503_retryable_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE) - .setContent(TestUtils.errorJson("Some error")); - } - }; - } - }; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - IOException exception = - Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken()); - assertTrue(exception.getCause().getMessage().contains("503")); - assertTrue(exception instanceof GoogleAuthException); - assertTrue(((GoogleAuthException) exception).isRetryable()); - } - - @Test - public void refresh_non503_ioexception_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - final Queue responseSequence = new ArrayDeque<>(); - IntStream.rangeClosed(400, 600).forEach(i -> responseSequence.add(i)); - - while (!responseSequence.isEmpty()) { - if (responseSequence.peek() == 503) { - responseSequence.poll(); - continue; - } - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(responseSequence.poll()) - .setContent(TestUtils.errorJson("Some error")); - } - }; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(responseSequence.poll()) + .setContent(TestUtils.errorJson("Some error")); + } + }; + } + }; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + IOException exception = + Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken()); + assertFalse(exception instanceof GoogleAuthException); + } + } + + @Test + public void getUniverseDomain_fromMetadata() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_OK) + .setContent("some-universe.xyz"); + } + }; + } + }; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String universeDomain = credentials.getUniverseDomain(); + assertEquals("some-universe.xyz", universeDomain); + assertEquals(false, credentials.isExplicitUniverseDomain()); + } + + @Test + public void getUniverseDomain_fromMetadata_emptyBecomesDefault() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_OK) + .setContent(""); + } + }; + } + }; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String universeDomain = credentials.getUniverseDomain(); + assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); + assertEquals(false, credentials.isExplicitUniverseDomain()); + } + + @Test + public void getUniverseDomain_fromMetadata_404_default() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND) + .setContent("some content"); + } + }; + } + }; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String universeDomain = credentials.getUniverseDomain(); + assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); + assertEquals(false, credentials.isExplicitUniverseDomain()); + } + + @Test + public void getUniverseDomain_explicitSet_NoMdsCall() throws IOException { + MockRequestCountingTransportFactory transportFactory = + new MockRequestCountingTransportFactory(); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setUniverseDomain("explicit.universe") + .build(); + + String universeDomain = credentials.getUniverseDomain(); + assertEquals("explicit.universe", universeDomain); + assertEquals(true, credentials.isExplicitUniverseDomain()); + assertEquals(0, transportFactory.transport.getRequestCount()); + } + + @Test + public void getUniverseDomain_explicitGduSet_NoMdsCall() throws IOException { + MockRequestCountingTransportFactory transportFactory = + new MockRequestCountingTransportFactory(); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) + .build(); + + String universeDomain = credentials.getUniverseDomain(); + assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); + assertEquals(true, credentials.isExplicitUniverseDomain()); + assertEquals(0, transportFactory.transport.getRequestCount()); + } + + @Test + public void getUniverseDomain_fromMetadata_non404error_throws() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + MockMetadataServerTransport transport = transportFactory.transport; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + for (int status = 400; status < 600; status++) { + // 404 should not throw and tested separately + if (status == 404) { + continue; } - }; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - IOException exception = - Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken()); - assertFalse(exception instanceof GoogleAuthException); - } - } - - @Test - public void getUniverseDomain_fromMetadata() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_OK) - .setContent("some-universe.xyz"); - } - }; - } - }; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String universeDomain = credentials.getUniverseDomain(); - assertEquals("some-universe.xyz", universeDomain); - assertEquals(false, credentials.isExplicitUniverseDomain()); - } - - @Test - public void getUniverseDomain_fromMetadata_emptyBecomesDefault() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_OK) - .setContent(""); - } - }; - } - }; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String universeDomain = credentials.getUniverseDomain(); - assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); - assertEquals(false, credentials.isExplicitUniverseDomain()); - } - - @Test - public void getUniverseDomain_fromMetadata_404_default() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND) - .setContent("some content"); - } - }; - } - }; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String universeDomain = credentials.getUniverseDomain(); - assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); - assertEquals(false, credentials.isExplicitUniverseDomain()); - } - - @Test - public void getUniverseDomain_explicitSet_NoMdsCall() throws IOException { - MockRequestCountingTransportFactory transportFactory = - new MockRequestCountingTransportFactory(); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(transportFactory) - .setUniverseDomain("explicit.universe") - .build(); - - String universeDomain = credentials.getUniverseDomain(); - assertEquals("explicit.universe", universeDomain); - assertEquals(true, credentials.isExplicitUniverseDomain()); - assertEquals(0, transportFactory.transport.getRequestCount()); - } - - @Test - public void getUniverseDomain_explicitGduSet_NoMdsCall() throws IOException { - MockRequestCountingTransportFactory transportFactory = - new MockRequestCountingTransportFactory(); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(transportFactory) - .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) - .build(); - - String universeDomain = credentials.getUniverseDomain(); - assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); - assertEquals(true, credentials.isExplicitUniverseDomain()); - assertEquals(0, transportFactory.transport.getRequestCount()); - } - - @Test - public void getUniverseDomain_fromMetadata_non404error_throws() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - MockMetadataServerTransport transport = transportFactory.transport; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - for (int status = 400; status < 600; status++) { - // 404 should not throw and tested separately - if (status == 404) { - continue; - } - try { - transportFactory.transport.setStatusCode(status); - credentials.getUniverseDomain(); - fail("Should not be able to use credential without exception."); - } catch (GoogleAuthException ex) { - assertTrue(ex.isRetryable()); - } - } - } - - @Test - public void sign_emptyContent_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - if (isSignRequestUrl(url)) { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_OK); - } - }; + try { + transportFactory.transport.setStatusCode(status); + credentials.getUniverseDomain(); + fail("Should not be able to use credential without exception."); + } catch (GoogleAuthException ex) { + assertTrue(ex.isRetryable()); } - return super.buildRequest(method, url); - } - }; - - transportFactory.transport.setAccessToken(ACCESS_TOKEN); - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - byte[] bytes = {0xD, 0xE, 0xA, 0xD}; - - SigningException exception = - Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); - assertEquals("Failed to sign the provided bytes", exception.getMessage()); - assertNotNull(exception.getCause()); - assertTrue(exception.getCause().getMessage().contains("Empty content")); - } - - @Test - public void idTokenWithAudience_sameAs() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setIdToken(STANDARD_ID_TOKEN); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String targetAudience = "https://foo.bar"; - IdTokenCredentials tokenCredential = - IdTokenCredentials.newBuilder() - .setIdTokenProvider(credentials) - .setTargetAudience(targetAudience) - .build(); - tokenCredential.refresh(); - assertEquals(STANDARD_ID_TOKEN, tokenCredential.getAccessToken().getTokenValue()); - assertEquals(STANDARD_ID_TOKEN, tokenCredential.getIdToken().getTokenValue()); - assertEquals( - targetAudience, - (String) tokenCredential.getIdToken().getJsonWebSignature().getPayload().getAudience()); - } - - @Test - public void idTokenWithAudience_standard() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String targetAudience = "https://foo.bar"; - IdTokenCredentials tokenCredential = - IdTokenCredentials.newBuilder() - .setIdTokenProvider(credentials) - .setTargetAudience(targetAudience) - .build(); - tokenCredential.refresh(); - assertEquals(STANDARD_ID_TOKEN, tokenCredential.getAccessToken().getTokenValue()); - assertEquals(STANDARD_ID_TOKEN, tokenCredential.getIdToken().getTokenValue()); - assertNull(tokenCredential.getIdToken().getJsonWebSignature().getPayload().get("google")); - } - - @Test - @SuppressWarnings("unchecked") - public void idTokenWithAudience_full() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String targetAudience = "https://foo.bar"; - IdTokenCredentials tokenCredential = - IdTokenCredentials.newBuilder() - .setIdTokenProvider(credentials) - .setTargetAudience(targetAudience) - .setOptions(Arrays.asList(IdTokenProvider.Option.FORMAT_FULL)) - .build(); - tokenCredential.refresh(); - Payload p = tokenCredential.getIdToken().getJsonWebSignature().getPayload(); - assertTrue("Full ID Token format not provided", p.containsKey("google")); - ArrayMap> googleClaim = - (ArrayMap>) p.get("google"); - assertTrue(googleClaim.containsKey("compute_engine")); - - // verify metrics header - Map> requestHeaders = transportFactory.transport.getRequest().getHeaders(); - com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "it", "mds"); - } - - @Test - @SuppressWarnings("unchecked") - public void idTokenWithAudience_licenses() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String targetAudience = "https://foo.bar"; - IdTokenCredentials tokenCredential = - IdTokenCredentials.newBuilder() - .setIdTokenProvider(credentials) - .setTargetAudience(targetAudience) - .setOptions( - Arrays.asList( - IdTokenProvider.Option.FORMAT_FULL, IdTokenProvider.Option.LICENSES_TRUE)) - .build(); - tokenCredential.refresh(); - Payload p = tokenCredential.getIdToken().getJsonWebSignature().getPayload(); - assertTrue("Full ID Token format not provided", p.containsKey("google")); - ArrayMap> googleClaim = - (ArrayMap>) p.get("google"); - assertTrue(googleClaim.containsKey("compute_engine")); - ArrayMap computeEngineClaim = - (ArrayMap) googleClaim.get("compute_engine"); - assertTrue(computeEngineClaim.containsKey("license_id")); - } - - @Test - public void idTokenWithAudience_404StatusCode() { - int statusCode = HttpStatusCodes.STATUS_CODE_NOT_FOUND; - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - IOException exception = - assertThrows(IOException.class, () -> credentials.idTokenWithAudience("Audience", null)); - assertEquals( - String.format( - "Error code %s trying to get identity token from" - + " Compute Engine metadata. This may be because the virtual machine instance" - + " does not have permission scopes specified.", - statusCode), - exception.getMessage()); - } - - @Test - public void idTokenWithAudience_emptyContent() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setEmptyContent(true); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - IOException exception = - assertThrows(IOException.class, () -> credentials.idTokenWithAudience("Audience", null)); - assertEquals(METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE, exception.getMessage()); - } - - @Test - public void idTokenWithAudience_503StatusCode() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - assertThrows( - GoogleAuthException.class, () -> credentials.idTokenWithAudience("Audience", null)); - } - - static class MockMetadataServerTransportFactory implements HttpTransportFactory { - - MockMetadataServerTransport transport = - new MockMetadataServerTransport(SCOPE_TO_ACCESS_TOKEN_MAP); - - @Override - public HttpTransport create() { - return transport; - } - } -} + } + } + + @Test + public void sign_emptyContent_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (isSignRequestUrl(url)) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_OK); + } + }; + } + return super.buildRequest(method, url); + } + }; + + transportFactory.transport.setAccessToken(ACCESS_TOKEN); + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + byte[] bytes = {0xD, 0xE, 0xA, 0xD}; + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); + assertEquals("Failed to sign the provided bytes", exception.getMessage()); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause().getMessage().contains("Empty content")); + } + + @Test + public void idTokenWithAudience_sameAs() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setIdToken(STANDARD_ID_TOKEN); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String targetAudience = "https://foo.bar"; + IdTokenCredentials tokenCredential = + IdTokenCredentials.newBuilder() + .setIdTokenProvider(credentials) + .setTargetAudience(targetAudience) + .build(); + tokenCredential.refresh(); + assertEquals(STANDARD_ID_TOKEN, tokenCredential.getAccessToken().getTokenValue()); + assertEquals(STANDARD_ID_TOKEN, tokenCredential.getIdToken().getTokenValue()); + assertEquals( + targetAudience, + (String) tokenCredential.getIdToken().getJsonWebSignature().getPayload().getAudience()); + } + + @Test + public void idTokenWithAudience_standard() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String targetAudience = "https://foo.bar"; + IdTokenCredentials tokenCredential = + IdTokenCredentials.newBuilder() + .setIdTokenProvider(credentials) + .setTargetAudience(targetAudience) + .build(); + tokenCredential.refresh(); + assertEquals(STANDARD_ID_TOKEN, tokenCredential.getAccessToken().getTokenValue()); + assertEquals(STANDARD_ID_TOKEN, tokenCredential.getIdToken().getTokenValue()); + assertNull(tokenCredential.getIdToken().getJsonWebSignature().getPayload().get("google")); + } + + @Test + @SuppressWarnings("unchecked") + public void idTokenWithAudience_full() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String targetAudience = "https://foo.bar"; + IdTokenCredentials tokenCredential = + IdTokenCredentials.newBuilder() + .setIdTokenProvider(credentials) + .setTargetAudience(targetAudience) + .setOptions(Arrays.asList(IdTokenProvider.Option.FORMAT_FULL)) + .build(); + tokenCredential.refresh(); + Payload p = tokenCredential.getIdToken().getJsonWebSignature().getPayload(); + assertTrue("Full ID Token format not provided", p.containsKey("google")); + ArrayMap> googleClaim = + (ArrayMap>) p.get("google"); + assertTrue(googleClaim.containsKey("compute_engine")); + + // verify metrics header + Map> requestHeaders = transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "it", "mds"); + } + + @Test + @SuppressWarnings("unchecked") + public void idTokenWithAudience_licenses() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String targetAudience = "https://foo.bar"; + IdTokenCredentials tokenCredential = + IdTokenCredentials.newBuilder() + .setIdTokenProvider(credentials) + .setTargetAudience(targetAudience) + .setOptions( + Arrays.asList( + IdTokenProvider.Option.FORMAT_FULL, IdTokenProvider.Option.LICENSES_TRUE)) + .build(); + tokenCredential.refresh(); + Payload p = tokenCredential.getIdToken().getJsonWebSignature().getPayload(); + assertTrue("Full ID Token format not provided", p.containsKey("google")); + ArrayMap> googleClaim = + (ArrayMap>) p.get("google"); + assertTrue(googleClaim.containsKey("compute_engine")); + ArrayMap computeEngineClaim = + (ArrayMap) googleClaim.get("compute_engine"); + assertTrue(computeEngineClaim.containsKey("license_id")); + } + + @Test + public void idTokenWithAudience_404StatusCode() { + int statusCode = HttpStatusCodes.STATUS_CODE_NOT_FOUND; + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + IOException exception = + assertThrows(IOException.class, () -> credentials.idTokenWithAudience("Audience", null)); + assertEquals( + String.format( + "Error code %s trying to get identity token from" + + " Compute Engine metadata. This may be because the virtual machine instance" + + " does not have permission scopes specified.", + statusCode), + exception.getMessage()); + } + + @Test + public void idTokenWithAudience_emptyContent() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setEmptyContent(true); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + IOException exception = + assertThrows(IOException.class, () -> credentials.idTokenWithAudience("Audience", null)); + assertEquals(METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE, exception.getMessage()); + } + + @Test + public void idTokenWithAudience_503StatusCode() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + assertThrows( + GoogleAuthException.class, () -> credentials.idTokenWithAudience("Audience", null)); + } + + // --- Agent Identity Tests --- + + @Test + public void refreshAccessToken_noAgentConfig_requestsNormalToken() throws IOException { + // No Agent Identity config in environment + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null); + + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + AccessToken token = credentials.refreshAccessToken(); + assertNotNull(token); + assertEquals(ACCESS_TOKEN, token.getTokenValue()); + + // Verify NO fingerprint was sent + String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); + assertFalse(requestUrl.contains("bindCertificateFingerprint")); + } + + @Test + public void refreshAccessToken_withStandardCert_requestsNormalToken() throws IOException { + setupCertConfig("agent_cert.pem"); // Standard cert, no SPIFFE + + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + AccessToken token = credentials.refreshAccessToken(); + assertNotNull(token); + assertEquals(ACCESS_TOKEN, token.getTokenValue()); + + // Verify NO fingerprint was sent because cert didn't match SPIFFE pattern + String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); + assertFalse(requestUrl.contains("bindCertificateFingerprint")); + } + +// @Test +// public void refreshAccessToken_withAgentCert_requestsBoundToken() throws IOException { +// setupCertConfig("agent_spiffe_cert.pem"); // Valid Agent Identity cert +// +// MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); +// transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); +// transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); +// +// ComputeEngineCredentials credentials = +// ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); +// +// AccessToken token = credentials.refreshAccessToken(); +// assertNotNull(token); +// assertEquals(ACCESS_TOKEN, token.getTokenValue()); +// +// // Verify fingerprint WAS sent +// String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); +// assertTrue(requestUrl.contains("bindCertificateFingerprint")); +// } + + @Test + public void refreshAccessToken_withAgentCert_optedOut_requestsNormalToken() throws IOException { + setupCertConfig("agent_spiffe_cert.pem"); + envProvider.setEnv("GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); + + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + AccessToken token = credentials.refreshAccessToken(); + assertNotNull(token); + assertEquals(ACCESS_TOKEN, token.getTokenValue()); + + // Verify NO fingerprint due to opt-out + String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); + assertFalse(requestUrl.contains("bindCertificateFingerprint")); + } + + @Test + public void refreshAccessToken_agentConfigMissingFile_throws() throws IOException { + // Point config to a non-existent file and shorten timeout for test + envProvider.setEnv( + "GOOGLE_API_CERTIFICATE_CONFIG", + tempDir.resolve("missing_config.json").toAbsolutePath().toString()); + AgentIdentityUtils.TOTAL_TIMEOUT_MS = 100; + AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 10; + AgentIdentityUtils.FAST_POLL_DURATION_MS = 50; + AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 10; + + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + IOException e = assertThrows(IOException.class, credentials::refreshAccessToken); + assertTrue( + e.getMessage() + .contains( + "Certificate config or certificate file not found after multiple retries")); + } + + private void setupCertConfig(String certResourceName) throws IOException { + // Copy cert resource to temp file + Path certPath = tempDir.resolve("cert.pem"); + try (InputStream certStream = + getClass().getClassLoader().getResourceAsStream(certResourceName)) { + assertNotNull("Test resource " + certResourceName + " not found", certStream); + Files.copy(certStream, certPath); + } + + // Create config file pointing to cert + Path configPath = tempDir.resolve("config.json"); + String configContent = + "{\"cert_configs\": {\"workload\": {\"cert_path\": \"" + + certPath.toAbsolutePath().toString().replace("\\", "\\\\") + + "\"}}}"; + Files.write(configPath, configContent.getBytes(StandardCharsets.UTF_8)); + + // Set env var + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configPath.toAbsolutePath().toString()); + } + + static class MockMetadataServerTransportFactory implements HttpTransportFactory { + + MockMetadataServerTransport transport = + new MockMetadataServerTransport(SCOPE_TO_ACCESS_TOKEN_MAP); + + @Override + public HttpTransport create() { + return transport; + } + } + + // A helper class to mock System.getenv for testing purposes within this file. + private static class TestEnvironmentProvider { + private final Map env = new HashMap<>(); + + void setEnv(String key, String value) { + env.put(key, value); + } + + String getEnv(String key) { + return env.get(key); + } + } +} \ No newline at end of file diff --git a/oauth2_http/testresources/agent_cert.pem b/oauth2_http/testresources/agent_cert.pem new file mode 100644 index 000000000..4219c2971 --- /dev/null +++ b/oauth2_http/testresources/agent_cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV +BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV +MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM +7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer +uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp +gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4 ++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3 +ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O +gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh +GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr +odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk ++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9 +ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql +ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT +cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB +-----END CERTIFICATE----- \ No newline at end of file diff --git a/oauth2_http/testresources/agent_spiffe_cert.pem b/oauth2_http/testresources/agent_spiffe_cert.pem new file mode 100644 index 000000000..bf875dd1d --- /dev/null +++ b/oauth2_http/testresources/agent_spiffe_cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC8zCCAdugAwIBAgIULk565/k1yqg/+j11n/aB9Q4u1KUwDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLVGVzdCBTUElGRkUwHhcNMjUwMTAxMDAwMDAwWhcNMzUw +MTAxMDAwMDAwWjAWMRQwEgYDVQQDDAtUZXN0IFNUElGRkUwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC5/g+uS+vGgB9cK2Y5b4x7oJg+185c+35d468+ +9/0/A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/ +e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/ ++A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f +/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/ +B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/ZAgMBAAGjUDBOMEwG +A1UdEQRFMEOCOSNzcGlmZmU6Ly9hZ2VudHMuZ2xvYmFsLm9yZy0xMjM0NS5zeXN0 +ZW0uaWQuZ29vZy90ZXN0MB0GA1UdDgQWBBS/1/2/3/4/5/6/7/8/9/+A/B/C/DAN +BgkqhkiG9w0BAQsFAAOCAQEAu/8A/9/+/v7+///////+/v///v7///7+///////+ +/v///v7///7+///////+/v///v7///7+///////+/v///v7///7+///////+/v// +/v7///7+///////+/v///v7///7+///////+/v///v7///7+///////+/v///v7/ +//7+///////+/v///v7///7+///////+/v///v7///7+///////+/v///v7///7+ +///////+/v///v7///7+///////+/v///v7///7+///////+/v///v7///7+//// +///+/v///v7///7+///////+/v///v7///7+///////+/v///v7///7+//// +-----END CERTIFICATE----- \ No newline at end of file From c21c6632595aae887f6a572edc9f44c531bc05c9 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Wed, 5 Nov 2025 23:48:32 -0800 Subject: [PATCH 2/6] Format changes --- .../auth/oauth2/AgentIdentityUtils.java | 337 ++- .../auth/oauth2/ComputeEngineCredentials.java | 3 +- .../auth/oauth2/AgentIdentityUtilsTest.java | 411 +-- .../oauth2/ComputeEngineCredentialsTest.java | 2492 ++++++++--------- 4 files changed, 1621 insertions(+), 1622 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java b/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java index c490843d0..bec88e59d 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java +++ b/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java @@ -57,189 +57,186 @@ /** Internal utility class for handling Agent Identity certificate-bound access tokens. */ final class AgentIdentityUtils { - private static final Logger LOGGER = Logger.getLogger(AgentIdentityUtils.class.getName()); - - static final String GOOGLE_API_CERTIFICATE_CONFIG = "GOOGLE_API_CERTIFICATE_CONFIG"; - static final String GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES = - "GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES"; - - private static final List AGENT_IDENTITY_SPIFFE_PATTERNS = - ImmutableList.of( - Pattern.compile("^agents\\.global\\.org-\\d+\\.system\\.id\\.goog$"), - Pattern.compile("^agents\\.global\\.proj-\\d+\\.system\\.id\\.goog$")); - - // Polling configuration - static long TOTAL_TIMEOUT_MS = 30000; // 30 seconds - static long FAST_POLL_DURATION_MS = 5000; // 5 seconds - static long FAST_POLL_INTERVAL_MS = 100; // 0.1 seconds - static long SLOW_POLL_INTERVAL_MS = 500; // 0.5 seconds - - private static final int SAN_URI_TYPE = 6; - private static final String SPIFFE_SCHEME_PREFIX = "spiffe://"; - - // Interface to allow mocking System.getenv for tests without exposing it publicly. - interface EnvReader { - String getEnv(String name); - } - - private static EnvReader envReader = System::getenv; - - private AgentIdentityUtils() {} - - /** - * Gets the Agent Identity certificate if available and enabled. - * - * @return The X509Certificate if found and Agent Identities are enabled, null otherwise. - * @throws IOException If there is an error reading the certificate file after retries. - */ - static X509Certificate getAgentIdentityCertificate() throws IOException { - if (isOptedOut()) { - return null; - } - - String certConfigPath = envReader.getEnv(GOOGLE_API_CERTIFICATE_CONFIG); - if (Strings.isNullOrEmpty(certConfigPath)) { - return null; - } - - String certPath = getCertificatePathWithRetry(certConfigPath); - return parseCertificate(certPath); + private static final Logger LOGGER = Logger.getLogger(AgentIdentityUtils.class.getName()); + + static final String GOOGLE_API_CERTIFICATE_CONFIG = "GOOGLE_API_CERTIFICATE_CONFIG"; + static final String GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES = + "GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES"; + + private static final List AGENT_IDENTITY_SPIFFE_PATTERNS = + ImmutableList.of( + Pattern.compile("^agents\\.global\\.org-\\d+\\.system\\.id\\.goog$"), + Pattern.compile("^agents\\.global\\.proj-\\d+\\.system\\.id\\.goog$")); + + // Polling configuration + static long TOTAL_TIMEOUT_MS = 30000; // 30 seconds + static long FAST_POLL_DURATION_MS = 5000; // 5 seconds + static long FAST_POLL_INTERVAL_MS = 100; // 0.1 seconds + static long SLOW_POLL_INTERVAL_MS = 500; // 0.5 seconds + + private static final int SAN_URI_TYPE = 6; + private static final String SPIFFE_SCHEME_PREFIX = "spiffe://"; + + // Interface to allow mocking System.getenv for tests without exposing it publicly. + interface EnvReader { + String getEnv(String name); + } + + private static EnvReader envReader = System::getenv; + + private AgentIdentityUtils() {} + + /** + * Gets the Agent Identity certificate if available and enabled. + * + * @return The X509Certificate if found and Agent Identities are enabled, null otherwise. + * @throws IOException If there is an error reading the certificate file after retries. + */ + static X509Certificate getAgentIdentityCertificate() throws IOException { + if (isOptedOut()) { + return null; } - /** Checks if the user has opted out of Agent Token sharing. */ - private static boolean isOptedOut() { - String optOut = envReader.getEnv(GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES); - return optOut != null && "false".equalsIgnoreCase(optOut); + String certConfigPath = envReader.getEnv(GOOGLE_API_CERTIFICATE_CONFIG); + if (Strings.isNullOrEmpty(certConfigPath)) { + return null; } - /** Polls for the certificate config file and the certificate file it references. */ - private static String getCertificatePathWithRetry(String certConfigPath) throws IOException { - long startTime = System.currentTimeMillis(); - boolean warned = false; - - while (true) { - try { - if (Files.exists(Paths.get(certConfigPath))) { - String certPath = extractCertPathFromConfig(certConfigPath); - if (!Strings.isNullOrEmpty(certPath) && Files.exists(Paths.get(certPath))) { - return certPath; - } - } - } catch (Exception e) { - // Ignore exceptions during polling and retry - LOGGER.log(Level.FINE, "Error while polling for certificate files", e); - } - - long elapsedTime = System.currentTimeMillis() - startTime; - if (elapsedTime >= TOTAL_TIMEOUT_MS) { - throw new IOException( - "Certificate config or certificate file not found after multiple retries. " - + "Token binding protection is failing. You can turn off this protection by setting " - + GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES - + " to false to fall back to unbound tokens."); - } - - if (!warned) { - LOGGER.warning( - String.format( - "Certificate config file not found at %s (from %s environment variable). " - + "Retrying for up to %d seconds.", - certConfigPath, - GOOGLE_API_CERTIFICATE_CONFIG, - TOTAL_TIMEOUT_MS / 1000)); - warned = true; - } - - try { - long sleepTime = - elapsedTime < FAST_POLL_DURATION_MS ? FAST_POLL_INTERVAL_MS : SLOW_POLL_INTERVAL_MS; - Thread.sleep(sleepTime); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Interrupted while waiting for certificate files", e); - } + String certPath = getCertificatePathWithRetry(certConfigPath); + return parseCertificate(certPath); + } + + /** Checks if the user has opted out of Agent Token sharing. */ + private static boolean isOptedOut() { + String optOut = envReader.getEnv(GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES); + return optOut != null && "false".equalsIgnoreCase(optOut); + } + + /** Polls for the certificate config file and the certificate file it references. */ + private static String getCertificatePathWithRetry(String certConfigPath) throws IOException { + long startTime = System.currentTimeMillis(); + boolean warned = false; + + while (true) { + try { + if (Files.exists(Paths.get(certConfigPath))) { + String certPath = extractCertPathFromConfig(certConfigPath); + if (!Strings.isNullOrEmpty(certPath) && Files.exists(Paths.get(certPath))) { + return certPath; + } } + } catch (Exception e) { + // Ignore exceptions during polling and retry + LOGGER.log(Level.FINE, "Error while polling for certificate files", e); + } + + long elapsedTime = System.currentTimeMillis() - startTime; + if (elapsedTime >= TOTAL_TIMEOUT_MS) { + throw new IOException( + "Certificate config or certificate file not found after multiple retries. " + + "Token binding protection is failing. You can turn off this protection by setting " + + GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES + + " to false to fall back to unbound tokens."); + } + + if (!warned) { + LOGGER.warning( + String.format( + "Certificate config file not found at %s (from %s environment variable). " + + "Retrying for up to %d seconds.", + certConfigPath, GOOGLE_API_CERTIFICATE_CONFIG, TOTAL_TIMEOUT_MS / 1000)); + warned = true; + } + + try { + long sleepTime = + elapsedTime < FAST_POLL_DURATION_MS ? FAST_POLL_INTERVAL_MS : SLOW_POLL_INTERVAL_MS; + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for certificate files", e); + } } - - @SuppressWarnings("unchecked") - private static String extractCertPathFromConfig(String certConfigPath) throws IOException { - try (InputStream stream = new FileInputStream(certConfigPath)) { - JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); - GenericJson config = - parser.parseAndClose(stream, StandardCharsets.UTF_8, GenericJson.class); - Map certConfigs = (Map) config.get("cert_configs"); - if (certConfigs != null) { - Map workload = (Map) certConfigs.get("workload"); - if (workload != null) { - return (String) workload.get("cert_path"); - } - } + } + + @SuppressWarnings("unchecked") + private static String extractCertPathFromConfig(String certConfigPath) throws IOException { + try (InputStream stream = new FileInputStream(certConfigPath)) { + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); + GenericJson config = parser.parseAndClose(stream, StandardCharsets.UTF_8, GenericJson.class); + Map certConfigs = (Map) config.get("cert_configs"); + if (certConfigs != null) { + Map workload = (Map) certConfigs.get("workload"); + if (workload != null) { + return (String) workload.get("cert_path"); } - return null; + } } - - private static X509Certificate parseCertificate(String certPath) throws IOException { - try (InputStream stream = new FileInputStream(certPath)) { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(stream); - } catch (GeneralSecurityException e) { - throw new IOException("Failed to parse certificate", e); - } + return null; + } + + private static X509Certificate parseCertificate(String certPath) throws IOException { + try (InputStream stream = new FileInputStream(certPath)) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(stream); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to parse certificate", e); } + } - /** Checks if the certificate belongs to an Agent Identity by inspecting SANs. */ - static boolean shouldRequestBoundToken(X509Certificate cert) { - try { - Collection> sans = cert.getSubjectAlternativeNames(); - if (sans == null) { - return false; - } - for (List san : sans) { - // SAN entry is a list where first element is the type (Integer) and second is value (mostly - // String) - if (san.size() >= 2 - && san.get(0) instanceof Integer - && (Integer) san.get(0) == SAN_URI_TYPE) { - Object value = san.get(1); - if (value instanceof String) { - String uri = (String) value; - if (uri.startsWith(SPIFFE_SCHEME_PREFIX)) { - // Extract trust domain: spiffe:///... - String withoutScheme = uri.substring(SPIFFE_SCHEME_PREFIX.length()); - int slashIndex = withoutScheme.indexOf('/'); - String trustDomain = - (slashIndex == -1) ? withoutScheme : withoutScheme.substring(0, slashIndex); - - for (Pattern pattern : AGENT_IDENTITY_SPIFFE_PATTERNS) { - if (pattern.matcher(trustDomain).matches()) { - return true; - } - } - } - } + /** Checks if the certificate belongs to an Agent Identity by inspecting SANs. */ + static boolean shouldRequestBoundToken(X509Certificate cert) { + try { + Collection> sans = cert.getSubjectAlternativeNames(); + if (sans == null) { + return false; + } + for (List san : sans) { + // SAN entry is a list where first element is the type (Integer) and second is value (mostly + // String) + if (san.size() >= 2 + && san.get(0) instanceof Integer + && (Integer) san.get(0) == SAN_URI_TYPE) { + Object value = san.get(1); + if (value instanceof String) { + String uri = (String) value; + if (uri.startsWith(SPIFFE_SCHEME_PREFIX)) { + // Extract trust domain: spiffe:///... + String withoutScheme = uri.substring(SPIFFE_SCHEME_PREFIX.length()); + int slashIndex = withoutScheme.indexOf('/'); + String trustDomain = + (slashIndex == -1) ? withoutScheme : withoutScheme.substring(0, slashIndex); + + for (Pattern pattern : AGENT_IDENTITY_SPIFFE_PATTERNS) { + if (pattern.matcher(trustDomain).matches()) { + return true; } + } } - } catch (CertificateParsingException e) { - LOGGER.log(Level.WARNING, "Failed to parse Subject Alternative Names from certificate", e); + } } - return false; + } + } catch (CertificateParsingException e) { + LOGGER.log(Level.WARNING, "Failed to parse Subject Alternative Names from certificate", e); } - - /** Calculates the SHA-256 fingerprint of the certificate, Base64Url encoded without padding. */ - static String calculateCertificateFingerprint(X509Certificate cert) throws IOException { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] der = cert.getEncoded(); - md.update(der); - byte[] digest = md.digest(); - return BaseEncoding.base64Url().omitPadding().encode(digest); - } catch (GeneralSecurityException e) { - throw new IOException("Failed to calculate certificate fingerprint", e); - } + return false; + } + + /** Calculates the SHA-256 fingerprint of the certificate, Base64Url encoded without padding. */ + static String calculateCertificateFingerprint(X509Certificate cert) throws IOException { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] der = cert.getEncoded(); + md.update(der); + byte[] digest = md.digest(); + return BaseEncoding.base64Url().omitPadding().encode(digest); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to calculate certificate fingerprint", e); } + } - @VisibleForTesting - static void setEnvReader(EnvReader reader) { - envReader = reader; - } -} \ No newline at end of file + @VisibleForTesting + static void setEnvReader(EnvReader reader) { + envReader = reader; + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index 300ef4939..f7dd7ddd6 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -348,8 +348,7 @@ public AccessToken refreshAccessToken() throws IOException { String tokenUrl = createTokenUrlWithScopes(); try { - X509Certificate cert = - AgentIdentityUtils.getAgentIdentityCertificate(); + X509Certificate cert = AgentIdentityUtils.getAgentIdentityCertificate(); if (cert != null && AgentIdentityUtils.shouldRequestBoundToken(cert)) { String fingerprint = AgentIdentityUtils.calculateCertificateFingerprint(cert); GenericUrl url = new GenericUrl(tokenUrl); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java index 475d9afe2..859daab5a 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java @@ -12,7 +12,6 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -32,212 +31,216 @@ @RunWith(JUnit4.class) public class AgentIdentityUtilsTest { - private static final String VALID_SPIFFE_ORG = - "spiffe://agents.global.org-12345.system.id.goog/path/to/resource"; - private static final String VALID_SPIFFE_PROJ = - "spiffe://agents.global.proj-98765.system.id.goog/another/path"; - private static final String INVALID_SPIFFE_DOMAIN = "spiffe://example.com/workload"; - private static final String INVALID_SPIFFE_FORMAT = - "spiffe://agents.global.org-INVALID.system.id.goog/path"; - - // A minimal, valid self-signed X.509 certificate (PEM format) for testing loading. - // Generated for testing purposes. - private static final String TEST_CERT_PEM = - "-----BEGIN CERTIFICATE-----\n" - + "MIIDWTCCAkGgAwIBAgIUX5/1aT1uuxgj1+F7Q/r+5Q9y4JQwDQYJKoZIhvcNAQEL\n" - + "BQAwHTEbMBkGA1UEAwwSdGVzdC5leGFtcGxlLmNvbTAeFw0yNDAxMDEwMDAwMDBa\n" - + "Fw0zNDAxMDEwMDAwMDBaMB0xGzAZBgNVBAMMEnRlc3QuZXhhbXBsZS5jb20wggEi\n" - + "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDV/8Q/5+8+X9Y+5+6+7+8+9+0+\n" - + "A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/\n" - + "g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B\n" - + "/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h\n" - + "/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/\n" - + "D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/\n" - + "j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E\n" - + "AgMBAAGjUzBRMB0GA1UdDgQWBBS/1/2/3/4/5/6/7/8/9/+A/B/C/DAfBgNVHSME\n" - + "GDAWgBS/1/2/3/4/5/6/7/8/9/+A/B/C/DAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\n" - + "SIb3DQEBCwUAA4IBAQDV/8Q/5+8+X9Y+5+6+7+8+9+0+A/B/C/D/E/F/G/H/I/J/\n" - + "K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/\n" - + "q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E/F/G/H/I/J/K/L\n" - + "/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r\n" - + "/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E/F/G/H/I/J/K/L/M/\n" - + "N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/\n" - + "t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E\n" - + "-----END CERTIFICATE-----"; - - private TestEnvironmentProvider envProvider; - private Path tempDir; - - @Before - public void setUp() throws IOException { - envProvider = new TestEnvironmentProvider(); - // Inject our test environment reader - AgentIdentityUtils.setEnvReader(envProvider::getEnv); - tempDir = Files.createTempDirectory("agent_identity_test"); + private static final String VALID_SPIFFE_ORG = + "spiffe://agents.global.org-12345.system.id.goog/path/to/resource"; + private static final String VALID_SPIFFE_PROJ = + "spiffe://agents.global.proj-98765.system.id.goog/another/path"; + private static final String INVALID_SPIFFE_DOMAIN = "spiffe://example.com/workload"; + private static final String INVALID_SPIFFE_FORMAT = + "spiffe://agents.global.org-INVALID.system.id.goog/path"; + + // A minimal, valid self-signed X.509 certificate (PEM format) for testing loading. + // Generated for testing purposes. + private static final String TEST_CERT_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIDWTCCAkGgAwIBAgIUX5/1aT1uuxgj1+F7Q/r+5Q9y4JQwDQYJKoZIhvcNAQEL\n" + + "BQAwHTEbMBkGA1UEAwwSdGVzdC5leGFtcGxlLmNvbTAeFw0yNDAxMDEwMDAwMDBa\n" + + "Fw0zNDAxMDEwMDAwMDBaMB0xGzAZBgNVBAMMEnRlc3QuZXhhbXBsZS5jb20wggEi\n" + + "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDV/8Q/5+8+X9Y+5+6+7+8+9+0+\n" + + "A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/\n" + + "g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B\n" + + "/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h\n" + + "/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/\n" + + "D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/\n" + + "j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E\n" + + "AgMBAAGjUzBRMB0GA1UdDgQWBBS/1/2/3/4/5/6/7/8/9/+A/B/C/DAfBgNVHSME\n" + + "GDAWgBS/1/2/3/4/5/6/7/8/9/+A/B/C/DAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\n" + + "SIb3DQEBCwUAA4IBAQDV/8Q/5+8+X9Y+5+6+7+8+9+0+A/B/C/D/E/F/G/H/I/J/\n" + + "K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/\n" + + "q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E/F/G/H/I/J/K/L\n" + + "/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r\n" + + "/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E/F/G/H/I/J/K/L/M/\n" + + "N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/\n" + + "t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E\n" + + "-----END CERTIFICATE-----"; + + private TestEnvironmentProvider envProvider; + private Path tempDir; + + @Before + public void setUp() throws IOException { + envProvider = new TestEnvironmentProvider(); + // Inject our test environment reader + AgentIdentityUtils.setEnvReader(envProvider::getEnv); + tempDir = Files.createTempDirectory("agent_identity_test"); + } + + @After + public void tearDown() throws IOException { + // Reset polling constants to defaults after each test to avoid side effects + AgentIdentityUtils.TOTAL_TIMEOUT_MS = 30000; + AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 100; + AgentIdentityUtils.FAST_POLL_DURATION_MS = 5000; + AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 500; + + // Clean up temp files + if (tempDir != null) { + Files.walk(tempDir) + .sorted(java.util.Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); } - - @After - public void tearDown() throws IOException { - // Reset polling constants to defaults after each test to avoid side effects - AgentIdentityUtils.TOTAL_TIMEOUT_MS = 30000; - AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 100; - AgentIdentityUtils.FAST_POLL_DURATION_MS = 5000; - AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 500; - - // Clean up temp files - if (tempDir != null) { - Files.walk(tempDir) - .sorted(java.util.Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); - } - } - - // --- 1. SPIFFE ID Validation Tests --- - - @Test - public void shouldRequestBoundToken_validOrgSpiffe_returnsTrue() throws CertificateException { - assertTrue(AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(VALID_SPIFFE_ORG))); - } - - @Test - public void shouldRequestBoundToken_validProjSpiffe_returnsTrue() throws CertificateException { - assertTrue(AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(VALID_SPIFFE_PROJ))); - } - - @Test - public void shouldRequestBoundToken_invalidDomain_returnsFalse() throws CertificateException { - assertFalse( - AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(INVALID_SPIFFE_DOMAIN))); + } + + // --- 1. SPIFFE ID Validation Tests --- + + @Test + public void shouldRequestBoundToken_validOrgSpiffe_returnsTrue() throws CertificateException { + assertTrue(AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(VALID_SPIFFE_ORG))); + } + + @Test + public void shouldRequestBoundToken_validProjSpiffe_returnsTrue() throws CertificateException { + assertTrue(AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(VALID_SPIFFE_PROJ))); + } + + @Test + public void shouldRequestBoundToken_invalidDomain_returnsFalse() throws CertificateException { + assertFalse( + AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(INVALID_SPIFFE_DOMAIN))); + } + + @Test + public void shouldRequestBoundToken_invalidFormat_returnsFalse() throws CertificateException { + assertFalse( + AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(INVALID_SPIFFE_FORMAT))); + } + + @Test + public void shouldRequestBoundToken_noSan_returnsFalse() throws CertificateException { + X509Certificate mockCert = mock(X509Certificate.class); + when(mockCert.getSubjectAlternativeNames()).thenReturn(null); + assertFalse(AgentIdentityUtils.shouldRequestBoundToken(mockCert)); + } + + // Helper to create a mock cert with a specific URI in SAN + private X509Certificate mockCertWithSanUri(String uri) throws CertificateException { + X509Certificate mockCert = mock(X509Certificate.class); + // SAN entry type 6 is URI + List spiffeEntry = Arrays.asList(6, uri); + Collection> sans = Collections.singletonList(spiffeEntry); + when(mockCert.getSubjectAlternativeNames()).thenReturn(sans); + return mockCert; + } + + // --- 2. Fingerprint Calculation Tests --- + + @Test + public void calculateCertificateFingerprint_knownInput_returnsExpectedOutput() throws Exception { + // We mock the getEncoded() method to return a fixed byte array to guarantee a stable hash + // regardless of the actual certificate implementation details. + X509Certificate mockCert = mock(X509Certificate.class); + byte[] fakeDer = new byte[] {0x01, 0x02, 0x03, 0x04, (byte) 0xFF}; + when(mockCert.getEncoded()).thenReturn(fakeDer); + + // SHA-256 of {0x01, 0x02, 0x03, 0x04, 0xFF} is: + // fc402e5e4d71483c6d537984a30c2b4c8b065539a4bd1b026c6112926ba52793 + // Base64Url (no padding) of that hash is: + // _EAuXk1xSDxtU3mEowwrTIsGVTmkvRsCbGESkmulJ5M + String expectedFingerprint = "_EAuXk1xSDxtU3mEowwrTIsGVTmkvRsCbGESkmulJ5M"; + + String actualFingerprint = AgentIdentityUtils.calculateCertificateFingerprint(mockCert); + assertEquals(expectedFingerprint, actualFingerprint); + } + + // --- 3. Environmental Control Tests --- + + @Test + public void getAgentIdentityCertificate_optedOut_returnsNullImmediately() throws IOException { + envProvider.setEnv("GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); + // Set config to a non-existent path; if it tried to load it, it would fail or retry. + // Returning null immediately proves it respected the opt-out. + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", "/non/existent/path"); + + assertNull(AgentIdentityUtils.getAgentIdentityCertificate()); + } + + @Test + public void getAgentIdentityCertificate_noConfigEnvVar_returnsNull() throws IOException { + // Default opt-in is true, but no config env var is set. + assertNull(AgentIdentityUtils.getAgentIdentityCertificate()); + } + + // --- 4. Certificate Loading & Polling Tests --- + + @Test + public void getAgentIdentityCertificate_happyPath_loadsCertificate() throws IOException { + // Setup: Get the absolute path of the test resource. + URL certUrl = getClass().getClassLoader().getResource("agent_cert.pem"); + assertNotNull("Test resource agent_cert.pem not found", certUrl); + String certPath = new File(certUrl.getFile()).getAbsolutePath(); + + // Create config file pointing to the cert. + // We still need a temp file for the config because it must contain the absolute path + // to the certificate, which varies by machine. + File configFile = tempDir.resolve("config.json").toFile(); + String configJson = + "{" + + " \"cert_configs\": {" + + " \"workload\": {" + + " \"cert_path\": \"" + + certPath.replace("\\", "\\\\") + + "\"" + + " }" + + " }" + + "}"; + try (FileOutputStream fos = new FileOutputStream(configFile)) { + fos.write(configJson.getBytes(StandardCharsets.UTF_8)); } - @Test - public void shouldRequestBoundToken_invalidFormat_returnsFalse() throws CertificateException { - assertFalse( - AgentIdentityUtils.shouldRequestBoundToken(mockCertWithSanUri(INVALID_SPIFFE_FORMAT))); + // Configure environment + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configFile.getAbsolutePath()); + + // Execute + X509Certificate cert = AgentIdentityUtils.getAgentIdentityCertificate(); + + // Verify + assertNotNull(cert); + // Basic verification that it loaded OUR cert (checking issuer from agent_cert.pem) + assertTrue(cert.getIssuerDN().getName().contains("unit-tests")); + } + + @Test + public void getAgentIdentityCertificate_timeout_throwsIOException() { + // Setup: Set config path to something that doesn't exist + envProvider.setEnv( + "GOOGLE_API_CERTIFICATE_CONFIG", + tempDir.resolve("missing.json").toAbsolutePath().toString()); + + // Reduce timeout to make test fast (e.g., 100ms total) + AgentIdentityUtils.TOTAL_TIMEOUT_MS = 100; + AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 10; + AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 10; + AgentIdentityUtils.FAST_POLL_DURATION_MS = 50; + + // Execute & Verify + IOException e = + assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertificate); + assertTrue( + e.getMessage() + .contains("Certificate config or certificate file not found after multiple retries")); + } + + // A helper class to mock System.getenv for testing purposes within this file. + private static class TestEnvironmentProvider { + private final java.util.Map env = new java.util.HashMap<>(); + + void setEnv(String key, String value) { + env.put(key, value); } - @Test - public void shouldRequestBoundToken_noSan_returnsFalse() throws CertificateException { - X509Certificate mockCert = mock(X509Certificate.class); - when(mockCert.getSubjectAlternativeNames()).thenReturn(null); - assertFalse(AgentIdentityUtils.shouldRequestBoundToken(mockCert)); - } - - // Helper to create a mock cert with a specific URI in SAN - private X509Certificate mockCertWithSanUri(String uri) throws CertificateException { - X509Certificate mockCert = mock(X509Certificate.class); - // SAN entry type 6 is URI - List spiffeEntry = Arrays.asList(6, uri); - Collection> sans = Collections.singletonList(spiffeEntry); - when(mockCert.getSubjectAlternativeNames()).thenReturn(sans); - return mockCert; - } - - // --- 2. Fingerprint Calculation Tests --- - - @Test - public void calculateCertificateFingerprint_knownInput_returnsExpectedOutput() throws Exception { - // We mock the getEncoded() method to return a fixed byte array to guarantee a stable hash - // regardless of the actual certificate implementation details. - X509Certificate mockCert = mock(X509Certificate.class); - byte[] fakeDer = new byte[] {0x01, 0x02, 0x03, 0x04, (byte) 0xFF}; - when(mockCert.getEncoded()).thenReturn(fakeDer); - - // SHA-256 of {0x01, 0x02, 0x03, 0x04, 0xFF} is: - // fc402e5e4d71483c6d537984a30c2b4c8b065539a4bd1b026c6112926ba52793 - // Base64Url (no padding) of that hash is: - // _EAuXk1xSDxtU3mEowwrTIsGVTmkvRsCbGESkmulJ5M - String expectedFingerprint = "_EAuXk1xSDxtU3mEowwrTIsGVTmkvRsCbGESkmulJ5M"; - - String actualFingerprint = AgentIdentityUtils.calculateCertificateFingerprint(mockCert); - assertEquals(expectedFingerprint, actualFingerprint); - } - - // --- 3. Environmental Control Tests --- - - @Test - public void getAgentIdentityCertificate_optedOut_returnsNullImmediately() throws IOException { - envProvider.setEnv("GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); - // Set config to a non-existent path; if it tried to load it, it would fail or retry. - // Returning null immediately proves it respected the opt-out. - envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", "/non/existent/path"); - - assertNull(AgentIdentityUtils.getAgentIdentityCertificate()); - } - - @Test - public void getAgentIdentityCertificate_noConfigEnvVar_returnsNull() throws IOException { - // Default opt-in is true, but no config env var is set. - assertNull(AgentIdentityUtils.getAgentIdentityCertificate()); - } - - // --- 4. Certificate Loading & Polling Tests --- - - @Test - public void getAgentIdentityCertificate_happyPath_loadsCertificate() throws IOException { - // Setup: Get the absolute path of the test resource. - URL certUrl = getClass().getClassLoader().getResource("agent_cert.pem"); - assertNotNull("Test resource agent_cert.pem not found", certUrl); - String certPath = new File(certUrl.getFile()).getAbsolutePath(); - - // Create config file pointing to the cert. - // We still need a temp file for the config because it must contain the absolute path - // to the certificate, which varies by machine. - File configFile = tempDir.resolve("config.json").toFile(); - String configJson = - "{" - + " \"cert_configs\": {" - + " \"workload\": {" - + " \"cert_path\": \"" - + certPath.replace("\\", "\\\\") - + "\"" - + " }" - + " }" - + "}"; - try (FileOutputStream fos = new FileOutputStream(configFile)) { - fos.write(configJson.getBytes(StandardCharsets.UTF_8)); - } - - // Configure environment - envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configFile.getAbsolutePath()); - - // Execute - X509Certificate cert = AgentIdentityUtils.getAgentIdentityCertificate(); - - // Verify - assertNotNull(cert); - // Basic verification that it loaded OUR cert (checking issuer from agent_cert.pem) - assertTrue(cert.getIssuerDN().getName().contains("unit-tests")); - } - - @Test - public void getAgentIdentityCertificate_timeout_throwsIOException() { - // Setup: Set config path to something that doesn't exist - envProvider.setEnv( - "GOOGLE_API_CERTIFICATE_CONFIG", tempDir.resolve("missing.json").toAbsolutePath().toString()); - - // Reduce timeout to make test fast (e.g., 100ms total) - AgentIdentityUtils.TOTAL_TIMEOUT_MS = 100; - AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 10; - AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 10; - AgentIdentityUtils.FAST_POLL_DURATION_MS = 50; - - // Execute & Verify - IOException e = assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertificate); - assertTrue(e.getMessage().contains("Certificate config or certificate file not found after multiple retries")); - } - - // A helper class to mock System.getenv for testing purposes within this file. - private static class TestEnvironmentProvider { - private final java.util.Map env = new java.util.HashMap<>(); - - void setEnv(String key, String value) { - env.put(key, value); - } - - String getEnv(String key) { - return env.get(key); - } + String getEnv(String key) { + return env.get(key); } -} \ No newline at end of file + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index 4f5ddc0e9..f1981b88c 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -58,7 +58,6 @@ import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.DefaultCredentialsProviderTest.MockRequestCountingTransportFactory; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -87,1251 +86,1252 @@ @RunWith(JUnit4.class) public class ComputeEngineCredentialsTest extends BaseSerializationTest { - private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo"); - - private static final String TOKEN_URL = - "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"; - - // Id Token which includes basic default claims - public static final String STANDARD_ID_TOKEN = - "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyO" - + "TNhZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Zvby5iYXIiL" - + "CJhenAiOiIxMDIxMDE1NTA4MzQyMDA3MDg1NjgiLCJleHAiOjE1NjQ0NzUwNTEsImlhdCI6MTU2NDQ3MTQ1MSwi" - + "aXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwNzA4NTY4In0" - + ".redacted"; - - // Id Token which includes GCE extended claims - public static final String FULL_ID_TOKEN = - "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyOTNh" - + "ZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Zvby5iYXIiLCJhe" - + "nAiOiIxMTIxNzkwNjI3MjAzOTEzMDU4ODUiLCJlbWFpbCI6IjEwNzEyODQxODQ0MzYtY29tcHV0ZUBkZXZlbG9wZ" - + "XIuZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NjQ1MTk0OTYsImdvb" - + "2dsZSI6eyJjb21wdXRlX2VuZ2luZSI6eyJpbnN0YW5jZV9jcmVhdGlvbl90aW1lc3RhbXAiOjE1NjMyMzA5MDcsI" - + "mluc3RhbmNlX2lkIjoiMzQ5Nzk3NDM5MzQ0MTE3OTI0MyIsImluc3RhbmNlX25hbWUiOiJpYW0iLCJwcm9qZWN0X" - + "2lkIjoibWluZXJhbC1taW51dGlhLTgyMCIsInByb2plY3RfbnVtYmVyIjoxMDcxMjg0MTg0NDM2LCJ6b25lIjoid" - + "XMtY2VudHJhbDEtYSJ9fSwiaWF0IjoxNTY0NTE1ODk2LCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb" - + "20iLCJzdWIiOiIxMTIxNzkwNjI3MjAzOTEzMDU4ODUifQ.redacted"; - - // Id Token which includes GCE extended claims and any VM License data (if applicable) - public static final String FULL_ID_TOKEN_WITH_LICENSES = - "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOG" - + "I3OTIyOTNhZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.ew0KICAiYXVkIjogImh0dHBzOi8" - + "vZm9vLmJhciIsDQogICJhenAiOiAiMTEyMTc5MDYyNzIwMzkxMzA1ODg1IiwNCiAgImVtYWlsIjogIjEyMzQ1Ni1" - + "jb21wdXRlQGRldmVsb3Blci5nc2VydmljZWFjY291bnQuY29tIiwNCiAgImVtYWlsX3ZlcmlmaWVkIjogdHJ1ZSw" - + "NCiAgImV4cCI6IDE1NjQ1MTk0OTYsDQogICJnb29nbGUiOiB7DQogICAgImNvbXB1dGVfZW5naW5lIjogew0KICA" - + "gICAgImluc3RhbmNlX2NyZWF0aW9uX3RpbWVzdGFtcCI6IDE1NjMyMzA5MDcsDQogICAgICAiaW5zdGFuY2VfaWQ" - + "iOiAiMzQ5Nzk3NDM5MzQ0MTE3OTI0MyIsDQogICAgICAiaW5zdGFuY2VfbmFtZSI6ICJpYW0iLA0KICAgICAgImx" - + "pY2Vuc2VfaWQiOiBbDQogICAgICAgICIxMDAxMDAwIiwNCiAgICAgICAgIjEwMDEwMDEiLA0KICAgICAgICAiMTA" - + "wMTAwOCINCiAgICAgIF0sDQogICAgICAicHJvamVjdF9pZCI6ICJmb28tYmFyLTgyMCIsDQogICAgICAicHJvamV" - + "jdF9udW1iZXIiOiAxMDcxMjg0MTg0NDM2LA0KICAgICAgInpvbmUiOiAidXMtY2VudHJhbDEtYSINCiAgICB9DQo" - + "gIH0sDQogICJpYXQiOiAxNTY0NTE1ODk2LA0KICAiaXNzIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSI" - + "sDQogICJzdWIiOiAiMTEyMTc5MDYyNzIwMzkxMzA1ODg1Ig0KfQ.redacted"; - private static final String ACCESS_TOKEN = "1/MkSJoj1xsli0AccessToken_NKPY2"; - private static final List SCOPES = Arrays.asList("foo", "bar"); - private static final String ACCESS_TOKEN_WITH_SCOPES = "1/MkSJoj1xsli0AccessTokenScoped_NKPY2"; - private static final Map SCOPE_TO_ACCESS_TOKEN_MAP = - Stream.of( - new String[][] { - {"default", ACCESS_TOKEN}, - {SCOPES.toString().replaceAll("\\s", ""), ACCESS_TOKEN_WITH_SCOPES}, - }) - .collect(Collectors.toMap(data -> data[0], data -> data[1])); - - private TestEnvironmentProvider envProvider; - private Path tempDir; - - @Before - public void setUp() throws IOException { - envProvider = new TestEnvironmentProvider(); - // Inject our test environment reader into AgentIdentityUtils - AgentIdentityUtils.setEnvReader(envProvider::getEnv); - tempDir = Files.createTempDirectory("compute_engine_creds_test"); - } - - @After - public void tearDown() throws IOException { - // Reset polling constants to defaults after each test - AgentIdentityUtils.TOTAL_TIMEOUT_MS = 30000; - AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 100; - AgentIdentityUtils.FAST_POLL_DURATION_MS = 5000; - AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 500; - - if (tempDir != null) { - Files.walk(tempDir) - .sorted(java.util.Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); - } - } - - @Test - public void buildTokenUrlWithScopes_null_scopes() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setScopes(null).build(); - Collection scopes = credentials.getScopes(); - String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL, tokenUrlWithScopes); - assertTrue(scopes.isEmpty()); - } - - @Test - public void buildTokenUrlWithScopes_empty_scopes() { - ComputeEngineCredentials.Builder builder = - ComputeEngineCredentials.newBuilder().setScopes(Collections.emptyList()); - ComputeEngineCredentials credentials = builder.build(); - Collection scopes = credentials.getScopes(); - String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL, tokenUrlWithScopes); - assertTrue(scopes.isEmpty()); - assertTrue(builder.getScopes().isEmpty()); - } - - @Test - public void buildTokenUrlWithScopes_single_scope() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setScopes(Arrays.asList("foo")).build(); - String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); - Collection scopes = credentials.getScopes(); - - assertEquals(TOKEN_URL + "?scopes=foo", tokenUrlWithScopes); - assertEquals(1, scopes.size()); - assertEquals("foo", scopes.toArray()[0]); - } - - @Test - public void buildTokenUrlWithScopes_multiple_scopes() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setScopes(Arrays.asList(null, "foo", "", "bar")) - .build(); - Collection scopes = credentials.getScopes(); - String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?scopes=foo,bar", tokenUrlWithScopes); - assertEquals(2, scopes.size()); - assertEquals("foo", scopes.toArray()[0]); - assertEquals("bar", scopes.toArray()[1]); - } - - @Test - public void buildTokenUrlWithScopes_defaultScopes() { - ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().build(); - credentials = - (ComputeEngineCredentials) - credentials.createScoped(null, Arrays.asList(null, "foo", "", "bar")); - Collection scopes = credentials.getScopes(); - String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?scopes=foo,bar", tokenUrlWithScopes); - assertEquals(2, scopes.size()); - assertEquals("foo", scopes.toArray()[0]); - assertEquals("bar", scopes.toArray()[1]); - } - - @Test - public void buildTokenUrl_nullTransport() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(null) - .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) - .build(); - String tokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?binding-enforcement=on", tokenUrl); - } - - @Test - public void buildTokenUrl_nullBindingEnforcement() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) - .setBindingEnforcement(null) - .build(); - String tokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?transport=mtls", tokenUrl); - } - - @Test - public void buildTokenUrl_nullTransport_nullBindingEnforcement() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(null) - .setBindingEnforcement(null) - .build(); - String softBoundTokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL, softBoundTokenUrl); - } - - @Test - public void buildTokenUrl_mtls_transport() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) - .build(); - String tokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?transport=mtls", tokenUrl); - } - - @Test - public void buildTokenUrl_iam_enforcement() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.IAM_POLICY) - .build(); - String tokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?binding-enforcement=iam-policy", tokenUrl); - } - - @Test - public void buildTokenUrlSoftMtlsBound_mtls_transport_iam_enforcement() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) - .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.IAM_POLICY) - .build(); - String softBoundTokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?transport=mtls&binding-enforcement=iam-policy", softBoundTokenUrl); - } - - @Test - public void buildTokenUrl_always_enforced() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) - .build(); - String tokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?binding-enforcement=on", tokenUrl); - } - - @Test - public void buildTokenUrlHardMtlsBound_mtls_transport_always_enforced() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) - .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) - .build(); - String hardBoundTokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?transport=mtls&binding-enforcement=on", hardBoundTokenUrl); - } - - @Test - public void buildTokenUrlHardDirectPathBound_alts_transport() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.ALTS) - .build(); - String hardBoundTokenUrl = credentials.createTokenUrlWithScopes(); - - assertEquals(TOKEN_URL + "?transport=alts", hardBoundTokenUrl); - } - - @Test - public void buildScoped_scopesPresent() throws IOException { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setScopes(null).build(); - ComputeEngineCredentials scopedCredentials = - (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); - Collection scopes = scopedCredentials.getScopes(); - - assertEquals(1, scopes.size()); - assertEquals("foo", scopes.toArray()[0]); - } - - @Test - public void buildScoped_correctMargins() throws IOException { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setScopes(null).build(); - ComputeEngineCredentials scopedCredentials = - (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); - - assertEquals( - ComputeEngineCredentials.COMPUTE_EXPIRATION_MARGIN, - scopedCredentials.getExpirationMargin()); - assertEquals( - ComputeEngineCredentials.COMPUTE_REFRESH_MARGIN, scopedCredentials.getRefreshMargin()); - } - - @Test - public void buildScoped_explicitUniverse() throws IOException { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setScopes(null) - .setUniverseDomain("some-universe") - .build(); - ComputeEngineCredentials scopedCredentials = - (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); - - assertEquals("some-universe", scopedCredentials.getUniverseDomain()); - assertTrue(scopedCredentials.isExplicitUniverseDomain()); - } - - @Test - public void createScoped_defaultScopes() { - GoogleCredentials credentials = - ComputeEngineCredentials.create().createScoped(null, Arrays.asList("foo")); - Collection scopes = ((ComputeEngineCredentials) credentials).getScopes(); - - assertEquals(1, scopes.size()); - assertEquals("foo", scopes.toArray()[0]); - } - - @Test - public void buildScoped_quotaProjectId() throws IOException { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setScopes(null) - .setQuotaProjectId("some-project-id") - .build(); - ComputeEngineCredentials scopedCredentials = - (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); - - assertEquals("some-project-id", scopedCredentials.getQuotaProjectId()); - } - - @Test - public void buildDefaultScoped_explicitUniverse() throws IOException { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setScopes(null) - .setUniverseDomain("some-universe") - .build(); - ComputeEngineCredentials scopedCredentials = - (ComputeEngineCredentials) credentials.createScoped(null, Arrays.asList("foo")); - - assertEquals("some-universe", scopedCredentials.getUniverseDomain()); - assertTrue(scopedCredentials.isExplicitUniverseDomain()); - } - - @Test - public void create_scoped_correctMargins() { - GoogleCredentials credentials = - ComputeEngineCredentials.create().createScoped(null, Arrays.asList("foo")); - - assertEquals( - ComputeEngineCredentials.COMPUTE_EXPIRATION_MARGIN, credentials.getExpirationMargin()); - assertEquals(ComputeEngineCredentials.COMPUTE_REFRESH_MARGIN, credentials.getRefreshMargin()); - } - - @Test - public void getRequestMetadata_hasAccessToken() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - Map> metadata = credentials.getRequestMetadata(CALL_URI); - - TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); - // verify metrics header added and other header intact - Map> requestHeaders = transportFactory.transport.getRequest().getHeaders(); - com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "at", "mds"); - assertTrue(requestHeaders.containsKey("metadata-flavor")); - assertTrue(requestHeaders.get("metadata-flavor").contains("Google")); - } - - @Test - public void getRequestMetadata_shouldInvalidateAccessTokenWhenScoped_newAccessTokenFromRefresh() - throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - Map> metadata = credentials.getRequestMetadata(CALL_URI); - - TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); - - assertNotNull(credentials.getAccessToken()); - ComputeEngineCredentials scopedCredentialCopy = - (ComputeEngineCredentials) credentials.createScoped(SCOPES); - assertNull(scopedCredentialCopy.getAccessToken()); - Map> metadataForCopiedCredentials = - scopedCredentialCopy.getRequestMetadata(CALL_URI); - TestUtils.assertContainsBearerToken(metadataForCopiedCredentials, ACCESS_TOKEN_WITH_SCOPES); - TestUtils.assertNotContainsBearerToken(metadataForCopiedCredentials, ACCESS_TOKEN); - } - - @Test - public void getRequestMetadata_missingServiceAccount_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND); - transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - try { - credentials.getRequestMetadata(CALL_URI); - fail("Expected error refreshing token."); - } catch (IOException expected) { - String message = expected.getMessage(); - assertTrue(message.contains(Integer.toString(HttpStatusCodes.STATUS_CODE_NOT_FOUND))); - // Message should mention scopes are missing on the VM. - assertTrue(message.contains("scope")); - } - } - - @Test - public void getRequestMetadata_serverError_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR); - transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - try { - credentials.getRequestMetadata(CALL_URI); - fail("Expected error refreshing token."); - } catch (IOException expected) { - String message = expected.getMessage(); - assertTrue(message.contains(Integer.toString(HttpStatusCodes.STATUS_CODE_SERVER_ERROR))); - assertTrue(message.contains("Unexpected")); - } - } - - @Test - public void equals_true() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - ComputeEngineCredentials explicitUniverseCredentials = - ComputeEngineCredentials.newBuilder() - .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) - .setHttpTransportFactory(transportFactory) - .build(); - ComputeEngineCredentials otherCredentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, otherCredentials.getUniverseDomain()); - assertFalse(explicitUniverseCredentials.equals(otherCredentials)); - assertFalse(otherCredentials.equals(explicitUniverseCredentials)); - ComputeEngineCredentials otherExplicitUniverseCredentials = - ComputeEngineCredentials.newBuilder() - .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) - .setHttpTransportFactory(transportFactory) - .build(); - assertTrue(explicitUniverseCredentials.equals(otherExplicitUniverseCredentials)); - assertTrue(otherExplicitUniverseCredentials.equals(explicitUniverseCredentials)); - } - - @Test - public void equals_false_transportFactory() throws IOException { - MockHttpTransportFactory httpTransportFactory = new MockHttpTransportFactory(); - MockMetadataServerTransportFactory serverTransportFactory = - new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(serverTransportFactory) - .build(); - ComputeEngineCredentials otherCredentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(httpTransportFactory).build(); - assertFalse(credentials.equals(otherCredentials)); - assertFalse(otherCredentials.equals(credentials)); - } - - @Test - public void toString_explicit_containsFields() throws IOException { - MockMetadataServerTransportFactory serverTransportFactory = - new MockMetadataServerTransportFactory(); - String expectedToString = - String.format( - "ComputeEngineCredentials{quotaProjectId=%s, universeDomain=%s, isExplicitUniverseDomain=%s, transportFactoryClassName=%s, scopes=%s}", - "some-project", - "some-domain", - true, - MockMetadataServerTransportFactory.class.getName(), - "[some scope]"); - GoogleCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(serverTransportFactory) - .setQuotaProjectId("some-project") - .setUniverseDomain("some-domain") - .build(); - credentials = credentials.createScoped("some scope"); - assertEquals(expectedToString, credentials.toString()); - } - - @Test - public void hashCode_equals() throws IOException { - MockMetadataServerTransportFactory serverTransportFactory = - new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(serverTransportFactory) - .build(); - ComputeEngineCredentials otherCredentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(serverTransportFactory) - .build(); - assertEquals(credentials.hashCode(), otherCredentials.hashCode()); - } - - @Test - public void toBuilder() { - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(new MockMetadataServerTransportFactory()) - .setQuotaProjectId("quota-project") - .build(); - - ComputeEngineCredentials secondCredentials = credentials.toBuilder().build(); - - assertEquals(credentials, secondCredentials); - } - - @Test - public void serialize() throws IOException, ClassNotFoundException { - MockMetadataServerTransportFactory serverTransportFactory = - new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(serverTransportFactory) - .build(); - GoogleCredentials deserializedCredentials = serializeAndDeserialize(credentials); - assertEquals(credentials, deserializedCredentials); - assertEquals(credentials.hashCode(), deserializedCredentials.hashCode()); - assertEquals(credentials.toString(), deserializedCredentials.toString()); - assertSame(deserializedCredentials.clock, Clock.SYSTEM); - credentials = ComputeEngineCredentials.newBuilder().build(); - deserializedCredentials = serializeAndDeserialize(credentials); - assertEquals(credentials, deserializedCredentials); - assertEquals(credentials.hashCode(), deserializedCredentials.hashCode()); - assertEquals(credentials.toString(), deserializedCredentials.toString()); - assertSame(deserializedCredentials.clock, Clock.SYSTEM); - } - - @Test - public void getAccount_sameAs() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - assertEquals(defaultAccountEmail, credentials.getAccount()); - - // metric headers are not supported for getAccount() - Map> headers = transportFactory.transport.getRequest().getHeaders(); - assertFalse(headers.containsKey(MetricsUtils.API_CLIENT_HEADER)); - } - - @Test - public void getAccount_missing_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - if (isGetServiceAccountsUrl(url)) { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND) - .setContent(""); - } - }; - } - return super.buildRequest(method, url); - } - }; - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - try { - credentials.getAccount(); - fail("Fetching default service account should have failed"); - } catch (RuntimeException e) { - assertEquals("Failed to get service account", e.getMessage()); - assertNotNull(e.getCause()); - assertTrue(e.getCause().getMessage().contains("404")); - } - } - - @Test - public void getAccount_emptyContent_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - if (isGetServiceAccountsUrl(url)) { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_OK); - } - }; - } - return super.buildRequest(method, url); - } - }; - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - try { - credentials.getAccount(); - fail("Fetching default service account should have failed"); - } catch (RuntimeException e) { - assertEquals("Failed to get service account", e.getMessage()); - assertNotNull(e.getCause()); - assertTrue(e.getCause().getMessage().contains("Empty content")); - } - } - - @Test - public void sign_sameAs() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; - - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - transportFactory.transport.setSignature(expectedSignature); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - assertArrayEquals(expectedSignature, credentials.sign(expectedSignature)); - } - - @Test - public void sign_getUniverseException() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - - String defaultAccountEmail = "mail@mail.com"; - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - transportFactory.transport.setStatusCode(501); - Assert.assertThrows(IOException.class, credentials::getUniverseDomain); - - byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; - SigningException signingException = - Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature)); - assertEquals("Failed to sign: Error obtaining universe domain", signingException.getMessage()); - } - - @Test - public void sign_getAccountFails() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; - - transportFactory.transport.setSignature(expectedSignature); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - SigningException exception = - Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature)); - assertNotNull(exception.getMessage()); - assertNotNull(exception.getCause()); - } - - @Test - public void sign_accessDenied_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - if (isSignRequestUrl(url)) { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_FORBIDDEN) - .setContent(TestUtils.errorJson("Sign Error")); - } - }; - } - return super.buildRequest(method, url); - } - }; - - transportFactory.transport.setAccessToken(ACCESS_TOKEN); - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - byte[] bytes = {0xD, 0xE, 0xA, 0xD}; - - SigningException exception = - Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); - assertEquals("Failed to sign the provided bytes", exception.getMessage()); - assertNotNull(exception.getCause()); - assertTrue(exception.getCause().getMessage().contains("403")); - } - - @Test - public void sign_serverError_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - if (isSignRequestUrl(url)) { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) - .setContent(TestUtils.errorJson("Sign Error")); - } - }; - } - return super.buildRequest(method, url); - } - }; - - transportFactory.transport.setAccessToken(ACCESS_TOKEN); - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - byte[] bytes = {0xD, 0xE, 0xA, 0xD}; - - SigningException exception = - Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); - assertEquals("Failed to sign the provided bytes", exception.getMessage()); - assertNotNull(exception.getCause()); - assertTrue(exception.getCause().getMessage().contains("500")); - } - - @Test - public void refresh_503_retryable_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE) - .setContent(TestUtils.errorJson("Some error")); - } - }; - } - }; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - IOException exception = - Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken()); - assertTrue(exception.getCause().getMessage().contains("503")); - assertTrue(exception instanceof GoogleAuthException); - assertTrue(((GoogleAuthException) exception).isRetryable()); - } - - @Test - public void refresh_non503_ioexception_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - final Queue responseSequence = new ArrayDeque<>(); - IntStream.rangeClosed(400, 600).forEach(i -> responseSequence.add(i)); - - while (!responseSequence.isEmpty()) { - if (responseSequence.peek() == 503) { - responseSequence.poll(); - continue; + private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo"); + + private static final String TOKEN_URL = + "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"; + + // Id Token which includes basic default claims + public static final String STANDARD_ID_TOKEN = + "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyO" + + "TNhZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Zvby5iYXIiL" + + "CJhenAiOiIxMDIxMDE1NTA4MzQyMDA3MDg1NjgiLCJleHAiOjE1NjQ0NzUwNTEsImlhdCI6MTU2NDQ3MTQ1MSwi" + + "aXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwNzA4NTY4In0" + + ".redacted"; + + // Id Token which includes GCE extended claims + public static final String FULL_ID_TOKEN = + "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyOTNh" + + "ZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Zvby5iYXIiLCJhe" + + "nAiOiIxMTIxNzkwNjI3MjAzOTEzMDU4ODUiLCJlbWFpbCI6IjEwNzEyODQxODQ0MzYtY29tcHV0ZUBkZXZlbG9wZ" + + "XIuZ3NlcnZpY2VhY2NvdW50LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NjQ1MTk0OTYsImdvb" + + "2dsZSI6eyJjb21wdXRlX2VuZ2luZSI6eyJpbnN0YW5jZV9jcmVhdGlvbl90aW1lc3RhbXAiOjE1NjMyMzA5MDcsI" + + "mluc3RhbmNlX2lkIjoiMzQ5Nzk3NDM5MzQ0MTE3OTI0MyIsImluc3RhbmNlX25hbWUiOiJpYW0iLCJwcm9qZWN0X" + + "2lkIjoibWluZXJhbC1taW51dGlhLTgyMCIsInByb2plY3RfbnVtYmVyIjoxMDcxMjg0MTg0NDM2LCJ6b25lIjoid" + + "XMtY2VudHJhbDEtYSJ9fSwiaWF0IjoxNTY0NTE1ODk2LCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb" + + "20iLCJzdWIiOiIxMTIxNzkwNjI3MjAzOTEzMDU4ODUifQ.redacted"; + + // Id Token which includes GCE extended claims and any VM License data (if applicable) + public static final String FULL_ID_TOKEN_WITH_LICENSES = + "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOG" + + "I3OTIyOTNhZDk3N2EwYjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.ew0KICAiYXVkIjogImh0dHBzOi8" + + "vZm9vLmJhciIsDQogICJhenAiOiAiMTEyMTc5MDYyNzIwMzkxMzA1ODg1IiwNCiAgImVtYWlsIjogIjEyMzQ1Ni1" + + "jb21wdXRlQGRldmVsb3Blci5nc2VydmljZWFjY291bnQuY29tIiwNCiAgImVtYWlsX3ZlcmlmaWVkIjogdHJ1ZSw" + + "NCiAgImV4cCI6IDE1NjQ1MTk0OTYsDQogICJnb29nbGUiOiB7DQogICAgImNvbXB1dGVfZW5naW5lIjogew0KICA" + + "gICAgImluc3RhbmNlX2NyZWF0aW9uX3RpbWVzdGFtcCI6IDE1NjMyMzA5MDcsDQogICAgICAiaW5zdGFuY2VfaWQ" + + "iOiAiMzQ5Nzk3NDM5MzQ0MTE3OTI0MyIsDQogICAgICAiaW5zdGFuY2VfbmFtZSI6ICJpYW0iLA0KICAgICAgImx" + + "pY2Vuc2VfaWQiOiBbDQogICAgICAgICIxMDAxMDAwIiwNCiAgICAgICAgIjEwMDEwMDEiLA0KICAgICAgICAiMTA" + + "wMTAwOCINCiAgICAgIF0sDQogICAgICAicHJvamVjdF9pZCI6ICJmb28tYmFyLTgyMCIsDQogICAgICAicHJvamV" + + "jdF9udW1iZXIiOiAxMDcxMjg0MTg0NDM2LA0KICAgICAgInpvbmUiOiAidXMtY2VudHJhbDEtYSINCiAgICB9DQo" + + "gIH0sDQogICJpYXQiOiAxNTY0NTE1ODk2LA0KICAiaXNzIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSI" + + "sDQogICJzdWIiOiAiMTEyMTc5MDYyNzIwMzkxMzA1ODg1Ig0KfQ.redacted"; + private static final String ACCESS_TOKEN = "1/MkSJoj1xsli0AccessToken_NKPY2"; + private static final List SCOPES = Arrays.asList("foo", "bar"); + private static final String ACCESS_TOKEN_WITH_SCOPES = "1/MkSJoj1xsli0AccessTokenScoped_NKPY2"; + private static final Map SCOPE_TO_ACCESS_TOKEN_MAP = + Stream.of( + new String[][] { + {"default", ACCESS_TOKEN}, + {SCOPES.toString().replaceAll("\\s", ""), ACCESS_TOKEN_WITH_SCOPES}, + }) + .collect(Collectors.toMap(data -> data[0], data -> data[1])); + + private TestEnvironmentProvider envProvider; + private Path tempDir; + + @Before + public void setUp() throws IOException { + envProvider = new TestEnvironmentProvider(); + // Inject our test environment reader into AgentIdentityUtils + AgentIdentityUtils.setEnvReader(envProvider::getEnv); + tempDir = Files.createTempDirectory("compute_engine_creds_test"); + } + + @After + public void tearDown() throws IOException { + // Reset polling constants to defaults after each test + AgentIdentityUtils.TOTAL_TIMEOUT_MS = 30000; + AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 100; + AgentIdentityUtils.FAST_POLL_DURATION_MS = 5000; + AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 500; + + if (tempDir != null) { + Files.walk(tempDir) + .sorted(java.util.Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } + + @Test + public void buildTokenUrlWithScopes_null_scopes() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setScopes(null).build(); + Collection scopes = credentials.getScopes(); + String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL, tokenUrlWithScopes); + assertTrue(scopes.isEmpty()); + } + + @Test + public void buildTokenUrlWithScopes_empty_scopes() { + ComputeEngineCredentials.Builder builder = + ComputeEngineCredentials.newBuilder().setScopes(Collections.emptyList()); + ComputeEngineCredentials credentials = builder.build(); + Collection scopes = credentials.getScopes(); + String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL, tokenUrlWithScopes); + assertTrue(scopes.isEmpty()); + assertTrue(builder.getScopes().isEmpty()); + } + + @Test + public void buildTokenUrlWithScopes_single_scope() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setScopes(Arrays.asList("foo")).build(); + String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); + Collection scopes = credentials.getScopes(); + + assertEquals(TOKEN_URL + "?scopes=foo", tokenUrlWithScopes); + assertEquals(1, scopes.size()); + assertEquals("foo", scopes.toArray()[0]); + } + + @Test + public void buildTokenUrlWithScopes_multiple_scopes() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setScopes(Arrays.asList(null, "foo", "", "bar")) + .build(); + Collection scopes = credentials.getScopes(); + String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?scopes=foo,bar", tokenUrlWithScopes); + assertEquals(2, scopes.size()); + assertEquals("foo", scopes.toArray()[0]); + assertEquals("bar", scopes.toArray()[1]); + } + + @Test + public void buildTokenUrlWithScopes_defaultScopes() { + ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().build(); + credentials = + (ComputeEngineCredentials) + credentials.createScoped(null, Arrays.asList(null, "foo", "", "bar")); + Collection scopes = credentials.getScopes(); + String tokenUrlWithScopes = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?scopes=foo,bar", tokenUrlWithScopes); + assertEquals(2, scopes.size()); + assertEquals("foo", scopes.toArray()[0]); + assertEquals("bar", scopes.toArray()[1]); + } + + @Test + public void buildTokenUrl_nullTransport() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(null) + .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) + .build(); + String tokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?binding-enforcement=on", tokenUrl); + } + + @Test + public void buildTokenUrl_nullBindingEnforcement() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) + .setBindingEnforcement(null) + .build(); + String tokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?transport=mtls", tokenUrl); + } + + @Test + public void buildTokenUrl_nullTransport_nullBindingEnforcement() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(null) + .setBindingEnforcement(null) + .build(); + String softBoundTokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL, softBoundTokenUrl); + } + + @Test + public void buildTokenUrl_mtls_transport() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) + .build(); + String tokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?transport=mtls", tokenUrl); + } + + @Test + public void buildTokenUrl_iam_enforcement() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.IAM_POLICY) + .build(); + String tokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?binding-enforcement=iam-policy", tokenUrl); + } + + @Test + public void buildTokenUrlSoftMtlsBound_mtls_transport_iam_enforcement() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) + .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.IAM_POLICY) + .build(); + String softBoundTokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?transport=mtls&binding-enforcement=iam-policy", softBoundTokenUrl); + } + + @Test + public void buildTokenUrl_always_enforced() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) + .build(); + String tokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?binding-enforcement=on", tokenUrl); + } + + @Test + public void buildTokenUrlHardMtlsBound_mtls_transport_always_enforced() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.MTLS) + .setBindingEnforcement(ComputeEngineCredentials.BindingEnforcement.ON) + .build(); + String hardBoundTokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?transport=mtls&binding-enforcement=on", hardBoundTokenUrl); + } + + @Test + public void buildTokenUrlHardDirectPathBound_alts_transport() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setGoogleAuthTransport(ComputeEngineCredentials.GoogleAuthTransport.ALTS) + .build(); + String hardBoundTokenUrl = credentials.createTokenUrlWithScopes(); + + assertEquals(TOKEN_URL + "?transport=alts", hardBoundTokenUrl); + } + + @Test + public void buildScoped_scopesPresent() throws IOException { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setScopes(null).build(); + ComputeEngineCredentials scopedCredentials = + (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); + Collection scopes = scopedCredentials.getScopes(); + + assertEquals(1, scopes.size()); + assertEquals("foo", scopes.toArray()[0]); + } + + @Test + public void buildScoped_correctMargins() throws IOException { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setScopes(null).build(); + ComputeEngineCredentials scopedCredentials = + (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); + + assertEquals( + ComputeEngineCredentials.COMPUTE_EXPIRATION_MARGIN, + scopedCredentials.getExpirationMargin()); + assertEquals( + ComputeEngineCredentials.COMPUTE_REFRESH_MARGIN, scopedCredentials.getRefreshMargin()); + } + + @Test + public void buildScoped_explicitUniverse() throws IOException { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setScopes(null) + .setUniverseDomain("some-universe") + .build(); + ComputeEngineCredentials scopedCredentials = + (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); + + assertEquals("some-universe", scopedCredentials.getUniverseDomain()); + assertTrue(scopedCredentials.isExplicitUniverseDomain()); + } + + @Test + public void createScoped_defaultScopes() { + GoogleCredentials credentials = + ComputeEngineCredentials.create().createScoped(null, Arrays.asList("foo")); + Collection scopes = ((ComputeEngineCredentials) credentials).getScopes(); + + assertEquals(1, scopes.size()); + assertEquals("foo", scopes.toArray()[0]); + } + + @Test + public void buildScoped_quotaProjectId() throws IOException { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setScopes(null) + .setQuotaProjectId("some-project-id") + .build(); + ComputeEngineCredentials scopedCredentials = + (ComputeEngineCredentials) credentials.createScoped(Arrays.asList("foo")); + + assertEquals("some-project-id", scopedCredentials.getQuotaProjectId()); + } + + @Test + public void buildDefaultScoped_explicitUniverse() throws IOException { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setScopes(null) + .setUniverseDomain("some-universe") + .build(); + ComputeEngineCredentials scopedCredentials = + (ComputeEngineCredentials) credentials.createScoped(null, Arrays.asList("foo")); + + assertEquals("some-universe", scopedCredentials.getUniverseDomain()); + assertTrue(scopedCredentials.isExplicitUniverseDomain()); + } + + @Test + public void create_scoped_correctMargins() { + GoogleCredentials credentials = + ComputeEngineCredentials.create().createScoped(null, Arrays.asList("foo")); + + assertEquals( + ComputeEngineCredentials.COMPUTE_EXPIRATION_MARGIN, credentials.getExpirationMargin()); + assertEquals(ComputeEngineCredentials.COMPUTE_REFRESH_MARGIN, credentials.getRefreshMargin()); + } + + @Test + public void getRequestMetadata_hasAccessToken() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + Map> metadata = credentials.getRequestMetadata(CALL_URI); + + TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); + // verify metrics header added and other header intact + Map> requestHeaders = transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "at", "mds"); + assertTrue(requestHeaders.containsKey("metadata-flavor")); + assertTrue(requestHeaders.get("metadata-flavor").contains("Google")); + } + + @Test + public void getRequestMetadata_shouldInvalidateAccessTokenWhenScoped_newAccessTokenFromRefresh() + throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + Map> metadata = credentials.getRequestMetadata(CALL_URI); + + TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); + + assertNotNull(credentials.getAccessToken()); + ComputeEngineCredentials scopedCredentialCopy = + (ComputeEngineCredentials) credentials.createScoped(SCOPES); + assertNull(scopedCredentialCopy.getAccessToken()); + Map> metadataForCopiedCredentials = + scopedCredentialCopy.getRequestMetadata(CALL_URI); + TestUtils.assertContainsBearerToken(metadataForCopiedCredentials, ACCESS_TOKEN_WITH_SCOPES); + TestUtils.assertNotContainsBearerToken(metadataForCopiedCredentials, ACCESS_TOKEN); + } + + @Test + public void getRequestMetadata_missingServiceAccount_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND); + transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + try { + credentials.getRequestMetadata(CALL_URI); + fail("Expected error refreshing token."); + } catch (IOException expected) { + String message = expected.getMessage(); + assertTrue(message.contains(Integer.toString(HttpStatusCodes.STATUS_CODE_NOT_FOUND))); + // Message should mention scopes are missing on the VM. + assertTrue(message.contains("scope")); + } + } + + @Test + public void getRequestMetadata_serverError_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR); + transportFactory.transport.setServiceAccountEmail("SA_CLIENT_EMAIL"); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + try { + credentials.getRequestMetadata(CALL_URI); + fail("Expected error refreshing token."); + } catch (IOException expected) { + String message = expected.getMessage(); + assertTrue(message.contains(Integer.toString(HttpStatusCodes.STATUS_CODE_SERVER_ERROR))); + assertTrue(message.contains("Unexpected")); + } + } + + @Test + public void equals_true() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + ComputeEngineCredentials explicitUniverseCredentials = + ComputeEngineCredentials.newBuilder() + .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) + .setHttpTransportFactory(transportFactory) + .build(); + ComputeEngineCredentials otherCredentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, otherCredentials.getUniverseDomain()); + assertFalse(explicitUniverseCredentials.equals(otherCredentials)); + assertFalse(otherCredentials.equals(explicitUniverseCredentials)); + ComputeEngineCredentials otherExplicitUniverseCredentials = + ComputeEngineCredentials.newBuilder() + .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) + .setHttpTransportFactory(transportFactory) + .build(); + assertTrue(explicitUniverseCredentials.equals(otherExplicitUniverseCredentials)); + assertTrue(otherExplicitUniverseCredentials.equals(explicitUniverseCredentials)); + } + + @Test + public void equals_false_transportFactory() throws IOException { + MockHttpTransportFactory httpTransportFactory = new MockHttpTransportFactory(); + MockMetadataServerTransportFactory serverTransportFactory = + new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(serverTransportFactory) + .build(); + ComputeEngineCredentials otherCredentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(httpTransportFactory).build(); + assertFalse(credentials.equals(otherCredentials)); + assertFalse(otherCredentials.equals(credentials)); + } + + @Test + public void toString_explicit_containsFields() throws IOException { + MockMetadataServerTransportFactory serverTransportFactory = + new MockMetadataServerTransportFactory(); + String expectedToString = + String.format( + "ComputeEngineCredentials{quotaProjectId=%s, universeDomain=%s, isExplicitUniverseDomain=%s, transportFactoryClassName=%s, scopes=%s}", + "some-project", + "some-domain", + true, + MockMetadataServerTransportFactory.class.getName(), + "[some scope]"); + GoogleCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(serverTransportFactory) + .setQuotaProjectId("some-project") + .setUniverseDomain("some-domain") + .build(); + credentials = credentials.createScoped("some scope"); + assertEquals(expectedToString, credentials.toString()); + } + + @Test + public void hashCode_equals() throws IOException { + MockMetadataServerTransportFactory serverTransportFactory = + new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(serverTransportFactory) + .build(); + ComputeEngineCredentials otherCredentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(serverTransportFactory) + .build(); + assertEquals(credentials.hashCode(), otherCredentials.hashCode()); + } + + @Test + public void toBuilder() { + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(new MockMetadataServerTransportFactory()) + .setQuotaProjectId("quota-project") + .build(); + + ComputeEngineCredentials secondCredentials = credentials.toBuilder().build(); + + assertEquals(credentials, secondCredentials); + } + + @Test + public void serialize() throws IOException, ClassNotFoundException { + MockMetadataServerTransportFactory serverTransportFactory = + new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(serverTransportFactory) + .build(); + GoogleCredentials deserializedCredentials = serializeAndDeserialize(credentials); + assertEquals(credentials, deserializedCredentials); + assertEquals(credentials.hashCode(), deserializedCredentials.hashCode()); + assertEquals(credentials.toString(), deserializedCredentials.toString()); + assertSame(deserializedCredentials.clock, Clock.SYSTEM); + credentials = ComputeEngineCredentials.newBuilder().build(); + deserializedCredentials = serializeAndDeserialize(credentials); + assertEquals(credentials, deserializedCredentials); + assertEquals(credentials.hashCode(), deserializedCredentials.hashCode()); + assertEquals(credentials.toString(), deserializedCredentials.toString()); + assertSame(deserializedCredentials.clock, Clock.SYSTEM); + } + + @Test + public void getAccount_sameAs() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + assertEquals(defaultAccountEmail, credentials.getAccount()); + + // metric headers are not supported for getAccount() + Map> headers = transportFactory.transport.getRequest().getHeaders(); + assertFalse(headers.containsKey(MetricsUtils.API_CLIENT_HEADER)); + } + + @Test + public void getAccount_missing_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (isGetServiceAccountsUrl(url)) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND) + .setContent(""); + } + }; } - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(responseSequence.poll()) - .setContent(TestUtils.errorJson("Some error")); - } - }; - } - }; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - IOException exception = - Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken()); - assertFalse(exception instanceof GoogleAuthException); - } - } - - @Test - public void getUniverseDomain_fromMetadata() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_OK) - .setContent("some-universe.xyz"); - } - }; - } - }; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String universeDomain = credentials.getUniverseDomain(); - assertEquals("some-universe.xyz", universeDomain); - assertEquals(false, credentials.isExplicitUniverseDomain()); - } - - @Test - public void getUniverseDomain_fromMetadata_emptyBecomesDefault() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_OK) - .setContent(""); - } - }; - } - }; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String universeDomain = credentials.getUniverseDomain(); - assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); - assertEquals(false, credentials.isExplicitUniverseDomain()); - } - - @Test - public void getUniverseDomain_fromMetadata_404_default() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND) - .setContent("some content"); - } - }; - } - }; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String universeDomain = credentials.getUniverseDomain(); - assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); - assertEquals(false, credentials.isExplicitUniverseDomain()); - } - - @Test - public void getUniverseDomain_explicitSet_NoMdsCall() throws IOException { - MockRequestCountingTransportFactory transportFactory = - new MockRequestCountingTransportFactory(); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(transportFactory) - .setUniverseDomain("explicit.universe") - .build(); - - String universeDomain = credentials.getUniverseDomain(); - assertEquals("explicit.universe", universeDomain); - assertEquals(true, credentials.isExplicitUniverseDomain()); - assertEquals(0, transportFactory.transport.getRequestCount()); - } - - @Test - public void getUniverseDomain_explicitGduSet_NoMdsCall() throws IOException { - MockRequestCountingTransportFactory transportFactory = - new MockRequestCountingTransportFactory(); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder() - .setHttpTransportFactory(transportFactory) - .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) - .build(); - - String universeDomain = credentials.getUniverseDomain(); - assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); - assertEquals(true, credentials.isExplicitUniverseDomain()); - assertEquals(0, transportFactory.transport.getRequestCount()); - } - - @Test - public void getUniverseDomain_fromMetadata_non404error_throws() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - MockMetadataServerTransport transport = transportFactory.transport; - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - for (int status = 400; status < 600; status++) { - // 404 should not throw and tested separately - if (status == 404) { - continue; + return super.buildRequest(method, url); + } + }; + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + try { + credentials.getAccount(); + fail("Fetching default service account should have failed"); + } catch (RuntimeException e) { + assertEquals("Failed to get service account", e.getMessage()); + assertNotNull(e.getCause()); + assertTrue(e.getCause().getMessage().contains("404")); + } + } + + @Test + public void getAccount_emptyContent_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (isGetServiceAccountsUrl(url)) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_OK); + } + }; } - try { - transportFactory.transport.setStatusCode(status); - credentials.getUniverseDomain(); - fail("Should not be able to use credential without exception."); - } catch (GoogleAuthException ex) { - assertTrue(ex.isRetryable()); + return super.buildRequest(method, url); + } + }; + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + try { + credentials.getAccount(); + fail("Fetching default service account should have failed"); + } catch (RuntimeException e) { + assertEquals("Failed to get service account", e.getMessage()); + assertNotNull(e.getCause()); + assertTrue(e.getCause().getMessage().contains("Empty content")); + } + } + + @Test + public void sign_sameAs() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; + + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + transportFactory.transport.setSignature(expectedSignature); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + assertArrayEquals(expectedSignature, credentials.sign(expectedSignature)); + } + + @Test + public void sign_getUniverseException() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + + String defaultAccountEmail = "mail@mail.com"; + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + transportFactory.transport.setStatusCode(501); + Assert.assertThrows(IOException.class, credentials::getUniverseDomain); + + byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; + SigningException signingException = + Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature)); + assertEquals("Failed to sign: Error obtaining universe domain", signingException.getMessage()); + } + + @Test + public void sign_getAccountFails() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD}; + + transportFactory.transport.setSignature(expectedSignature); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(expectedSignature)); + assertNotNull(exception.getMessage()); + assertNotNull(exception.getCause()); + } + + @Test + public void sign_accessDenied_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (isSignRequestUrl(url)) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_FORBIDDEN) + .setContent(TestUtils.errorJson("Sign Error")); + } + }; } - } - } - - @Test - public void sign_emptyContent_throws() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - String defaultAccountEmail = "mail@mail.com"; - - transportFactory.transport = - new MockMetadataServerTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - if (isSignRequestUrl(url)) { - return new MockLowLevelHttpRequest(url) { - @Override - public LowLevelHttpResponse execute() throws IOException { - return new MockLowLevelHttpResponse() - .setStatusCode(HttpStatusCodes.STATUS_CODE_OK); - } - }; - } - return super.buildRequest(method, url); - } - }; - - transportFactory.transport.setAccessToken(ACCESS_TOKEN); - transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - byte[] bytes = {0xD, 0xE, 0xA, 0xD}; - - SigningException exception = - Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); - assertEquals("Failed to sign the provided bytes", exception.getMessage()); - assertNotNull(exception.getCause()); - assertTrue(exception.getCause().getMessage().contains("Empty content")); - } - - @Test - public void idTokenWithAudience_sameAs() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setIdToken(STANDARD_ID_TOKEN); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String targetAudience = "https://foo.bar"; - IdTokenCredentials tokenCredential = - IdTokenCredentials.newBuilder() - .setIdTokenProvider(credentials) - .setTargetAudience(targetAudience) - .build(); - tokenCredential.refresh(); - assertEquals(STANDARD_ID_TOKEN, tokenCredential.getAccessToken().getTokenValue()); - assertEquals(STANDARD_ID_TOKEN, tokenCredential.getIdToken().getTokenValue()); - assertEquals( - targetAudience, - (String) tokenCredential.getIdToken().getJsonWebSignature().getPayload().getAudience()); - } - - @Test - public void idTokenWithAudience_standard() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String targetAudience = "https://foo.bar"; - IdTokenCredentials tokenCredential = - IdTokenCredentials.newBuilder() - .setIdTokenProvider(credentials) - .setTargetAudience(targetAudience) - .build(); - tokenCredential.refresh(); - assertEquals(STANDARD_ID_TOKEN, tokenCredential.getAccessToken().getTokenValue()); - assertEquals(STANDARD_ID_TOKEN, tokenCredential.getIdToken().getTokenValue()); - assertNull(tokenCredential.getIdToken().getJsonWebSignature().getPayload().get("google")); - } - - @Test - @SuppressWarnings("unchecked") - public void idTokenWithAudience_full() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String targetAudience = "https://foo.bar"; - IdTokenCredentials tokenCredential = - IdTokenCredentials.newBuilder() - .setIdTokenProvider(credentials) - .setTargetAudience(targetAudience) - .setOptions(Arrays.asList(IdTokenProvider.Option.FORMAT_FULL)) - .build(); - tokenCredential.refresh(); - Payload p = tokenCredential.getIdToken().getJsonWebSignature().getPayload(); - assertTrue("Full ID Token format not provided", p.containsKey("google")); - ArrayMap> googleClaim = - (ArrayMap>) p.get("google"); - assertTrue(googleClaim.containsKey("compute_engine")); - - // verify metrics header - Map> requestHeaders = transportFactory.transport.getRequest().getHeaders(); - com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "it", "mds"); - } - - @Test - @SuppressWarnings("unchecked") - public void idTokenWithAudience_licenses() throws IOException { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - String targetAudience = "https://foo.bar"; - IdTokenCredentials tokenCredential = - IdTokenCredentials.newBuilder() - .setIdTokenProvider(credentials) - .setTargetAudience(targetAudience) - .setOptions( - Arrays.asList( - IdTokenProvider.Option.FORMAT_FULL, IdTokenProvider.Option.LICENSES_TRUE)) - .build(); - tokenCredential.refresh(); - Payload p = tokenCredential.getIdToken().getJsonWebSignature().getPayload(); - assertTrue("Full ID Token format not provided", p.containsKey("google")); - ArrayMap> googleClaim = - (ArrayMap>) p.get("google"); - assertTrue(googleClaim.containsKey("compute_engine")); - ArrayMap computeEngineClaim = - (ArrayMap) googleClaim.get("compute_engine"); - assertTrue(computeEngineClaim.containsKey("license_id")); - } - - @Test - public void idTokenWithAudience_404StatusCode() { - int statusCode = HttpStatusCodes.STATUS_CODE_NOT_FOUND; - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - IOException exception = - assertThrows(IOException.class, () -> credentials.idTokenWithAudience("Audience", null)); - assertEquals( - String.format( - "Error code %s trying to get identity token from" - + " Compute Engine metadata. This may be because the virtual machine instance" - + " does not have permission scopes specified.", - statusCode), - exception.getMessage()); - } - - @Test - public void idTokenWithAudience_emptyContent() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setEmptyContent(true); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - IOException exception = - assertThrows(IOException.class, () -> credentials.idTokenWithAudience("Audience", null)); - assertEquals(METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE, exception.getMessage()); - } - - @Test - public void idTokenWithAudience_503StatusCode() { - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE); - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - assertThrows( - GoogleAuthException.class, () -> credentials.idTokenWithAudience("Audience", null)); - } - - // --- Agent Identity Tests --- - - @Test - public void refreshAccessToken_noAgentConfig_requestsNormalToken() throws IOException { - // No Agent Identity config in environment - envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null); - - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); - transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - AccessToken token = credentials.refreshAccessToken(); - assertNotNull(token); - assertEquals(ACCESS_TOKEN, token.getTokenValue()); - - // Verify NO fingerprint was sent - String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); - assertFalse(requestUrl.contains("bindCertificateFingerprint")); - } - - @Test - public void refreshAccessToken_withStandardCert_requestsNormalToken() throws IOException { - setupCertConfig("agent_cert.pem"); // Standard cert, no SPIFFE - - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); - transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - AccessToken token = credentials.refreshAccessToken(); - assertNotNull(token); - assertEquals(ACCESS_TOKEN, token.getTokenValue()); - - // Verify NO fingerprint was sent because cert didn't match SPIFFE pattern - String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); - assertFalse(requestUrl.contains("bindCertificateFingerprint")); - } - -// @Test -// public void refreshAccessToken_withAgentCert_requestsBoundToken() throws IOException { -// setupCertConfig("agent_spiffe_cert.pem"); // Valid Agent Identity cert -// -// MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); -// transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); -// transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); -// -// ComputeEngineCredentials credentials = -// ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); -// -// AccessToken token = credentials.refreshAccessToken(); -// assertNotNull(token); -// assertEquals(ACCESS_TOKEN, token.getTokenValue()); -// -// // Verify fingerprint WAS sent -// String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); -// assertTrue(requestUrl.contains("bindCertificateFingerprint")); -// } - - @Test - public void refreshAccessToken_withAgentCert_optedOut_requestsNormalToken() throws IOException { - setupCertConfig("agent_spiffe_cert.pem"); - envProvider.setEnv("GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); - - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); - transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - AccessToken token = credentials.refreshAccessToken(); - assertNotNull(token); - assertEquals(ACCESS_TOKEN, token.getTokenValue()); - - // Verify NO fingerprint due to opt-out - String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); - assertFalse(requestUrl.contains("bindCertificateFingerprint")); - } - - @Test - public void refreshAccessToken_agentConfigMissingFile_throws() throws IOException { - // Point config to a non-existent file and shorten timeout for test - envProvider.setEnv( - "GOOGLE_API_CERTIFICATE_CONFIG", - tempDir.resolve("missing_config.json").toAbsolutePath().toString()); - AgentIdentityUtils.TOTAL_TIMEOUT_MS = 100; - AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 10; - AgentIdentityUtils.FAST_POLL_DURATION_MS = 50; - AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 10; - - MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); - - ComputeEngineCredentials credentials = - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - - IOException e = assertThrows(IOException.class, credentials::refreshAccessToken); - assertTrue( - e.getMessage() - .contains( - "Certificate config or certificate file not found after multiple retries")); - } - - private void setupCertConfig(String certResourceName) throws IOException { - // Copy cert resource to temp file - Path certPath = tempDir.resolve("cert.pem"); - try (InputStream certStream = - getClass().getClassLoader().getResourceAsStream(certResourceName)) { - assertNotNull("Test resource " + certResourceName + " not found", certStream); - Files.copy(certStream, certPath); - } - - // Create config file pointing to cert - Path configPath = tempDir.resolve("config.json"); - String configContent = - "{\"cert_configs\": {\"workload\": {\"cert_path\": \"" - + certPath.toAbsolutePath().toString().replace("\\", "\\\\") - + "\"}}}"; - Files.write(configPath, configContent.getBytes(StandardCharsets.UTF_8)); - - // Set env var - envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configPath.toAbsolutePath().toString()); - } - - static class MockMetadataServerTransportFactory implements HttpTransportFactory { - - MockMetadataServerTransport transport = - new MockMetadataServerTransport(SCOPE_TO_ACCESS_TOKEN_MAP); - - @Override - public HttpTransport create() { - return transport; - } - } - - // A helper class to mock System.getenv for testing purposes within this file. - private static class TestEnvironmentProvider { - private final Map env = new HashMap<>(); - - void setEnv(String key, String value) { - env.put(key, value); - } - - String getEnv(String key) { - return env.get(key); - } - } -} \ No newline at end of file + return super.buildRequest(method, url); + } + }; + + transportFactory.transport.setAccessToken(ACCESS_TOKEN); + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + byte[] bytes = {0xD, 0xE, 0xA, 0xD}; + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); + assertEquals("Failed to sign the provided bytes", exception.getMessage()); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause().getMessage().contains("403")); + } + + @Test + public void sign_serverError_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (isSignRequestUrl(url)) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(TestUtils.errorJson("Sign Error")); + } + }; + } + return super.buildRequest(method, url); + } + }; + + transportFactory.transport.setAccessToken(ACCESS_TOKEN); + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + byte[] bytes = {0xD, 0xE, 0xA, 0xD}; + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); + assertEquals("Failed to sign the provided bytes", exception.getMessage()); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause().getMessage().contains("500")); + } + + @Test + public void refresh_503_retryable_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE) + .setContent(TestUtils.errorJson("Some error")); + } + }; + } + }; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + IOException exception = + Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken()); + assertTrue(exception.getCause().getMessage().contains("503")); + assertTrue(exception instanceof GoogleAuthException); + assertTrue(((GoogleAuthException) exception).isRetryable()); + } + + @Test + public void refresh_non503_ioexception_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + final Queue responseSequence = new ArrayDeque<>(); + IntStream.rangeClosed(400, 600).forEach(i -> responseSequence.add(i)); + + while (!responseSequence.isEmpty()) { + if (responseSequence.peek() == 503) { + responseSequence.poll(); + continue; + } + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(responseSequence.poll()) + .setContent(TestUtils.errorJson("Some error")); + } + }; + } + }; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + IOException exception = + Assert.assertThrows(IOException.class, () -> credentials.refreshAccessToken()); + assertFalse(exception instanceof GoogleAuthException); + } + } + + @Test + public void getUniverseDomain_fromMetadata() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_OK) + .setContent("some-universe.xyz"); + } + }; + } + }; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String universeDomain = credentials.getUniverseDomain(); + assertEquals("some-universe.xyz", universeDomain); + assertEquals(false, credentials.isExplicitUniverseDomain()); + } + + @Test + public void getUniverseDomain_fromMetadata_emptyBecomesDefault() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_OK) + .setContent(""); + } + }; + } + }; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String universeDomain = credentials.getUniverseDomain(); + assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); + assertEquals(false, credentials.isExplicitUniverseDomain()); + } + + @Test + public void getUniverseDomain_fromMetadata_404_default() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND) + .setContent("some content"); + } + }; + } + }; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String universeDomain = credentials.getUniverseDomain(); + assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); + assertEquals(false, credentials.isExplicitUniverseDomain()); + } + + @Test + public void getUniverseDomain_explicitSet_NoMdsCall() throws IOException { + MockRequestCountingTransportFactory transportFactory = + new MockRequestCountingTransportFactory(); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setUniverseDomain("explicit.universe") + .build(); + + String universeDomain = credentials.getUniverseDomain(); + assertEquals("explicit.universe", universeDomain); + assertEquals(true, credentials.isExplicitUniverseDomain()); + assertEquals(0, transportFactory.transport.getRequestCount()); + } + + @Test + public void getUniverseDomain_explicitGduSet_NoMdsCall() throws IOException { + MockRequestCountingTransportFactory transportFactory = + new MockRequestCountingTransportFactory(); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setUniverseDomain(Credentials.GOOGLE_DEFAULT_UNIVERSE) + .build(); + + String universeDomain = credentials.getUniverseDomain(); + assertEquals(Credentials.GOOGLE_DEFAULT_UNIVERSE, universeDomain); + assertEquals(true, credentials.isExplicitUniverseDomain()); + assertEquals(0, transportFactory.transport.getRequestCount()); + } + + @Test + public void getUniverseDomain_fromMetadata_non404error_throws() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + MockMetadataServerTransport transport = transportFactory.transport; + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + for (int status = 400; status < 600; status++) { + // 404 should not throw and tested separately + if (status == 404) { + continue; + } + try { + transportFactory.transport.setStatusCode(status); + credentials.getUniverseDomain(); + fail("Should not be able to use credential without exception."); + } catch (GoogleAuthException ex) { + assertTrue(ex.isRetryable()); + } + } + } + + @Test + public void sign_emptyContent_throws() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + String defaultAccountEmail = "mail@mail.com"; + + transportFactory.transport = + new MockMetadataServerTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (isSignRequestUrl(url)) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + return new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_OK); + } + }; + } + return super.buildRequest(method, url); + } + }; + + transportFactory.transport.setAccessToken(ACCESS_TOKEN); + transportFactory.transport.setServiceAccountEmail(defaultAccountEmail); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + byte[] bytes = {0xD, 0xE, 0xA, 0xD}; + + SigningException exception = + Assert.assertThrows(SigningException.class, () -> credentials.sign(bytes)); + assertEquals("Failed to sign the provided bytes", exception.getMessage()); + assertNotNull(exception.getCause()); + assertTrue(exception.getCause().getMessage().contains("Empty content")); + } + + @Test + public void idTokenWithAudience_sameAs() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setIdToken(STANDARD_ID_TOKEN); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String targetAudience = "https://foo.bar"; + IdTokenCredentials tokenCredential = + IdTokenCredentials.newBuilder() + .setIdTokenProvider(credentials) + .setTargetAudience(targetAudience) + .build(); + tokenCredential.refresh(); + assertEquals(STANDARD_ID_TOKEN, tokenCredential.getAccessToken().getTokenValue()); + assertEquals(STANDARD_ID_TOKEN, tokenCredential.getIdToken().getTokenValue()); + assertEquals( + targetAudience, + (String) tokenCredential.getIdToken().getJsonWebSignature().getPayload().getAudience()); + } + + @Test + public void idTokenWithAudience_standard() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String targetAudience = "https://foo.bar"; + IdTokenCredentials tokenCredential = + IdTokenCredentials.newBuilder() + .setIdTokenProvider(credentials) + .setTargetAudience(targetAudience) + .build(); + tokenCredential.refresh(); + assertEquals(STANDARD_ID_TOKEN, tokenCredential.getAccessToken().getTokenValue()); + assertEquals(STANDARD_ID_TOKEN, tokenCredential.getIdToken().getTokenValue()); + assertNull(tokenCredential.getIdToken().getJsonWebSignature().getPayload().get("google")); + } + + @Test + @SuppressWarnings("unchecked") + public void idTokenWithAudience_full() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String targetAudience = "https://foo.bar"; + IdTokenCredentials tokenCredential = + IdTokenCredentials.newBuilder() + .setIdTokenProvider(credentials) + .setTargetAudience(targetAudience) + .setOptions(Arrays.asList(IdTokenProvider.Option.FORMAT_FULL)) + .build(); + tokenCredential.refresh(); + Payload p = tokenCredential.getIdToken().getJsonWebSignature().getPayload(); + assertTrue("Full ID Token format not provided", p.containsKey("google")); + ArrayMap> googleClaim = + (ArrayMap>) p.get("google"); + assertTrue(googleClaim.containsKey("compute_engine")); + + // verify metrics header + Map> requestHeaders = transportFactory.transport.getRequest().getHeaders(); + com.google.auth.oauth2.TestUtils.validateMetricsHeader(requestHeaders, "it", "mds"); + } + + @Test + @SuppressWarnings("unchecked") + public void idTokenWithAudience_licenses() throws IOException { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + String targetAudience = "https://foo.bar"; + IdTokenCredentials tokenCredential = + IdTokenCredentials.newBuilder() + .setIdTokenProvider(credentials) + .setTargetAudience(targetAudience) + .setOptions( + Arrays.asList( + IdTokenProvider.Option.FORMAT_FULL, IdTokenProvider.Option.LICENSES_TRUE)) + .build(); + tokenCredential.refresh(); + Payload p = tokenCredential.getIdToken().getJsonWebSignature().getPayload(); + assertTrue("Full ID Token format not provided", p.containsKey("google")); + ArrayMap> googleClaim = + (ArrayMap>) p.get("google"); + assertTrue(googleClaim.containsKey("compute_engine")); + ArrayMap computeEngineClaim = + (ArrayMap) googleClaim.get("compute_engine"); + assertTrue(computeEngineClaim.containsKey("license_id")); + } + + @Test + public void idTokenWithAudience_404StatusCode() { + int statusCode = HttpStatusCodes.STATUS_CODE_NOT_FOUND; + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_NOT_FOUND); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + IOException exception = + assertThrows(IOException.class, () -> credentials.idTokenWithAudience("Audience", null)); + assertEquals( + String.format( + "Error code %s trying to get identity token from" + + " Compute Engine metadata. This may be because the virtual machine instance" + + " does not have permission scopes specified.", + statusCode), + exception.getMessage()); + } + + @Test + public void idTokenWithAudience_emptyContent() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setEmptyContent(true); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + IOException exception = + assertThrows(IOException.class, () -> credentials.idTokenWithAudience("Audience", null)); + assertEquals(METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE, exception.getMessage()); + } + + @Test + public void idTokenWithAudience_503StatusCode() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + assertThrows( + GoogleAuthException.class, () -> credentials.idTokenWithAudience("Audience", null)); + } + + // --- Agent Identity Tests --- + + @Test + public void refreshAccessToken_noAgentConfig_requestsNormalToken() throws IOException { + // No Agent Identity config in environment + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null); + + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + AccessToken token = credentials.refreshAccessToken(); + assertNotNull(token); + assertEquals(ACCESS_TOKEN, token.getTokenValue()); + + // Verify NO fingerprint was sent + String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); + assertFalse(requestUrl.contains("bindCertificateFingerprint")); + } + + @Test + public void refreshAccessToken_withStandardCert_requestsNormalToken() throws IOException { + setupCertConfig("agent_cert.pem"); // Standard cert, no SPIFFE + + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + AccessToken token = credentials.refreshAccessToken(); + assertNotNull(token); + assertEquals(ACCESS_TOKEN, token.getTokenValue()); + + // Verify NO fingerprint was sent because cert didn't match SPIFFE pattern + String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); + assertFalse(requestUrl.contains("bindCertificateFingerprint")); + } + + // @Test + // public void refreshAccessToken_withAgentCert_requestsBoundToken() throws IOException { + // setupCertConfig("agent_spiffe_cert.pem"); // Valid Agent Identity cert + // + // MockMetadataServerTransportFactory transportFactory = new + // MockMetadataServerTransportFactory(); + // transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + // transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); + // + // ComputeEngineCredentials credentials = + // + // ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + // + // AccessToken token = credentials.refreshAccessToken(); + // assertNotNull(token); + // assertEquals(ACCESS_TOKEN, token.getTokenValue()); + // + // // Verify fingerprint WAS sent + // String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); + // assertTrue(requestUrl.contains("bindCertificateFingerprint")); + // } + + @Test + public void refreshAccessToken_withAgentCert_optedOut_requestsNormalToken() throws IOException { + setupCertConfig("agent_spiffe_cert.pem"); + envProvider.setEnv("GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); + + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + AccessToken token = credentials.refreshAccessToken(); + assertNotNull(token); + assertEquals(ACCESS_TOKEN, token.getTokenValue()); + + // Verify NO fingerprint due to opt-out + String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); + assertFalse(requestUrl.contains("bindCertificateFingerprint")); + } + + @Test + public void refreshAccessToken_agentConfigMissingFile_throws() throws IOException { + // Point config to a non-existent file and shorten timeout for test + envProvider.setEnv( + "GOOGLE_API_CERTIFICATE_CONFIG", + tempDir.resolve("missing_config.json").toAbsolutePath().toString()); + AgentIdentityUtils.TOTAL_TIMEOUT_MS = 100; + AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 10; + AgentIdentityUtils.FAST_POLL_DURATION_MS = 50; + AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 10; + + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + IOException e = assertThrows(IOException.class, credentials::refreshAccessToken); + assertTrue( + e.getMessage() + .contains("Certificate config or certificate file not found after multiple retries")); + } + + private void setupCertConfig(String certResourceName) throws IOException { + // Copy cert resource to temp file + Path certPath = tempDir.resolve("cert.pem"); + try (InputStream certStream = + getClass().getClassLoader().getResourceAsStream(certResourceName)) { + assertNotNull("Test resource " + certResourceName + " not found", certStream); + Files.copy(certStream, certPath); + } + + // Create config file pointing to cert + Path configPath = tempDir.resolve("config.json"); + String configContent = + "{\"cert_configs\": {\"workload\": {\"cert_path\": \"" + + certPath.toAbsolutePath().toString().replace("\\", "\\\\") + + "\"}}}"; + Files.write(configPath, configContent.getBytes(StandardCharsets.UTF_8)); + + // Set env var + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configPath.toAbsolutePath().toString()); + } + + static class MockMetadataServerTransportFactory implements HttpTransportFactory { + + MockMetadataServerTransport transport = + new MockMetadataServerTransport(SCOPE_TO_ACCESS_TOKEN_MAP); + + @Override + public HttpTransport create() { + return transport; + } + } + + // A helper class to mock System.getenv for testing purposes within this file. + private static class TestEnvironmentProvider { + private final Map env = new HashMap<>(); + + void setEnv(String key, String value) { + env.put(key, value); + } + + String getEnv(String key) { + return env.get(key); + } + } +} From aa70b56ecb020ca0573c294cef4aa987be7409e3 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 6 Nov 2025 17:47:26 -0800 Subject: [PATCH 3/6] Changed so the tests wait time is reduced by using TimeService to control the sleep time for threads. Fixed the agent_spiffe_cert.pem --- .../auth/oauth2/AgentIdentityUtils.java | 61 +++++++- .../auth/oauth2/AgentIdentityUtilsTest.java | 64 +++----- .../oauth2/ComputeEngineCredentialsTest.java | 146 +++++++++++------- .../DefaultCredentialsProviderTest.java | 14 ++ .../testresources/agent_spiffe_cert.pem | 37 ++--- 5 files changed, 200 insertions(+), 122 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java b/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java index bec88e59d..f30e596ba 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java +++ b/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java @@ -69,10 +69,10 @@ final class AgentIdentityUtils { Pattern.compile("^agents\\.global\\.proj-\\d+\\.system\\.id\\.goog$")); // Polling configuration - static long TOTAL_TIMEOUT_MS = 30000; // 30 seconds - static long FAST_POLL_DURATION_MS = 5000; // 5 seconds - static long FAST_POLL_INTERVAL_MS = 100; // 0.1 seconds - static long SLOW_POLL_INTERVAL_MS = 500; // 0.5 seconds + private static final long TOTAL_TIMEOUT_MS = 30000; // 30 seconds + private static final long FAST_POLL_DURATION_MS = 5000; // 5 seconds + private static final long FAST_POLL_INTERVAL_MS = 100; // 0.1 seconds + private static final long SLOW_POLL_INTERVAL_MS = 500; // 0.5 seconds private static final int SAN_URI_TYPE = 6; private static final String SPIFFE_SCHEME_PREFIX = "spiffe://"; @@ -84,6 +84,30 @@ interface EnvReader { private static EnvReader envReader = System::getenv; + /** + * Internal interface to allow mocking time and sleep for tests. This is used to prevent tests + * from running for long periods of time when polling is involved. + */ + @VisibleForTesting + interface TimeService { + long currentTimeMillis(); + + void sleep(long millis) throws InterruptedException; + } + + private static TimeService timeService = + new TimeService() { + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } + + @Override + public void sleep(long millis) throws InterruptedException { + Thread.sleep(millis); + } + }; + private AgentIdentityUtils() {} /** @@ -114,7 +138,7 @@ private static boolean isOptedOut() { /** Polls for the certificate config file and the certificate file it references. */ private static String getCertificatePathWithRetry(String certConfigPath) throws IOException { - long startTime = System.currentTimeMillis(); + long startTime = timeService.currentTimeMillis(); boolean warned = false; while (true) { @@ -127,10 +151,10 @@ private static String getCertificatePathWithRetry(String certConfigPath) throws } } catch (Exception e) { // Ignore exceptions during polling and retry - LOGGER.log(Level.FINE, "Error while polling for certificate files", e); + LOGGER.log(Level.FINE, "Error while polling for certificate files"); } - long elapsedTime = System.currentTimeMillis() - startTime; + long elapsedTime = timeService.currentTimeMillis() - startTime; if (elapsedTime >= TOTAL_TIMEOUT_MS) { throw new IOException( "Certificate config or certificate file not found after multiple retries. " @@ -151,7 +175,7 @@ private static String getCertificatePathWithRetry(String certConfigPath) throws try { long sleepTime = elapsedTime < FAST_POLL_DURATION_MS ? FAST_POLL_INTERVAL_MS : SLOW_POLL_INTERVAL_MS; - Thread.sleep(sleepTime); + timeService.sleep(sleepTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Interrupted while waiting for certificate files", e); @@ -239,4 +263,25 @@ static String calculateCertificateFingerprint(X509Certificate cert) throws IOExc static void setEnvReader(EnvReader reader) { envReader = reader; } + + @VisibleForTesting + static void setTimeService(TimeService service) { + timeService = service; + } + + @VisibleForTesting + static void resetTimeService() { + timeService = + new TimeService() { + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } + + @Override + public void sleep(long millis) throws InterruptedException { + Thread.sleep(millis); + } + }; + } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java index 859daab5a..4711f8d36 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -39,49 +40,20 @@ public class AgentIdentityUtilsTest { private static final String INVALID_SPIFFE_FORMAT = "spiffe://agents.global.org-INVALID.system.id.goog/path"; - // A minimal, valid self-signed X.509 certificate (PEM format) for testing loading. - // Generated for testing purposes. - private static final String TEST_CERT_PEM = - "-----BEGIN CERTIFICATE-----\n" - + "MIIDWTCCAkGgAwIBAgIUX5/1aT1uuxgj1+F7Q/r+5Q9y4JQwDQYJKoZIhvcNAQEL\n" - + "BQAwHTEbMBkGA1UEAwwSdGVzdC5leGFtcGxlLmNvbTAeFw0yNDAxMDEwMDAwMDBa\n" - + "Fw0zNDAxMDEwMDAwMDBaMB0xGzAZBgNVBAMMEnRlc3QuZXhhbXBsZS5jb20wggEi\n" - + "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDV/8Q/5+8+X9Y+5+6+7+8+9+0+\n" - + "A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/\n" - + "g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B\n" - + "/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h\n" - + "/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/\n" - + "D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/\n" - + "j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E\n" - + "AgMBAAGjUzBRMB0GA1UdDgQWBBS/1/2/3/4/5/6/7/8/9/+A/B/C/DAfBgNVHSME\n" - + "GDAWgBS/1/2/3/4/5/6/7/8/9/+A/B/C/DAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\n" - + "SIb3DQEBCwUAA4IBAQDV/8Q/5+8+X9Y+5+6+7+8+9+0+A/B/C/D/E/F/G/H/I/J/\n" - + "K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/\n" - + "q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E/F/G/H/I/J/K/L\n" - + "/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r\n" - + "/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E/F/G/H/I/J/K/L/M/\n" - + "N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/\n" - + "t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/B/C/D/E\n" - + "-----END CERTIFICATE-----"; - private TestEnvironmentProvider envProvider; private Path tempDir; @Before public void setUp() throws IOException { envProvider = new TestEnvironmentProvider(); - // Inject our test environment reader AgentIdentityUtils.setEnvReader(envProvider::getEnv); tempDir = Files.createTempDirectory("agent_identity_test"); } @After public void tearDown() throws IOException { - // Reset polling constants to defaults after each test to avoid side effects - AgentIdentityUtils.TOTAL_TIMEOUT_MS = 30000; - AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 100; - AgentIdentityUtils.FAST_POLL_DURATION_MS = 5000; - AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 500; + // Reset the time service to default after each test + AgentIdentityUtils.resetTimeService(); // Clean up temp files if (tempDir != null) { @@ -176,8 +148,8 @@ public void getAgentIdentityCertificate_noConfigEnvVar_returnsNull() throws IOEx @Test public void getAgentIdentityCertificate_happyPath_loadsCertificate() throws IOException { // Setup: Get the absolute path of the test resource. - URL certUrl = getClass().getClassLoader().getResource("agent_cert.pem"); - assertNotNull("Test resource agent_cert.pem not found", certUrl); + URL certUrl = getClass().getClassLoader().getResource("x509_leaf_certificate.pem"); + assertNotNull("Test resource x509_leaf_certificate.pem not found", certUrl); String certPath = new File(certUrl.getFile()).getAbsolutePath(); // Create config file pointing to the cert. @@ -206,7 +178,7 @@ public void getAgentIdentityCertificate_happyPath_loadsCertificate() throws IOEx // Verify assertNotNull(cert); - // Basic verification that it loaded OUR cert (checking issuer from agent_cert.pem) + // Basic verification that it loaded OUR cert assertTrue(cert.getIssuerDN().getName().contains("unit-tests")); } @@ -217,11 +189,9 @@ public void getAgentIdentityCertificate_timeout_throwsIOException() { "GOOGLE_API_CERTIFICATE_CONFIG", tempDir.resolve("missing.json").toAbsolutePath().toString()); - // Reduce timeout to make test fast (e.g., 100ms total) - AgentIdentityUtils.TOTAL_TIMEOUT_MS = 100; - AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 10; - AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 10; - AgentIdentityUtils.FAST_POLL_DURATION_MS = 50; + // Use a fake time service that advances time rapidly when sleep is called. + // This allows the 30s timeout loop to complete instantly in test execution time. + AgentIdentityUtils.setTimeService(new FakeTimeService()); // Execute & Verify IOException e = @@ -231,6 +201,22 @@ public void getAgentIdentityCertificate_timeout_throwsIOException() { .contains("Certificate config or certificate file not found after multiple retries")); } + // Fake time service that advances time when sleep is requested. + private static class FakeTimeService implements AgentIdentityUtils.TimeService { + private final AtomicLong currentTime = new AtomicLong(0); + + @Override + public long currentTimeMillis() { + return currentTime.get(); + } + + @Override + public void sleep(long millis) throws InterruptedException { + // Instead of actually sleeping, just advance the fake clock. + currentTime.addAndGet(millis); + } + } + // A helper class to mock System.getenv for testing purposes within this file. private static class TestEnvironmentProvider { private final java.util.Map env = new java.util.HashMap<>(); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index f1981b88c..0f4e15961 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -57,7 +57,6 @@ import com.google.auth.TestUtils; import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.DefaultCredentialsProviderTest.MockRequestCountingTransportFactory; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -72,6 +71,7 @@ import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -148,19 +148,10 @@ public void setUp() throws IOException { } @After - public void tearDown() throws IOException { - // Reset polling constants to defaults after each test - AgentIdentityUtils.TOTAL_TIMEOUT_MS = 30000; - AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 100; - AgentIdentityUtils.FAST_POLL_DURATION_MS = 5000; - AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 500; - - if (tempDir != null) { - Files.walk(tempDir) - .sorted(java.util.Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); - } + public void tearDown() { + // Reset the mocks + AgentIdentityUtils.resetTimeService(); + AgentIdentityUtils.setEnvReader(System::getenv); } @Test @@ -1181,8 +1172,6 @@ public void idTokenWithAudience_503StatusCode() { GoogleAuthException.class, () -> credentials.idTokenWithAudience("Audience", null)); } - // --- Agent Identity Tests --- - @Test public void refreshAccessToken_noAgentConfig_requestsNormalToken() throws IOException { // No Agent Identity config in environment @@ -1206,7 +1195,7 @@ public void refreshAccessToken_noAgentConfig_requestsNormalToken() throws IOExce @Test public void refreshAccessToken_withStandardCert_requestsNormalToken() throws IOException { - setupCertConfig("agent_cert.pem"); // Standard cert, no SPIFFE + setupCertConfig("x509_leaf_certificate.pem"); // Standard cert, no SPIFFE MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); @@ -1224,32 +1213,9 @@ public void refreshAccessToken_withStandardCert_requestsNormalToken() throws IOE assertFalse(requestUrl.contains("bindCertificateFingerprint")); } - // @Test - // public void refreshAccessToken_withAgentCert_requestsBoundToken() throws IOException { - // setupCertConfig("agent_spiffe_cert.pem"); // Valid Agent Identity cert - // - // MockMetadataServerTransportFactory transportFactory = new - // MockMetadataServerTransportFactory(); - // transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); - // transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); - // - // ComputeEngineCredentials credentials = - // - // ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - // - // AccessToken token = credentials.refreshAccessToken(); - // assertNotNull(token); - // assertEquals(ACCESS_TOKEN, token.getTokenValue()); - // - // // Verify fingerprint WAS sent - // String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); - // assertTrue(requestUrl.contains("bindCertificateFingerprint")); - // } - @Test - public void refreshAccessToken_withAgentCert_optedOut_requestsNormalToken() throws IOException { - setupCertConfig("agent_spiffe_cert.pem"); - envProvider.setEnv("GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); + public void refreshAccessToken_withAgentCert_requestsBoundToken() throws IOException { + setupCertConfig("agent_spiffe_cert.pem"); // Valid Agent Identity cert MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); @@ -1262,34 +1228,100 @@ public void refreshAccessToken_withAgentCert_optedOut_requestsNormalToken() thro assertNotNull(token); assertEquals(ACCESS_TOKEN, token.getTokenValue()); - // Verify NO fingerprint due to opt-out + // Verify fingerprint WAS sent String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); - assertFalse(requestUrl.contains("bindCertificateFingerprint")); + assertTrue(requestUrl.contains("bindCertificateFingerprint")); } @Test - public void refreshAccessToken_agentConfigMissingFile_throws() throws IOException { - // Point config to a non-existent file and shorten timeout for test - envProvider.setEnv( - "GOOGLE_API_CERTIFICATE_CONFIG", - tempDir.resolve("missing_config.json").toAbsolutePath().toString()); - AgentIdentityUtils.TOTAL_TIMEOUT_MS = 100; - AgentIdentityUtils.FAST_POLL_INTERVAL_MS = 10; - AgentIdentityUtils.FAST_POLL_DURATION_MS = 50; - AgentIdentityUtils.SLOW_POLL_INTERVAL_MS = 10; + public void refreshAccessToken_withAgentCert_optedOut_requestsNormalToken() throws IOException { + setupCertConfig("agent_spiffe_cert.pem"); + envProvider.setEnv("GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - IOException e = assertThrows(IOException.class, credentials::refreshAccessToken); - assertTrue( - e.getMessage() - .contains("Certificate config or certificate file not found after multiple retries")); + AccessToken token = credentials.refreshAccessToken(); + assertNotNull(token); + assertEquals(ACCESS_TOKEN, token.getTokenValue()); + + // Verify NO fingerprint due to opt-out + String requestUrl = transportFactory.transport.getRequest().getUrl().toString(); + assertFalse(requestUrl.contains("bindCertificateFingerprint")); } + @Test + + public void refreshAccessToken_agentConfigMissingFile_throws() throws IOException { + + // Point config to a non-existent file. + + envProvider.setEnv( + + AgentIdentityUtils.GOOGLE_API_CERTIFICATE_CONFIG, + + tempDir.resolve("missing_config.json").toAbsolutePath().toString()); + + + + // Use a mock TimeService to avoid actual sleeping and control time flow. + + final AtomicLong currentTime = new AtomicLong(0); + + AgentIdentityUtils.setTimeService( + + new AgentIdentityUtils.TimeService() { + + @Override + + public long currentTimeMillis() { + + return currentTime.get(); + + } + + + + @Override + + public void sleep(long millis) { + + currentTime.addAndGet(millis); + + } + + }); + + + + MockMetadataServerTransportFactory transportFactory = + + new MockMetadataServerTransportFactory(); + + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + + + + ComputeEngineCredentials credentials = + + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + + + IOException e = assertThrows(IOException.class, credentials::refreshAccessToken); + + assertTrue( + + e.getMessage() + + .contains("Certificate config or certificate file not found after multiple retries")); + + } + private void setupCertConfig(String certResourceName) throws IOException { // Copy cert resource to temp file Path certPath = tempDir.resolve("cert.pem"); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java index cb6f5a8cc..6b526a8ae 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java @@ -65,6 +65,8 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -73,6 +75,18 @@ @RunWith(JUnit4.class) public class DefaultCredentialsProviderTest { + @Before + public void setUp() { + // Isolate tests from user's GOOGLE_API_CERTIFICATE_CONFIG environment variable. + AgentIdentityUtils.setEnvReader(name -> null); + } + + @After + public void tearDown() { + // Reset to default behavior. + AgentIdentityUtils.setEnvReader(System::getenv); + } + private static final String USER_CLIENT_SECRET = "jakuaL9YyieakhECKL2SwZcu"; private static final String USER_CLIENT_ID = "ya29.1.AADtN_UtlxN3PuGAxrN2XQnZTVRvDyVWnYq4I6dws"; private static final String GCLOUDSDK_CLIENT_ID = diff --git a/oauth2_http/testresources/agent_spiffe_cert.pem b/oauth2_http/testresources/agent_spiffe_cert.pem index bf875dd1d..79a5f36fc 100644 --- a/oauth2_http/testresources/agent_spiffe_cert.pem +++ b/oauth2_http/testresources/agent_spiffe_cert.pem @@ -1,19 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIC8zCCAdugAwIBAgIULk565/k1yqg/+j11n/aB9Q4u1KUwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAwwLVGVzdCBTUElGRkUwHhcNMjUwMTAxMDAwMDAwWhcNMzUw -MTAxMDAwMDAwWjAWMRQwEgYDVQQDDAtUZXN0IFNUElGRkUwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQC5/g+uS+vGgB9cK2Y5b4x7oJg+185c+35d468+ -9/0/A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/ -e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/ -+A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/a/b/c/d/e/f -/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/0/1/2/3/4/5/6/7/8/9/+A/ -B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/ZAgMBAAGjUDBOMEwG -A1UdEQRFMEOCOSNzcGlmZmU6Ly9hZ2VudHMuZ2xvYmFsLm9yZy0xMjM0NS5zeXN0 -ZW0uaWQuZ29vZy90ZXN0MB0GA1UdDgQWBBS/1/2/3/4/5/6/7/8/9/+A/B/C/DAN -BgkqhkiG9w0BAQsFAAOCAQEAu/8A/9/+/v7+///////+/v///v7///7+///////+ -/v///v7///7+///////+/v///v7///7+///////+/v///v7///7+///////+/v// -/v7///7+///////+/v///v7///7+///////+/v///v7///7+///////+/v///v7/ -//7+///////+/v///v7///7+///////+/v///v7///7+///////+/v///v7///7+ -///////+/v///v7///7+///////+/v///v7///7+///////+/v///v7///7+//// -///+/v///v7///7+///////+/v///v7///7+///////+/v///v7///7+//// ------END CERTIFICATE----- \ No newline at end of file +MIIDPjCCAiagAwIBAgIUCYeV4dwM29T5yucwWrSWlOC9wwYwDQYJKoZIhvcNAQEL +BQAwIjEgMB4GA1UEAwwXVGVzdCBTUElGRkUgQ2VydGlmaWNhdGUwHhcNMjUxMTA3 +MDEyMjQ4WhcNMzUxMTA1MDEyMjQ4WjAiMSAwHgYDVQQDDBdUZXN0IFNQSUZGRSBD +ZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDr1Bzo +KtzIZB35acQ+mpk6yScf59AnwHjjgNCMbC7kq2DSUfQzTlu9Kd0uUB6O7DmJ73D8 +Pge4XLE/Q1B6dI6DzJx7lhPoC1BiQFUGJ4Cu+TbbdlK3RiXNAZYjIj9UKP7DejCY +WRgFB+PYyLczEkByvU9cy7Z9Uuufsn6LnYu7qOG+DcRSE41ThurZxQ14OWvLfjZm +lhZXam4VBBli8Qku8qFIALe78kpy+hp2YCRnK84amATwPpGprRACp9WVka2JDYKD +LY0OoYlyAQel6960aS11N3/2v0cvx03/LM5+Yj+DTvdyb2Mk/NVeRIKo8cM5YwPn +sTLCf1cdxJvseRMCAwEAAaNsMGowSQYDVR0RBEIwQIY+c3BpZmZlOi8vYWdlbnRz +Lmdsb2JhbC5wcm9qLTEyMzQ1LnN5c3RlbS5pZC5nb29nL3Rlc3Qtd29ya2xvYWQw +HQYDVR0OBBYEFPvn+KXBcrYCmAMopkghUczUx/IkMA0GCSqGSIb3DQEBCwUAA4IB +AQCbwd9RMFkr1C9AEgnLMWd1l9ciBbK0t1Sydu3eA0SNm2w6E58ih8O+huo6eGsM +7z0E4i7YuaHnTdah/lPMqd75YRO57GSRbvi2g+yPyw6XdFl9HCHwF4WARdTF4Nkf +1c1WstvBXb24PSSQQdy9un72ZG6f9fSVQrko6hchv8Rg6yyBTFE8APPkeMR/EJtV +cnXg4CgsQIPHxJGQrhNvQhF7VLZePlTass4bqTqTYXwAte2jX/KW3qlW/t/v4AJe +/q+pcXmNIvwRpT8zYA5tJHIDVJ+v9pWZA+nhoD9Qtr7FVHfB4mdNuFv7bMPoXN0+ +mCPzP08MnjgbX7zRETVlblrx +-----END CERTIFICATE----- From 445498cc9338f8116adb6c5aaa30c667e4d95ac1 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Thu, 6 Nov 2025 17:47:52 -0800 Subject: [PATCH 4/6] Lint fix. --- .../oauth2/ComputeEngineCredentialsTest.java | 81 +++++++------------ 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index 0f4e15961..7a069f00e 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -1254,73 +1254,48 @@ public void refreshAccessToken_withAgentCert_optedOut_requestsNormalToken() thro assertFalse(requestUrl.contains("bindCertificateFingerprint")); } - @Test - - public void refreshAccessToken_agentConfigMissingFile_throws() throws IOException { - - // Point config to a non-existent file. - - envProvider.setEnv( - - AgentIdentityUtils.GOOGLE_API_CERTIFICATE_CONFIG, - - tempDir.resolve("missing_config.json").toAbsolutePath().toString()); - - - - // Use a mock TimeService to avoid actual sleeping and control time flow. - - final AtomicLong currentTime = new AtomicLong(0); - - AgentIdentityUtils.setTimeService( - - new AgentIdentityUtils.TimeService() { - - @Override - - public long currentTimeMillis() { - - return currentTime.get(); - - } - - - - @Override - - public void sleep(long millis) { - - currentTime.addAndGet(millis); - - } + @Test + public void refreshAccessToken_agentConfigMissingFile_throws() throws IOException { - }); + // Point config to a non-existent file. - + envProvider.setEnv( + AgentIdentityUtils.GOOGLE_API_CERTIFICATE_CONFIG, + tempDir.resolve("missing_config.json").toAbsolutePath().toString()); - MockMetadataServerTransportFactory transportFactory = + // Use a mock TimeService to avoid actual sleeping and control time flow. - new MockMetadataServerTransportFactory(); + final AtomicLong currentTime = new AtomicLong(0); - transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + AgentIdentityUtils.setTimeService( + new AgentIdentityUtils.TimeService() { - + @Override + public long currentTimeMillis() { - ComputeEngineCredentials credentials = + return currentTime.get(); + } - ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + @Override + public void sleep(long millis) { - + currentTime.addAndGet(millis); + } + }); - IOException e = assertThrows(IOException.class, credentials::refreshAccessToken); + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - assertTrue( + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); - e.getMessage() + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); - .contains("Certificate config or certificate file not found after multiple retries")); + IOException e = assertThrows(IOException.class, credentials::refreshAccessToken); - } + assertTrue( + e.getMessage() + .contains("Certificate config or certificate file not found after multiple retries")); + } private void setupCertConfig(String certResourceName) throws IOException { // Copy cert resource to temp file From b08355818a66a00d4378c4c27e1d3d411e440cc9 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Tue, 2 Dec 2025 12:02:56 -0500 Subject: [PATCH 5/6] Made changes to polling intervals for certificate load. Docstring changes. --- .../auth/oauth2/AgentIdentityUtils.java | 108 +++++++++++++----- .../auth/oauth2/ComputeEngineCredentials.java | 21 ++-- .../oauth2/ComputeEngineCredentialsTest.java | 7 +- 3 files changed, 87 insertions(+), 49 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java b/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java index f30e596ba..476fc3d6e 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java +++ b/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java @@ -48,9 +48,7 @@ import java.security.cert.CertificateFactory; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; @@ -67,15 +65,34 @@ final class AgentIdentityUtils { ImmutableList.of( Pattern.compile("^agents\\.global\\.org-\\d+\\.system\\.id\\.goog$"), Pattern.compile("^agents\\.global\\.proj-\\d+\\.system\\.id\\.goog$")); + private static final int SAN_URI_TYPE = 6; + private static final String SPIFFE_SCHEME_PREFIX = "spiffe://"; // Polling configuration - private static final long TOTAL_TIMEOUT_MS = 30000; // 30 seconds - private static final long FAST_POLL_DURATION_MS = 5000; // 5 seconds + private static final int FAST_POLL_CYCLES = 50; private static final long FAST_POLL_INTERVAL_MS = 100; // 0.1 seconds private static final long SLOW_POLL_INTERVAL_MS = 500; // 0.5 seconds + private static final long TOTAL_TIMEOUT_MS = 30000; // 30 seconds + private static final List POLLING_INTERVALS; - private static final int SAN_URI_TYPE = 6; - private static final String SPIFFE_SCHEME_PREFIX = "spiffe://"; + // Pre-calculates the sequence of polling intervals + static { + List intervals = new ArrayList<>(); + + for (int i = 0; i < FAST_POLL_CYCLES; i++) { + intervals.add(FAST_POLL_INTERVAL_MS); + } + + long remainingTime = TOTAL_TIMEOUT_MS - (FAST_POLL_CYCLES * FAST_POLL_INTERVAL_MS); + // Integer division is sufficient here as we want full cycles + int slowPollCycles = (int) (remainingTime / SLOW_POLL_INTERVAL_MS); + + for (int i = 0; i < slowPollCycles; i++) { + intervals.add(SLOW_POLL_INTERVAL_MS); + } + + POLLING_INTERVALS = Collections.unmodifiableList(intervals); + } // Interface to allow mocking System.getenv for tests without exposing it publicly. interface EnvReader { @@ -111,7 +128,8 @@ public void sleep(long millis) throws InterruptedException { private AgentIdentityUtils() {} /** - * Gets the Agent Identity certificate if available and enabled. + * Gets the Agent Identity certificate if certificate is available and agent token sharing is not + * disabled. * * @return The X509Certificate if found and Agent Identities are enabled, null otherwise. * @throws IOException If there is an error reading the certificate file after retries. @@ -130,18 +148,33 @@ static X509Certificate getAgentIdentityCertificate() throws IOException { return parseCertificate(certPath); } - /** Checks if the user has opted out of Agent Token sharing. */ + /** + * Checks if Agent Identity token sharing is disabled via an environment variable. + * + * @return {@code true} if the {@link #GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES} + * variable is set to {@code "false"}, otherwise returns {@code false}. + */ private static boolean isOptedOut() { String optOut = envReader.getEnv(GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES); return optOut != null && "false".equalsIgnoreCase(optOut); } - /** Polls for the certificate config file and the certificate file it references. */ + /** + * Polls for the certificate config file and the certificate file it references, and returns the + * certificate's path. + * + *

This method will retry for a total of {@link #TOTAL_TIMEOUT_MS} milliseconds before failing. + * + * @param certConfigPath The path to the certificate configuration JSON file. + * @return The path to the certificate file extracted from the config. + * @throws IOException If the files cannot be found after the timeout, or if the thread is + * interrupted while waiting. + */ private static String getCertificatePathWithRetry(String certConfigPath) throws IOException { - long startTime = timeService.currentTimeMillis(); boolean warned = false; - while (true) { + // Deterministic polling loop based on pre-calculated intervals. + for (long sleepInterval : POLLING_INTERVALS) { try { if (Files.exists(Paths.get(certConfigPath))) { String certPath = extractCertPathFromConfig(certConfigPath); @@ -149,20 +182,12 @@ private static String getCertificatePathWithRetry(String certConfigPath) throws return certPath; } } - } catch (Exception e) { - // Ignore exceptions during polling and retry - LOGGER.log(Level.FINE, "Error while polling for certificate files"); - } - - long elapsedTime = timeService.currentTimeMillis() - startTime; - if (elapsedTime >= TOTAL_TIMEOUT_MS) { - throw new IOException( - "Certificate config or certificate file not found after multiple retries. " - + "Token binding protection is failing. You can turn off this protection by setting " - + GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES - + " to false to fall back to unbound tokens."); + } catch (IOException e) { + // Do not log here to prevent noise in the logs per iteration. + // Fall through to the sleep logic to retry. } + // If we are here, we failed to find the certificate, log a warning only once. if (!warned) { LOGGER.warning( String.format( @@ -172,17 +197,32 @@ private static String getCertificatePathWithRetry(String certConfigPath) throws warned = true; } + // Sleep before the next attempt. try { - long sleepTime = - elapsedTime < FAST_POLL_DURATION_MS ? FAST_POLL_INTERVAL_MS : SLOW_POLL_INTERVAL_MS; - timeService.sleep(sleepTime); + timeService.sleep(sleepInterval); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new IOException("Interrupted while waiting for certificate files", e); + throw new IOException( + "Interrupted while waiting for Agent Identity certificate files for bound token request.", + e); } } + + // If the loop completes without returning, we have timed out. + throw new IOException( + "Unable to find Agent Identity certificate config or file for bound token request after multiple retries. " + + "Token binding protection is failing. You can turn off this protection by setting " + + GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES + + " to false to fall back to unbound tokens."); } + /** + * Parses the certificate configuration JSON file and extracts the path to the certificate. + * + * @param certConfigPath The path to the certificate configuration JSON file. + * @return The certificate file path, or {@code null} if not found in the config. + * @throws IOException If the configuration file cannot be read. + */ @SuppressWarnings("unchecked") private static String extractCertPathFromConfig(String certConfigPath) throws IOException { try (InputStream stream = new FileInputStream(certConfigPath)) { @@ -199,12 +239,20 @@ private static String extractCertPathFromConfig(String certConfigPath) throws IO return null; } + /** + * Parses an X.509 certificate from the given file path. + * + * @param certPath The path to the certificate file. + * @return The parsed {@link X509Certificate}. + * @throws IOException If the certificate file cannot be read or parsed. + */ private static X509Certificate parseCertificate(String certPath) throws IOException { try (InputStream stream = new FileInputStream(certPath)) { CertificateFactory cf = CertificateFactory.getInstance("X.509"); return (X509Certificate) cf.generateCertificate(stream); } catch (GeneralSecurityException e) { - throw new IOException("Failed to parse certificate", e); + throw new IOException( + "Failed to parse Agent Identity certificate for bound token request.", e); } } @@ -255,7 +303,7 @@ static String calculateCertificateFingerprint(X509Certificate cert) throws IOExc byte[] digest = md.digest(); return BaseEncoding.base64Url().omitPadding().encode(digest); } catch (GeneralSecurityException e) { - throw new IOException("Failed to calculate certificate fingerprint", e); + throw new IOException("Failed to calculate fingerprint for Agent Identity certificate.", e); } } diff --git a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index f7dd7ddd6..ef6cc0d64 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -347,20 +347,13 @@ private String getUniverseDomainFromMetadata() throws IOException { public AccessToken refreshAccessToken() throws IOException { String tokenUrl = createTokenUrlWithScopes(); - try { - X509Certificate cert = AgentIdentityUtils.getAgentIdentityCertificate(); - if (cert != null && AgentIdentityUtils.shouldRequestBoundToken(cert)) { - String fingerprint = AgentIdentityUtils.calculateCertificateFingerprint(cert); - GenericUrl url = new GenericUrl(tokenUrl); - url.set("bindCertificateFingerprint", fingerprint); - tokenUrl = url.build(); - } - } catch (IOException e) { - LOGGER.log( - Level.WARNING, - "Failed to process Agent Identity certificate for bound token request.", - e); - throw e; + // Checks whether access token has to be bound to certificate for agent identity. + X509Certificate cert = AgentIdentityUtils.getAgentIdentityCertificate(); + if (cert != null && AgentIdentityUtils.shouldRequestBoundToken(cert)) { + String fingerprint = AgentIdentityUtils.calculateCertificateFingerprint(cert); + GenericUrl url = new GenericUrl(tokenUrl); + url.set("bindCertificateFingerprint", fingerprint); + tokenUrl = url.build(); } HttpResponse response = getMetadataResponse(tokenUrl, RequestType.ACCESS_TOKEN_REQUEST, true); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index 7a069f00e..3f88115af 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -1258,13 +1258,11 @@ public void refreshAccessToken_withAgentCert_optedOut_requestsNormalToken() thro public void refreshAccessToken_agentConfigMissingFile_throws() throws IOException { // Point config to a non-existent file. - envProvider.setEnv( AgentIdentityUtils.GOOGLE_API_CERTIFICATE_CONFIG, tempDir.resolve("missing_config.json").toAbsolutePath().toString()); // Use a mock TimeService to avoid actual sleeping and control time flow. - final AtomicLong currentTime = new AtomicLong(0); AgentIdentityUtils.setTimeService( @@ -1284,9 +1282,7 @@ public void sleep(long millis) { }); MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); - transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); - ComputeEngineCredentials credentials = ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); @@ -1294,7 +1290,8 @@ public void sleep(long millis) { assertTrue( e.getMessage() - .contains("Certificate config or certificate file not found after multiple retries")); + .contains( + "Unable to find Agent Identity certificate config or file for bound token request after multiple retries.")); } private void setupCertConfig(String certResourceName) throws IOException { From 5353838db277998879a580b744d1a3e68b4d9724 Mon Sep 17 00:00:00 2001 From: Pranav Iyer Date: Tue, 2 Dec 2025 12:11:13 -0500 Subject: [PATCH 6/6] Test fix. --- .../com/google/auth/oauth2/AgentIdentityUtilsTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java index 4711f8d36..69ed8866b 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AgentIdentityUtilsTest.java @@ -198,7 +198,8 @@ public void getAgentIdentityCertificate_timeout_throwsIOException() { assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertificate); assertTrue( e.getMessage() - .contains("Certificate config or certificate file not found after multiple retries")); + .contains( + "Unable to find Agent Identity certificate config or file for bound token request after multiple retries.")); } // Fake time service that advances time when sleep is requested.