Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support querying S2A Addresses from MDS #1400

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c96cb4a
utils.
rmehta19 Feb 23, 2024
f90be0b
formatted.
rmehta19 Feb 29, 2024
0c64a0a
static mtls config.
rmehta19 Mar 12, 2024
993663d
update autoconfig endpoint URL.
rmehta19 Mar 13, 2024
0f96e86
plaintext and mtls S2A address.
rmehta19 Mar 13, 2024
3d68cef
utils.
rmehta19 Feb 23, 2024
6d75a4e
formatted.
rmehta19 Feb 29, 2024
d932e0c
static mtls config.
rmehta19 Mar 12, 2024
6aa071b
update autoconfig endpoint URL.
rmehta19 Mar 13, 2024
ddac7aa
plaintext and mtls S2A address.
rmehta19 Mar 13, 2024
2c26736
Merge remote-tracking branch 'refs/remotes/origin/s2a-java-integratio…
rmehta19 May 17, 2024
67f9462
Use logic in ComputeEngineCredentials to get MDS URL.
rmehta19 May 17, 2024
36d4cd1
retry MDS request.
rmehta19 May 17, 2024
fc2b246
Merge branch 'main' into s2a-java-integration
rmehta19 Sep 26, 2024
359fd43
rebranch MtlsConfig as S2AConfig.
rmehta19 Sep 26, 2024
bce602e
change naming to S2AConfig elsewhere.
rmehta19 Sep 26, 2024
32caef5
set config in constructor.
rmehta19 Sep 26, 2024
b82790a
make error message more specific.
rmehta19 Sep 26, 2024
05aa9cc
move creation of transportFactory and parser out of loop.
rmehta19 Sep 26, 2024
1466f0d
construct request once.
rmehta19 Oct 2, 2024
be1cfd2
move declare to loop.
rmehta19 Oct 2, 2024
c89b56c
resolve merge conflict + add licenses.
rmehta19 Oct 23, 2024
544d9d1
remove unnecessary empty constructor.
rmehta19 Oct 23, 2024
c3ede1d
Use default retry value.
rmehta19 Oct 23, 2024
8238d50
set config in constructor.
rmehta19 Oct 23, 2024
36ab0a9
make MDS MTLS autoconfig endpoint a static constant.
rmehta19 Oct 23, 2024
36a0ac7
make S2AConfig private.
rmehta19 Oct 23, 2024
ae545c8
make constants package private.
rmehta19 Oct 24, 2024
47b3f2e
Use Builder pattern.
rmehta19 Oct 24, 2024
fb577a1
Improve javadoc.
rmehta19 Oct 24, 2024
1f333b4
Do not retry if autoconfig endpoint doesn't exist.
rmehta19 Oct 24, 2024
0bbd320
add comment around catching IOException.
rmehta19 Oct 24, 2024
0e6f5ce
Try and parse each address returned from MDS.
rmehta19 Oct 24, 2024
e786886
update license dates on added files.
rmehta19 Oct 24, 2024
12b248d
Use Google Java Http client built in retry.
rmehta19 Oct 25, 2024
4d05638
Explain why no format check.
rmehta19 Oct 25, 2024
7447f0b
run linter.
rmehta19 Oct 25, 2024
ed681f5
move it all into 1 try block.
rmehta19 Oct 25, 2024
20825f7
MockMetadataServerTransport populate content on 200.
rmehta19 Oct 25, 2024
594df7b
MockMetadataServerTransport uses s2aContentMap.
rmehta19 Oct 25, 2024
16fd964
Run mvn fmt:format.
rmehta19 Oct 25, 2024
1e6c058
Use ImmutableMap.
rmehta19 Oct 25, 2024
934679c
update javadoc to reference AIP.
rmehta19 Oct 28, 2024
257ed12
Merge branch 'main' into s2a-java-integration
rmehta19 Oct 28, 2024
0e1631a
Merge branch 'main' into s2a-java-integration
rmehta19 Oct 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/S2A.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.google.auth.oauth2;
zhumin8 marked this conversation as resolved.
Show resolved Hide resolved

import com.google.api.client.http.GenericUrl;
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.GenericData;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.io.InputStream;
import java.util.ServiceLoader;
import javax.annotation.concurrent.ThreadSafe;

/**
* Utilities to fetch the S2A (Secure Session Agent) address from the mTLS configuration.
*
* <p>mTLS configuration is queried from the MDS MTLS Autoconfiguration endpoint.
*/
@ThreadSafe
public final class S2A {
public static final String S2A_CONFIG_ENDPOINT_POSTFIX =
"/computeMetadata/v1/instance/platform-security/auto-mtls-configuration";

public static final String METADATA_FLAVOR = "Metadata-Flavor";
public static final String GOOGLE = "Google";
lqiu96 marked this conversation as resolved.
Show resolved Hide resolved
private static final int MAX_MDS_PING_TRIES = 3;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the default retry value here:

static final int DEFAULT_NUMBER_OF_RETRIES = 3;
?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c3ede1d

private static final String PARSE_ERROR_S2A = "Error parsing S2A Config from MDS JSON response.";

private S2AConfig config;

private transient HttpTransportFactory transportFactory;

public S2A() {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This empty constructor seems a bit unnecessary as Java compiler provides it as default? Is it added to show this class can be instantiated without arguments?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is why the constructor was included, but thank you for pointing that out Min, I've removed it in 544d9d1


public void setHttpTransportFactory(HttpTransportFactory tf) {
this.transportFactory = tf;
this.config = getS2AConfigFromMDS();
}
lqiu96 marked this conversation as resolved.
Show resolved Hide resolved

/** @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();
}

/**
* Queries the MDS mTLS Autoconfiguration endpoint and returns the {@link S2AConfig}.
*
* <p>Returns {@link S2AConfig} with empty addresses on error.
*
* @return the {@link S2AConfig}.
Copy link
Contributor

@lqiu96 lqiu96 Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a more descriptive javadocs that details when empty addresses occur and what users are expected to do with it? I know it's a private method, but I think it would be helpful for future reference, especially given the information detailed in this comment: https://github.com/googleapis/google-auth-library-java/pull/1400/files#r1785153627

Also, what if only plaintextAddress is empty or mtsAddress is empty? Is that different when both are empty?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in fb577a1

Also, what if only plaintextAddress is empty or mtsAddress is empty? Is that different when both are empty?

The behavior we will add to the Client libraries (specifically, we are adding logic to decide to create channel using S2A in GAX layer) is:
try to fetch mtlsS2AAdress first (call getMtlsS2AAddress(). if that is empty or if there is a failure when trying to load the mTLS-to-MDS credentials, then fallback to fetch plaintextS2AAddress (call getPlaintextS2AAddress()) . If the plaintextS2AAdress is also empty, that means there is some error when trying to talk the MDS, or S2A is not available in that environment, thus we should not try and use S2A.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plaintextS2AAddress =
            OAuth2Utils.validateString(responseData, "plaintext_address", PARSE_ERROR_S2A);
        mtlsS2AAddress = OAuth2Utils.validateString(responseData, "mtls_address", PARSE_ERROR_S2A);
      } catch (IOException e) {
...
      }

If the response data is something like:

{
   plaintext_address: null,
   mtls_address: "localhost",
}

then I believe an IOException would be thrown and mtls_address wouldn't be set even though it's valid. Just want confirm the behavior.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this behavior could happen where an IOException is thrown and mtls_address is not set even though it is able to be parsed (I don't think it's too likely since we own the MDS mTLS autoconfiguration endpoint, and the json format is tested).

In 0e6f5ce I've adjusted the code so that it catches and ignores any exception thrown when parsing the addresses. So in this case, if for whatever reason, parsing the plaintext_address address fails, we still try and parse the mtls_address address. I chatted with @xmenxk, and we agree that in this case we shouldn't really retry since if we get a successful response from the MDS, it is unlikely to change if we query it again.

*/
private S2AConfig getS2AConfigFromMDS() {
String url = getMdsMtlsEndpoint();
GenericUrl genericUrl = new GenericUrl(url);
JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
if (transportFactory == null) {
transportFactory =
Iterables.getFirst(
ServiceLoader.load(HttpTransportFactory.class), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
}

HttpRequest request;
try {
request =
transportFactory.create().createRequestFactory().buildGetRequest(genericUrl);
request.setParser(parser);
request.getHeaders().set(METADATA_FLAVOR, GOOGLE);
request.setThrowExceptionOnExecuteError(false);
} catch (IOException e) {
return S2AConfig.createBuilder().build();
}

for (int i = 0; i < MAX_MDS_PING_TRIES; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not super familiar with google-http-client, but can this retry logic be handled by maybe HttpRequestRetryHandler?

ExponentialBackOff used in retrys here is not thread-safe.
cc.@lqiu96 who might be more familiar

Copy link
Author

@rmehta19 rmehta19 Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out Min! A few followup questions:

Apologies if I'm misunderstanding something here!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only mentioned ExponentialBackOff because that is used in one of the recent changes related to retry (https://github.com/googleapis/google-auth-library-java/pull/1452/files)
I suppose you can do similar, by using HttpRequest's built-in retry handlers and set request.setNumberOfRetries(OAuth2Utils.DEFAULT_NUMBER_OF_RETRIES);? If you do not need ExponentialBackOff, maybe something simple e.g.

      // Set the number of retries
      request.setNumberOfRetries(OAuth2Utils.DEFAULT_NUMBER_OF_RETRIES); 
      // Retry on specific status codes (you might want to adjust these)
      request.setUnsuccessfulResponseHandler(
          new HttpUnsuccessfulResponseHandler() {
            @Override
            public boolean handleResponse(
                HttpRequest request, HttpResponse response, boolean supportsRetry)
                throws IOException   {
              return   RETRYABLE_STATUS_CODES.contains(response.getStatusCode());
            }
          });

About thread-safety, noticed HttpRequest is also marked as not safe here, likely because its member variable hold state information . But I don't see it a concern in this implementation, as executeAsync() is not used and getS2AConfigFromMDS() creates and executes a request each time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree and I would prefer if the loops could be modified to use the unsuccessfulResponseHandler and exponential backoff (configured with a list of status codes that can be retried on).

Copy link
Author

@rmehta19 rmehta19 Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detail @zhumin8 ! In 12b248d I have modified to set the number of retries on the request and use ExponentialBackOff and HttpUnsuccessfulResponseHandler. We only want to be retrying on 5xx error codes (503 is the expected one, but I listed all 5xx part of the HttpStatusCodes package), so I have specified them as retryable error codes.

String plaintextS2AAddress = "";
String mtlsS2AAddress = "";
try {
HttpResponse response = request.execute();
if (!response.isSuccessStatusCode()) {
continue;
}
InputStream content = response.getContent();
if (content == null) {
continue;
}
GenericData responseData = response.parseAs(GenericData.class);
plaintextS2AAddress =
OAuth2Utils.validateString(responseData, "plaintext_address", PARSE_ERROR_S2A);
mtlsS2AAddress = OAuth2Utils.validateString(responseData, "mtls_address", PARSE_ERROR_S2A);
} catch (IOException e) {
continue;
}
lqiu96 marked this conversation as resolved.
Show resolved Hide resolved
return S2AConfig.createBuilder()
.setPlaintextAddress(plaintextS2AAddress)
.setMtlsAddress(mtlsS2AAddress)
.build();
}
return S2AConfig.createBuilder().build();
rmehta19 marked this conversation as resolved.
Show resolved Hide resolved
}

/** @return MDS mTLS autoconfig endpoint. */
private String getMdsMtlsEndpoint() {
return ComputeEngineCredentials.getMetadataServerUrl() + S2A_CONFIG_ENDPOINT_POSTFIX;
}
lqiu96 marked this conversation as resolved.
Show resolved Hide resolved
}
58 changes: 58 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/S2AConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.google.auth.oauth2;
lqiu96 marked this conversation as resolved.
Show resolved Hide resolved

import com.google.errorprone.annotations.CanIgnoreReturnValue;

/** Holds an mTLS configuration (consists of address of S2A) retrieved from the Metadata Server. */
public final class S2AConfig {
lqiu96 marked this conversation as resolved.
Show resolved Hide resolved
// 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();
}

public String getPlaintextAddress() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like Javadocs for all these public things, pointing to public docs for MDS.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have this documented in public MDS docs (e.g. https://cloud.google.com/compute/docs/metadata/predefined-metadata-keys). We do have an AIP: https://google.aip.dev/auth/4115 which discusses this autconfig endpoint and how it fits in the mTLS via S2A + bound tokens story. WDYT about 934679c?

return plaintextAddress;
}

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) {
this.plaintextAddress = plaintextAddress;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does these addresses need any validation or format check when setting with builder?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No validation/format check is necessary here, because we own the MDS endpoint that is being queried to get the address, and it is up to the client which consumes the address (S2A client) to return error if there is a problem connecting to the S2A at that address.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this explanation as javadoc comment here or to getS2AConfigFromMDS() for future references?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4d05638

return this;
}

@CanIgnoreReturnValue
public Builder setMtlsAddress(String mtlsAddress) {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ public class MockMetadataServerTransport extends MockHttpTransport {

private byte[] signature;

private String plaintextS2AAddress;

private String mtlsS2AAddress;

private boolean emptyContent;

public MockMetadataServerTransport() {}

public MockMetadataServerTransport(String accessToken) {
Expand Down Expand Up @@ -101,6 +107,18 @@ public void setIdToken(String idToken) {
this.idToken = idToken;
}

public void setPlaintextS2AAddress(String address) {
this.plaintextS2AAddress = address;
}

public void setMtlsS2AAddress(String address) {
this.mtlsS2AAddress = address;
}

public void setEmptyContent(boolean emptyContent) {
this.emptyContent = emptyContent;
}

@Override
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
if (url.startsWith(ComputeEngineCredentials.getTokenServerEncodedUrl())) {
Expand All @@ -111,6 +129,8 @@ public LowLevelHttpRequest buildRequest(String method, String url) throws IOExce
return getMockRequestForSign(url);
} else if (isIdentityDocumentUrl(url)) {
return getMockRequestForIdentityDocument(url);
} else if (isMtlsConfigRequestUrl(url)) {
return getMockRequestForMtlsConfig(url);
}
return new MockLowLevelHttpRequest(url) {
@Override
Expand Down Expand Up @@ -260,6 +280,37 @@ 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);
content.put("plaintext_address", plaintextS2AAddress);
content.put("mtls_address", mtlsS2AAddress);
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());
}
Expand All @@ -273,4 +324,10 @@ protected boolean isSignRequestUrl(String url) {
protected boolean isIdentityDocumentUrl(String url) {
return url.startsWith(String.format(ComputeEngineCredentials.getIdentityDocumentUrl()));
}

protected boolean isMtlsConfigRequestUrl(String url) {
return plaintextS2AAddress != null
&& mtlsS2AAddress != null
&& url.equals(String.format(ComputeEngineCredentials.getMetadataServerUrl() + S2A.S2A_CONFIG_ENDPOINT_POSTFIX));
}
}
33 changes: 33 additions & 0 deletions oauth2_http/javatests/com/google/auth/oauth2/S2AConfigTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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());
}
}
65 changes: 65 additions & 0 deletions oauth2_http/javatests/com/google/auth/oauth2/S2ATest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 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 S2A_PLAINTEXT_ADDRESS = "plaintext";
private static final String S2A_MTLS_ADDRESS = "mtls";

@Test
public void getS2AAddress_validAddress() {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
transportFactory.transport.setPlaintextS2AAddress(S2A_PLAINTEXT_ADDRESS);
transportFactory.transport.setMtlsS2AAddress(S2A_MTLS_ADDRESS);
transportFactory.transport.setRequestStatusCode(HttpStatusCodes.STATUS_CODE_OK);

S2A s2aUtils = new S2A();
s2aUtils.setHttpTransportFactory(transportFactory);
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.setPlaintextS2AAddress(S2A_PLAINTEXT_ADDRESS);
transportFactory.transport.setMtlsS2AAddress(S2A_MTLS_ADDRESS);
transportFactory.transport.setRequestStatusCode(
HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE);

S2A s2aUtils = new S2A();
s2aUtils.setHttpTransportFactory(transportFactory);
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.setPlaintextS2AAddress(S2A_PLAINTEXT_ADDRESS);
transportFactory.transport.setMtlsS2AAddress(S2A_MTLS_ADDRESS);
transportFactory.transport.setRequestStatusCode(HttpStatusCodes.STATUS_CODE_OK);
transportFactory.transport.setEmptyContent(true);

S2A s2aUtils = new S2A();
s2aUtils.setHttpTransportFactory(transportFactory);
String plaintextS2AAddress = s2aUtils.getPlaintextS2AAddress();
String mtlsS2AAddress = s2aUtils.getMtlsS2AAddress();
assertTrue(plaintextS2AAddress.isEmpty());
assertTrue(mtlsS2AAddress.isEmpty());
}
}