From df06bd1f94d03c4f8807c2adf42d25d29b731531 Mon Sep 17 00:00:00 2001 From: Riya Mehta <55350838+rmehta19@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:59:56 -0700 Subject: [PATCH] feat: Support querying S2A Addresses from MDS (#1400) * utils. * formatted. * static mtls config. * update autoconfig endpoint URL. * plaintext and mtls S2A address. * utils. * formatted. * static mtls config. * update autoconfig endpoint URL. * plaintext and mtls S2A address. * Use logic in ComputeEngineCredentials to get MDS URL. * retry MDS request. * rebranch MtlsConfig as S2AConfig. * change naming to S2AConfig elsewhere. * set config in constructor. * make error message more specific. * move creation of transportFactory and parser out of loop. * construct request once. * move declare to loop. * remove unnecessary empty constructor. * Use default retry value. * set config in constructor. * make MDS MTLS autoconfig endpoint a static constant. * make S2AConfig private. * make constants package private. * Use Builder pattern. * Improve javadoc. * Do not retry if autoconfig endpoint doesn't exist. * add comment around catching IOException. * Try and parse each address returned from MDS. * update license dates on added files. * Use Google Java Http client built in retry. * Explain why no format check. * run linter. * move it all into 1 try block. * MockMetadataServerTransport populate content on 200. * MockMetadataServerTransport uses s2aContentMap. * Run mvn fmt:format. * Use ImmutableMap. * update javadoc to reference AIP. * Don't nest try/catch + add some comments about why no throw errors. * update javadoc for each public method. * add experimental note. * format. --- .../java/com/google/auth/oauth2/S2A.java | 210 ++++++++++++++++++ .../com/google/auth/oauth2/S2AConfig.java | 98 ++++++++ .../oauth2/MockMetadataServerTransport.java | 54 +++++ .../com/google/auth/oauth2/S2AConfigTest.java | 63 ++++++ .../com/google/auth/oauth2/S2ATest.java | 142 ++++++++++++ 5 files changed, 567 insertions(+) create mode 100644 oauth2_http/java/com/google/auth/oauth2/S2A.java create mode 100644 oauth2_http/java/com/google/auth/oauth2/S2AConfig.java create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/S2AConfigTest.java create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/S2ATest.java 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 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