Skip to content

Commit df06bd1

Browse files
authored
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.
1 parent ee92b88 commit df06bd1

File tree

5 files changed

+567
-0
lines changed

5 files changed

+567
-0
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright 2024, Google Inc. All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google Inc. nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
package com.google.auth.oauth2;
32+
33+
import com.google.api.client.http.GenericUrl;
34+
import com.google.api.client.http.HttpBackOffIOExceptionHandler;
35+
import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler;
36+
import com.google.api.client.http.HttpRequest;
37+
import com.google.api.client.http.HttpResponse;
38+
import com.google.api.client.json.JsonObjectParser;
39+
import com.google.api.client.util.ExponentialBackOff;
40+
import com.google.api.client.util.GenericData;
41+
import com.google.auth.http.HttpTransportFactory;
42+
import com.google.common.collect.Iterables;
43+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
44+
import java.io.IOException;
45+
import java.io.InputStream;
46+
import java.util.Arrays;
47+
import java.util.HashSet;
48+
import java.util.ServiceLoader;
49+
import java.util.Set;
50+
import javax.annotation.concurrent.ThreadSafe;
51+
52+
/**
53+
* Utilities to fetch the S2A (Secure Session Agent) address from the mTLS configuration.
54+
*
55+
* <p>mTLS configuration is queried from the MDS MTLS Autoconfiguration endpoint. See
56+
* https://google.aip.dev/auth/4115 for details.
57+
*
58+
* <p>This is an experimental utility.
59+
*/
60+
@ThreadSafe
61+
public final class S2A {
62+
static final String S2A_PLAINTEXT_ADDRESS_JSON_KEY = "plaintext_address";
63+
static final String S2A_MTLS_ADDRESS_JSON_KEY = "mtls_address";
64+
static final String S2A_CONFIG_ENDPOINT_POSTFIX =
65+
"/computeMetadata/v1/instance/platform-security/auto-mtls-configuration";
66+
67+
static final String METADATA_FLAVOR = "Metadata-Flavor";
68+
static final String GOOGLE = "Google";
69+
private static final Set<Integer> RETRYABLE_STATUS_CODES =
70+
new HashSet<>(Arrays.asList(500, 502, 503));
71+
private static final String PARSE_ERROR_S2A = "Error parsing S2A Config from MDS JSON response.";
72+
private static final String MDS_MTLS_ENDPOINT =
73+
ComputeEngineCredentials.getMetadataServerUrl() + S2A_CONFIG_ENDPOINT_POSTFIX;
74+
75+
private S2AConfig config;
76+
77+
private transient HttpTransportFactory transportFactory;
78+
79+
S2A(S2A.Builder builder) {
80+
this.transportFactory = builder.getHttpTransportFactory();
81+
this.config = getS2AConfigFromMDS();
82+
}
83+
84+
/** @return the mTLS S2A Address from the mTLS config. */
85+
public String getMtlsS2AAddress() {
86+
return config.getMtlsAddress();
87+
}
88+
89+
/** @return the plaintext S2A Address from the mTLS config. */
90+
public String getPlaintextS2AAddress() {
91+
return config.getPlaintextAddress();
92+
}
93+
94+
public static Builder newBuilder() {
95+
return new Builder();
96+
}
97+
98+
public static class Builder {
99+
private HttpTransportFactory transportFactory;
100+
101+
protected Builder() {}
102+
103+
@CanIgnoreReturnValue
104+
public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
105+
this.transportFactory = transportFactory;
106+
return this;
107+
}
108+
109+
public HttpTransportFactory getHttpTransportFactory() {
110+
return this.transportFactory;
111+
}
112+
113+
public S2A build() {
114+
return new S2A(this);
115+
}
116+
}
117+
118+
/**
119+
* Queries the MDS mTLS Autoconfiguration endpoint and returns the {@link S2AConfig}.
120+
*
121+
* <p>Returns {@link S2AConfig}. If S2A is not running, or if any error occurs when making the
122+
* request to MDS / processing the response, {@link S2AConfig} will be populated with empty
123+
* addresses.
124+
*
125+
* <p>Users are expected to try to fetch the mTLS-S2A address first (via {@link
126+
* getMtlsS2AAddress}). If it is empty or they have some problem loading the mTLS-MDS credentials,
127+
* they should then fallback to fetching the plaintext-S2A address (via {@link
128+
* getPlaintextS2AAddress}). If the plaintext-S2A address is empty it means that an error occurred
129+
* when talking to the MDS / processing the response or that S2A is not running in the
130+
* environment; in either case this indicates S2A shouldn't be used.
131+
*
132+
* @return the {@link S2AConfig}.
133+
*/
134+
private S2AConfig getS2AConfigFromMDS() {
135+
if (transportFactory == null) {
136+
transportFactory =
137+
Iterables.getFirst(
138+
ServiceLoader.load(HttpTransportFactory.class), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
139+
}
140+
141+
HttpRequest request = null;
142+
GenericUrl genericUrl = new GenericUrl(MDS_MTLS_ENDPOINT);
143+
try {
144+
request = transportFactory.create().createRequestFactory().buildGetRequest(genericUrl);
145+
} catch (IOException ignore) {
146+
/*
147+
* Return empty addresses in {@link S2AConfig} if error building the GET request.
148+
*/
149+
return S2AConfig.createBuilder().build();
150+
}
151+
152+
request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY));
153+
request.getHeaders().set(METADATA_FLAVOR, GOOGLE);
154+
request.setThrowExceptionOnExecuteError(false);
155+
request.setNumberOfRetries(OAuth2Utils.DEFAULT_NUMBER_OF_RETRIES);
156+
157+
ExponentialBackOff backoff =
158+
new ExponentialBackOff.Builder()
159+
.setInitialIntervalMillis(OAuth2Utils.INITIAL_RETRY_INTERVAL_MILLIS)
160+
.setRandomizationFactor(OAuth2Utils.RETRY_RANDOMIZATION_FACTOR)
161+
.setMultiplier(OAuth2Utils.RETRY_MULTIPLIER)
162+
.build();
163+
164+
// Retry on 5xx status codes.
165+
request.setUnsuccessfulResponseHandler(
166+
new HttpBackOffUnsuccessfulResponseHandler(backoff)
167+
.setBackOffRequired(
168+
response -> RETRYABLE_STATUS_CODES.contains(response.getStatusCode())));
169+
request.setIOExceptionHandler(new HttpBackOffIOExceptionHandler(backoff));
170+
171+
GenericData responseData = null;
172+
try {
173+
HttpResponse response = request.execute();
174+
InputStream content = response.getContent();
175+
if (content == null) {
176+
return S2AConfig.createBuilder().build();
177+
}
178+
responseData = response.parseAs(GenericData.class);
179+
} catch (IOException ignore) {
180+
/*
181+
* Return empty addresses in {@link S2AConfig} once all retries have been exhausted.
182+
*/
183+
return S2AConfig.createBuilder().build();
184+
}
185+
186+
String plaintextS2AAddress = "";
187+
String mtlsS2AAddress = "";
188+
try {
189+
plaintextS2AAddress =
190+
OAuth2Utils.validateString(responseData, S2A_PLAINTEXT_ADDRESS_JSON_KEY, PARSE_ERROR_S2A);
191+
} catch (IOException ignore) {
192+
/*
193+
* Do not throw error because of parsing error, just leave the address as empty in {@link S2AConfig}.
194+
*/
195+
}
196+
try {
197+
mtlsS2AAddress =
198+
OAuth2Utils.validateString(responseData, S2A_MTLS_ADDRESS_JSON_KEY, PARSE_ERROR_S2A);
199+
} catch (IOException ignore) {
200+
/*
201+
* Do not throw error because of parsing error, just leave the address as empty in {@link S2AConfig}.
202+
*/
203+
}
204+
205+
return S2AConfig.createBuilder()
206+
.setPlaintextAddress(plaintextS2AAddress)
207+
.setMtlsAddress(mtlsS2AAddress)
208+
.build();
209+
}
210+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2024, Google Inc. All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google Inc. nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
package com.google.auth.oauth2;
32+
33+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
34+
35+
/** Holds an mTLS configuration (consists of address of S2A) retrieved from the Metadata Server. */
36+
final class S2AConfig {
37+
// plaintextAddress is the plaintext address to reach the S2A.
38+
private final String plaintextAddress;
39+
40+
// mtlsAddress is the mTLS address to reach the S2A.
41+
private final String mtlsAddress;
42+
43+
public static Builder createBuilder() {
44+
return new Builder();
45+
}
46+
47+
/** @return the plaintext S2A Address. */
48+
public String getPlaintextAddress() {
49+
return plaintextAddress;
50+
}
51+
52+
/** @return the mTLS S2A Address. */
53+
public String getMtlsAddress() {
54+
return mtlsAddress;
55+
}
56+
57+
public static final class Builder {
58+
// plaintextAddress is the plaintext address to reach the S2A.
59+
private String plaintextAddress;
60+
61+
// mtlsAddress is the mTLS address to reach the S2A.
62+
private String mtlsAddress;
63+
64+
Builder() {
65+
plaintextAddress = "";
66+
mtlsAddress = "";
67+
}
68+
69+
@CanIgnoreReturnValue
70+
public Builder setPlaintextAddress(String plaintextAddress) {
71+
/*
72+
* No validation / format check is necessary here. It is up to the client which consumes this address
73+
* to return error if there is a problem connecting to S2A at that address.
74+
*/
75+
this.plaintextAddress = plaintextAddress;
76+
return this;
77+
}
78+
79+
@CanIgnoreReturnValue
80+
public Builder setMtlsAddress(String mtlsAddress) {
81+
/*
82+
* No validation / format check is necessary here. It is up to the client which consumes this address
83+
* to return error if there is a problem connecting to S2A at that address.
84+
*/
85+
this.mtlsAddress = mtlsAddress;
86+
return this;
87+
}
88+
89+
public S2AConfig build() {
90+
return new S2AConfig(plaintextAddress, mtlsAddress);
91+
}
92+
}
93+
94+
private S2AConfig(String plaintextAddress, String mtlsAddress) {
95+
this.plaintextAddress = plaintextAddress;
96+
this.mtlsAddress = mtlsAddress;
97+
}
98+
}

0 commit comments

Comments
 (0)