diff --git a/oauth2_http/java/com/google/auth/oauth2/S2A.java b/oauth2_http/java/com/google/auth/oauth2/S2A.java new file mode 100644 index 000000000..aecf4a3b2 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/S2A.java @@ -0,0 +1,210 @@ +/* + * Copyright 2024, Google Inc. All rights reserved. + * + * 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 Inc. 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.http.GenericUrl; +import com.google.api.client.http.HttpBackOffIOExceptionHandler; +import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.client.util.GenericData; +import com.google.auth.http.HttpTransportFactory; +import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.ServiceLoader; +import java.util.Set; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Utilities to fetch the S2A (Secure Session Agent) address from the mTLS configuration. + * + *

mTLS configuration is queried from the MDS MTLS Autoconfiguration endpoint. See + * https://google.aip.dev/auth/4115 for details. + * + *

This is an experimental utility. + */ +@ThreadSafe +public final class S2A { + static final String S2A_PLAINTEXT_ADDRESS_JSON_KEY = "plaintext_address"; + static final String S2A_MTLS_ADDRESS_JSON_KEY = "mtls_address"; + static final String S2A_CONFIG_ENDPOINT_POSTFIX = + "/computeMetadata/v1/instance/platform-security/auto-mtls-configuration"; + + static final String METADATA_FLAVOR = "Metadata-Flavor"; + static final String GOOGLE = "Google"; + private static final Set RETRYABLE_STATUS_CODES = + new HashSet<>(Arrays.asList(500, 502, 503)); + private static final String PARSE_ERROR_S2A = "Error parsing S2A Config from MDS JSON response."; + private static final String MDS_MTLS_ENDPOINT = + ComputeEngineCredentials.getMetadataServerUrl() + S2A_CONFIG_ENDPOINT_POSTFIX; + + private S2AConfig config; + + private transient HttpTransportFactory transportFactory; + + S2A(S2A.Builder builder) { + this.transportFactory = builder.getHttpTransportFactory(); + this.config = getS2AConfigFromMDS(); + } + + /** @return the mTLS S2A Address from the mTLS config. */ + public String getMtlsS2AAddress() { + return config.getMtlsAddress(); + } + + /** @return the plaintext S2A Address from the mTLS config. */ + public String getPlaintextS2AAddress() { + return config.getPlaintextAddress(); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private HttpTransportFactory transportFactory; + + protected Builder() {} + + @CanIgnoreReturnValue + public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { + this.transportFactory = transportFactory; + return this; + } + + public HttpTransportFactory getHttpTransportFactory() { + return this.transportFactory; + } + + public S2A build() { + return new S2A(this); + } + } + + /** + * Queries the MDS mTLS Autoconfiguration endpoint and returns the {@link S2AConfig}. + * + *

Returns {@link S2AConfig}. If S2A is not running, or if any error occurs when making the + * request to MDS / processing the response, {@link S2AConfig} will be populated with empty + * addresses. + * + *

Users are expected to try to fetch the mTLS-S2A address first (via {@link + * getMtlsS2AAddress}). If it is empty or they have some problem loading the mTLS-MDS credentials, + * they should then fallback to fetching the plaintext-S2A address (via {@link + * getPlaintextS2AAddress}). If the plaintext-S2A address is empty it means that an error occurred + * when talking to the MDS / processing the response or that S2A is not running in the + * environment; in either case this indicates S2A shouldn't be used. + * + * @return the {@link S2AConfig}. + */ + private S2AConfig getS2AConfigFromMDS() { + if (transportFactory == null) { + transportFactory = + Iterables.getFirst( + ServiceLoader.load(HttpTransportFactory.class), OAuth2Utils.HTTP_TRANSPORT_FACTORY); + } + + HttpRequest request = null; + GenericUrl genericUrl = new GenericUrl(MDS_MTLS_ENDPOINT); + try { + request = transportFactory.create().createRequestFactory().buildGetRequest(genericUrl); + } catch (IOException ignore) { + /* + * Return empty addresses in {@link S2AConfig} if error building the GET request. + */ + return S2AConfig.createBuilder().build(); + } + + request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY)); + request.getHeaders().set(METADATA_FLAVOR, GOOGLE); + request.setThrowExceptionOnExecuteError(false); + request.setNumberOfRetries(OAuth2Utils.DEFAULT_NUMBER_OF_RETRIES); + + ExponentialBackOff backoff = + new ExponentialBackOff.Builder() + .setInitialIntervalMillis(OAuth2Utils.INITIAL_RETRY_INTERVAL_MILLIS) + .setRandomizationFactor(OAuth2Utils.RETRY_RANDOMIZATION_FACTOR) + .setMultiplier(OAuth2Utils.RETRY_MULTIPLIER) + .build(); + + // Retry on 5xx status codes. + request.setUnsuccessfulResponseHandler( + new HttpBackOffUnsuccessfulResponseHandler(backoff) + .setBackOffRequired( + response -> RETRYABLE_STATUS_CODES.contains(response.getStatusCode()))); + request.setIOExceptionHandler(new HttpBackOffIOExceptionHandler(backoff)); + + GenericData responseData = null; + try { + HttpResponse response = request.execute(); + InputStream content = response.getContent(); + if (content == null) { + return S2AConfig.createBuilder().build(); + } + responseData = response.parseAs(GenericData.class); + } catch (IOException ignore) { + /* + * Return empty addresses in {@link S2AConfig} once all retries have been exhausted. + */ + return S2AConfig.createBuilder().build(); + } + + String plaintextS2AAddress = ""; + String mtlsS2AAddress = ""; + try { + plaintextS2AAddress = + OAuth2Utils.validateString(responseData, S2A_PLAINTEXT_ADDRESS_JSON_KEY, PARSE_ERROR_S2A); + } catch (IOException ignore) { + /* + * Do not throw error because of parsing error, just leave the address as empty in {@link S2AConfig}. + */ + } + try { + mtlsS2AAddress = + OAuth2Utils.validateString(responseData, S2A_MTLS_ADDRESS_JSON_KEY, PARSE_ERROR_S2A); + } catch (IOException ignore) { + /* + * Do not throw error because of parsing error, just leave the address as empty in {@link S2AConfig}. + */ + } + + return S2AConfig.createBuilder() + .setPlaintextAddress(plaintextS2AAddress) + .setMtlsAddress(mtlsS2AAddress) + .build(); + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/S2AConfig.java b/oauth2_http/java/com/google/auth/oauth2/S2AConfig.java new file mode 100644 index 000000000..90765f096 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/S2AConfig.java @@ -0,0 +1,98 @@ +/* + * Copyright 2024, Google Inc. All rights reserved. + * + * 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 Inc. 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.errorprone.annotations.CanIgnoreReturnValue; + +/** Holds an mTLS configuration (consists of address of S2A) retrieved from the Metadata Server. */ +final class S2AConfig { + // plaintextAddress is the plaintext address to reach the S2A. + private final String plaintextAddress; + + // mtlsAddress is the mTLS address to reach the S2A. + private final String mtlsAddress; + + public static Builder createBuilder() { + return new Builder(); + } + + /** @return the plaintext S2A Address. */ + public String getPlaintextAddress() { + return plaintextAddress; + } + + /** @return the mTLS S2A Address. */ + public String getMtlsAddress() { + return mtlsAddress; + } + + public static final class Builder { + // plaintextAddress is the plaintext address to reach the S2A. + private String plaintextAddress; + + // mtlsAddress is the mTLS address to reach the S2A. + private String mtlsAddress; + + Builder() { + plaintextAddress = ""; + mtlsAddress = ""; + } + + @CanIgnoreReturnValue + public Builder setPlaintextAddress(String plaintextAddress) { + /* + * No validation / format check is necessary here. It is up to the client which consumes this address + * to return error if there is a problem connecting to S2A at that address. + */ + this.plaintextAddress = plaintextAddress; + return this; + } + + @CanIgnoreReturnValue + public Builder setMtlsAddress(String mtlsAddress) { + /* + * No validation / format check is necessary here. It is up to the client which consumes this address + * to return error if there is a problem connecting to S2A at that address. + */ + this.mtlsAddress = mtlsAddress; + return this; + } + + public S2AConfig build() { + return new S2AConfig(plaintextAddress, mtlsAddress); + } + } + + private S2AConfig(String plaintextAddress, String mtlsAddress) { + this.plaintextAddress = plaintextAddress; + this.mtlsAddress = mtlsAddress; + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java index d21491027..5a854480c 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java @@ -39,6 +39,7 @@ import com.google.api.client.testing.http.MockLowLevelHttpRequest; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -62,6 +63,9 @@ public class MockMetadataServerTransport extends MockHttpTransport { private byte[] signature; + private Map s2aContentMap = new HashMap<>(); + + private boolean emptyContent; private MockLowLevelHttpRequest request; public MockMetadataServerTransport() {} @@ -103,6 +107,14 @@ public void setIdToken(String idToken) { this.idToken = idToken; } + public void setS2AContentMap(ImmutableMap map) { + this.s2aContentMap = map; + } + + public void setEmptyContent(boolean emptyContent) { + this.emptyContent = emptyContent; + } + public MockLowLevelHttpRequest getRequest() { return request; } @@ -121,6 +133,8 @@ public LowLevelHttpRequest buildRequest(String method, String url) throws IOExce } else if (isIdentityDocumentUrl(url)) { this.request = getMockRequestForIdentityDocument(url); return this.request; + } else if (isMtlsConfigRequestUrl(url)) { + return getMockRequestForMtlsConfig(url); } this.request = new MockLowLevelHttpRequest(url) { @@ -272,6 +286,40 @@ public LowLevelHttpResponse execute() throws IOException { }; } + private MockLowLevelHttpRequest getMockRequestForMtlsConfig(String url) { + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() throws IOException { + + String metadataRequestHeader = getFirstHeaderValue(S2A.METADATA_FLAVOR); + if (!S2A.GOOGLE.equals(metadataRequestHeader)) { + throw new IOException("Metadata request header not found"); + } + + // Create the JSON response + GenericJson content = new GenericJson(); + content.setFactory(OAuth2Utils.JSON_FACTORY); + if (requestStatusCode == 200) { + for (Map.Entry entrySet : s2aContentMap.entrySet()) { + content.put(entrySet.getKey(), entrySet.getValue()); + } + } + String contentText = content.toPrettyString(); + + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + + if (requestStatusCode != null) { + response.setStatusCode(requestStatusCode); + } + if (emptyContent == true) { + return response.setZeroContent(); + } + response.setContentType(Json.MEDIA_TYPE).setContent(contentText); + return response; + } + }; + } + protected boolean isGetServiceAccountsUrl(String url) { return url.equals(ComputeEngineCredentials.getServiceAccountsUrl()); } @@ -285,4 +333,10 @@ protected boolean isSignRequestUrl(String url) { protected boolean isIdentityDocumentUrl(String url) { return url.startsWith(String.format(ComputeEngineCredentials.getIdentityDocumentUrl())); } + + protected boolean isMtlsConfigRequestUrl(String url) { + return url.equals( + String.format( + ComputeEngineCredentials.getMetadataServerUrl() + S2A.S2A_CONFIG_ENDPOINT_POSTFIX)); + } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/S2AConfigTest.java b/oauth2_http/javatests/com/google/auth/oauth2/S2AConfigTest.java new file mode 100644 index 000000000..cd8cca899 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/S2AConfigTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024, Google Inc. All rights reserved. + * + * 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 Inc. 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test cases for {@link S2AConfig}. */ +@RunWith(JUnit4.class) +public class S2AConfigTest { + private static final String S2A_PLAINTEXT_ADDRESS = "plaintext"; + private static final String S2A_MTLS_ADDRESS = "mtls"; + + @Test + public void createS2AConfig_success() { + S2AConfig config = + S2AConfig.createBuilder() + .setPlaintextAddress(S2A_PLAINTEXT_ADDRESS) + .setMtlsAddress(S2A_MTLS_ADDRESS) + .build(); + assertEquals(S2A_PLAINTEXT_ADDRESS, config.getPlaintextAddress()); + assertEquals(S2A_MTLS_ADDRESS, config.getMtlsAddress()); + } + + @Test + public void createEmptyS2AConfig_success() { + S2AConfig config = S2AConfig.createBuilder().build(); + assertTrue(config.getPlaintextAddress().isEmpty()); + assertTrue(config.getMtlsAddress().isEmpty()); + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/S2ATest.java b/oauth2_http/javatests/com/google/auth/oauth2/S2ATest.java new file mode 100644 index 000000000..f532a2727 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/S2ATest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2024, Google Inc. All rights reserved. + * + * 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 Inc. 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.http.HttpStatusCodes; +import com.google.auth.oauth2.ComputeEngineCredentialsTest.MockMetadataServerTransportFactory; +import com.google.common.collect.ImmutableMap; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test cases for {@link S2A}. */ +@RunWith(JUnit4.class) +public class S2ATest { + + private static final String INVALID_JSON_KEY = "invalid_key"; + private static final String S2A_PLAINTEXT_ADDRESS = "plaintext"; + private static final String S2A_MTLS_ADDRESS = "mtls"; + + @Test + public void getS2AAddress_validAddress() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setS2AContentMap( + ImmutableMap.of( + S2A.S2A_PLAINTEXT_ADDRESS_JSON_KEY, + S2A_PLAINTEXT_ADDRESS, + S2A.S2A_MTLS_ADDRESS_JSON_KEY, + S2A_MTLS_ADDRESS)); + transportFactory.transport.setRequestStatusCode(HttpStatusCodes.STATUS_CODE_OK); + + S2A s2aUtils = S2A.newBuilder().setHttpTransportFactory(transportFactory).build(); + String plaintextS2AAddress = s2aUtils.getPlaintextS2AAddress(); + String mtlsS2AAddress = s2aUtils.getMtlsS2AAddress(); + assertEquals(S2A_PLAINTEXT_ADDRESS, plaintextS2AAddress); + assertEquals(S2A_MTLS_ADDRESS, mtlsS2AAddress); + } + + @Test + public void getS2AAddress_queryEndpointResponseErrorCode_emptyAddress() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setS2AContentMap( + ImmutableMap.of( + S2A.S2A_PLAINTEXT_ADDRESS_JSON_KEY, + S2A_PLAINTEXT_ADDRESS, + S2A.S2A_MTLS_ADDRESS_JSON_KEY, + S2A_MTLS_ADDRESS)); + transportFactory.transport.setRequestStatusCode( + HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE); + + S2A s2aUtils = S2A.newBuilder().setHttpTransportFactory(transportFactory).build(); + String plaintextS2AAddress = s2aUtils.getPlaintextS2AAddress(); + String mtlsS2AAddress = s2aUtils.getMtlsS2AAddress(); + assertTrue(plaintextS2AAddress.isEmpty()); + assertTrue(mtlsS2AAddress.isEmpty()); + } + + @Test + public void getS2AAddress_queryEndpointResponseEmpty_emptyAddress() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setS2AContentMap( + ImmutableMap.of( + S2A.S2A_PLAINTEXT_ADDRESS_JSON_KEY, + S2A_PLAINTEXT_ADDRESS, + S2A.S2A_MTLS_ADDRESS_JSON_KEY, + S2A_MTLS_ADDRESS)); + transportFactory.transport.setRequestStatusCode(HttpStatusCodes.STATUS_CODE_OK); + transportFactory.transport.setEmptyContent(true); + + S2A s2aUtils = S2A.newBuilder().setHttpTransportFactory(transportFactory).build(); + String plaintextS2AAddress = s2aUtils.getPlaintextS2AAddress(); + String mtlsS2AAddress = s2aUtils.getMtlsS2AAddress(); + assertTrue(plaintextS2AAddress.isEmpty()); + assertTrue(mtlsS2AAddress.isEmpty()); + } + + @Test + public void getS2AAddress_queryEndpointResponseInvalidPlaintextJsonKey_plaintextEmptyAddress() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setS2AContentMap( + ImmutableMap.of( + INVALID_JSON_KEY, + S2A_PLAINTEXT_ADDRESS, + S2A.S2A_MTLS_ADDRESS_JSON_KEY, + S2A_MTLS_ADDRESS)); + transportFactory.transport.setRequestStatusCode(HttpStatusCodes.STATUS_CODE_OK); + + S2A s2aUtils = S2A.newBuilder().setHttpTransportFactory(transportFactory).build(); + String plaintextS2AAddress = s2aUtils.getPlaintextS2AAddress(); + String mtlsS2AAddress = s2aUtils.getMtlsS2AAddress(); + assertTrue(plaintextS2AAddress.isEmpty()); + assertEquals(S2A_MTLS_ADDRESS, mtlsS2AAddress); + } + + @Test + public void getS2AAddress_queryEndpointResponseInvalidMtlsJsonKey_mtlsEmptyAddress() { + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setS2AContentMap( + ImmutableMap.of( + S2A.S2A_PLAINTEXT_ADDRESS_JSON_KEY, + S2A_PLAINTEXT_ADDRESS, + INVALID_JSON_KEY, + S2A_MTLS_ADDRESS)); + transportFactory.transport.setRequestStatusCode(HttpStatusCodes.STATUS_CODE_OK); + + S2A s2aUtils = S2A.newBuilder().setHttpTransportFactory(transportFactory).build(); + String plaintextS2AAddress = s2aUtils.getPlaintextS2AAddress(); + String mtlsS2AAddress = s2aUtils.getMtlsS2AAddress(); + assertEquals(S2A_PLAINTEXT_ADDRESS, plaintextS2AAddress); + assertTrue(mtlsS2AAddress.isEmpty()); + } +}