Skip to content

Commit fbaa366

Browse files
vvermanlqiu96
andauthored
feat(auth): mTLS endpoint for Regional Access Boundaries (#13318)
Added logic to: 1. Centralize mTLS enablement logic within the auth library. 2. Based on 1. determine whether mtls or regular RAB lookup endpoint should be called. 3. Added tests for the same. --------- Co-authored-by: Lawrence Qiu <lawrenceqiu@google.com>
1 parent fc4464a commit fbaa366

19 files changed

Lines changed: 1012 additions & 183 deletions

google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
package com.google.auth.mtls;
3333

34+
import com.google.api.client.http.HttpTransport;
3435
import com.google.api.client.http.javanet.NetHttpTransport;
3536
import com.google.api.core.InternalApi;
3637
import com.google.auth.http.HttpTransportFactory;
@@ -64,7 +65,7 @@ public MtlsHttpTransportFactory(KeyStore mtlsKeyStore) {
6465
}
6566

6667
@Override
67-
public NetHttpTransport create() {
68+
public HttpTransport create() {
6869
try {
6970
// Build the mTLS transport using the provided KeyStore.
7071
return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build();

google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

Lines changed: 246 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,20 @@
3131
package com.google.auth.mtls;
3232

3333
import com.google.api.core.InternalApi;
34+
import com.google.auth.http.HttpTransportFactory;
3435
import com.google.auth.oauth2.EnvironmentProvider;
36+
import com.google.auth.oauth2.OAuth2Utils;
3537
import com.google.auth.oauth2.PropertyProvider;
3638
import com.google.common.base.Strings;
3739
import java.io.File;
3840
import java.io.FileInputStream;
3941
import java.io.IOException;
4042
import java.io.InputStream;
43+
import java.security.KeyStore;
4144
import java.util.Locale;
45+
import java.util.logging.Logger;
4246
import org.jspecify.annotations.NullMarked;
47+
import org.jspecify.annotations.Nullable;
4348

4449
/**
4550
* Utility class for mTLS related operations.
@@ -49,10 +54,27 @@
4954
@NullMarked
5055
@InternalApi
5156
public class MtlsUtils {
57+
private static final Logger LOGGER = Logger.getLogger(MtlsUtils.class.getName());
58+
5259
static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG";
5360
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
5461
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
5562

63+
/**
64+
* The policy determining when to use mutual TLS (mTLS) endpoints.
65+
*
66+
* <p>See <a href="https://google.aip.dev/auth/4114">AIP-4114</a> for the specification on mTLS
67+
* endpoint usage.
68+
*/
69+
public enum MtlsEndpointUsagePolicy {
70+
/** Always use the mTLS endpoint, and fail if client certificates are not configured. */
71+
ALWAYS,
72+
/** Never use the mTLS endpoint. */
73+
NEVER,
74+
/** Use the mTLS endpoint if client certificates are configured (auto-discovery). */
75+
AUTO
76+
}
77+
5678
private MtlsUtils() {
5779
// Prevent instantiation for Utility class
5880
}
@@ -65,7 +87,9 @@ private MtlsUtils() {
6587
* @throws IOException if the certificate configuration cannot be found or loaded.
6688
*/
6789
public static String getCertificatePath(
68-
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
90+
EnvironmentProvider envProvider,
91+
PropertyProvider propProvider,
92+
@Nullable String certConfigPathOverride)
6993
throws IOException {
7094
String certPath =
7195
getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride)
@@ -78,39 +102,38 @@ public static String getCertificatePath(
78102
}
79103

80104
/**
81-
* Resolves and loads the workload certificate configuration.
105+
* Resolves and parses the workload certificate configuration.
82106
*
83-
* <p>The configuration file is resolved in the following order of precedence: 1. The provided
84-
* certConfigPathOverride (if not null). 2. The path specified by the
85-
* GOOGLE_API_CERTIFICATE_CONFIG environment variable. 3. The well-known certificate configuration
86-
* file in the gcloud config directory.
107+
* <p>This locates the certificate configuration file via {@link #resolveCertificateConfigFile}
108+
* and parses its contents into a {@link WorkloadCertificateConfiguration}.
87109
*
88110
* @param envProvider the environment provider to use for resolving environment variables
89111
* @param propProvider the property provider to use for resolving system properties
90112
* @param certConfigPathOverride optional override path for the configuration file
91113
* @return the loaded WorkloadCertificateConfiguration
92-
* @throws IOException if the configuration file cannot be found, read, or parsed
114+
* @throws IOException if the configuration file cannot be resolved, read, or parsed
93115
*/
94116
static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration(
95-
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
117+
EnvironmentProvider envProvider,
118+
PropertyProvider propProvider,
119+
@Nullable String certConfigPathOverride)
96120
throws IOException {
97-
File certConfig;
98-
if (certConfigPathOverride != null) {
99-
certConfig = new File(certConfigPathOverride);
100-
} else {
101-
String envCredentialsPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
102-
if (!Strings.isNullOrEmpty(envCredentialsPath)) {
103-
certConfig = new File(envCredentialsPath);
104-
} else {
105-
certConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
121+
File certConfig =
122+
resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride);
123+
if (certConfig == null) {
124+
try {
125+
File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
126+
throw new CertificateSourceUnavailableException(
127+
"Certificate configuration file does not exist or is not a file: "
128+
+ wellKnownConfig.getAbsolutePath());
129+
} catch (IOException e) {
130+
if (e instanceof CertificateSourceUnavailableException) {
131+
throw (CertificateSourceUnavailableException) e;
132+
}
133+
throw new CertificateSourceUnavailableException(
134+
"Failed to get well-known certificate config file path", e);
106135
}
107136
}
108-
109-
if (!certConfig.isFile()) {
110-
throw new CertificateSourceUnavailableException(
111-
"Certificate configuration file does not exist or is not a file: "
112-
+ certConfig.getAbsolutePath());
113-
}
114137
try (InputStream certConfigStream = new FileInputStream(certConfig)) {
115138
return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream);
116139
}
@@ -139,4 +162,204 @@ private static File getWellKnownCertificateConfigFile(
139162
}
140163
return new File(cloudConfigPath, WELL_KNOWN_CERTIFICATE_CONFIG_FILE);
141164
}
165+
166+
/**
167+
* Centralized helper method to determine if mutual TLS (mTLS) can be enabled.
168+
*
169+
* @param envProvider the environment provider to use for resolving environment variables
170+
* @param propProvider the property provider to use for resolving system properties
171+
* @param certConfigPathOverride optional override path for the configuration file
172+
* @return true if mTLS should be enabled, false otherwise
173+
* @throws IOException if the configuration file is present but contains missing or malformed
174+
* files
175+
*/
176+
public static boolean canBeEnabled(
177+
EnvironmentProvider envProvider,
178+
PropertyProvider propProvider,
179+
@Nullable String certConfigPathOverride)
180+
throws IOException {
181+
182+
// Check if client certificate usage is allowed
183+
String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
184+
if ("false".equalsIgnoreCase(useClientCertificate)) {
185+
return false;
186+
}
187+
188+
MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider);
189+
if (policy == MtlsEndpointUsagePolicy.NEVER) {
190+
return false;
191+
}
192+
193+
File certConfigFile =
194+
resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride);
195+
if (certConfigFile == null) {
196+
return false;
197+
}
198+
199+
try {
200+
WorkloadCertificateConfiguration config =
201+
getWorkloadCertificateConfiguration(envProvider, propProvider, certConfigPathOverride);
202+
File certFile = new File(config.getCertPath());
203+
File keyFile = new File(config.getPrivateKeyPath());
204+
return certFile.isFile() && keyFile.isFile();
205+
} catch (IOException e) {
206+
return false;
207+
}
208+
}
209+
210+
/**
211+
* Returns whether the mutual TLS (mTLS) endpoint should be used.
212+
*
213+
* @param envProvider the environment provider to use for resolving environment variables
214+
* @param propProvider the property provider to use for resolving system properties
215+
* @param certConfigPathOverride optional override path for the configuration file
216+
* @return true if the mTLS endpoint should be used, false otherwise
217+
*/
218+
public static boolean shouldMtlsEndpointBeUsed(
219+
EnvironmentProvider envProvider,
220+
PropertyProvider propProvider,
221+
@Nullable String certConfigPathOverride) {
222+
MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider);
223+
if (policy == MtlsEndpointUsagePolicy.ALWAYS) {
224+
return true;
225+
}
226+
if (policy == MtlsEndpointUsagePolicy.NEVER) {
227+
return false;
228+
}
229+
// policy is AUTO: use mTLS endpoint if client certificate can be enabled
230+
try {
231+
return canBeEnabled(envProvider, propProvider, certConfigPathOverride);
232+
} catch (IOException e) {
233+
return false;
234+
}
235+
}
236+
237+
/**
238+
* Resolves the mutual TLS (mTLS) certificate configuration file.
239+
*
240+
* <p>The configuration file is resolved in the following order of precedence:
241+
*
242+
* <ol>
243+
* <li>The developer-provided {@code certConfigPathOverride} (if not null).
244+
* <li>The path specified by the {@code GOOGLE_API_CERTIFICATE_CONFIG} environment variable.
245+
* <li>The well-known automatic gcloud workload identity provisioning location (via {@link
246+
* #getWellKnownCertificateConfigFile}).
247+
* </ol>
248+
*
249+
* <p>If an explicit configuration file is specified (via override or environment variable) and it
250+
* is missing or invalid, an exception is thrown. If no explicit file is specified and the default
251+
* well-known file is missing, {@code null} is returned.
252+
*
253+
* @param envProvider the environment provider to use for resolving environment variables
254+
* @param propProvider the property provider to use for resolving system properties
255+
* @param certConfigPathOverride optional override path for the configuration file
256+
* @return the resolved File object, or null if no configuration was found
257+
* @throws IOException if an explicit configuration file is missing or malformed
258+
*/
259+
static @Nullable File resolveCertificateConfigFile(
260+
EnvironmentProvider envProvider,
261+
PropertyProvider propProvider,
262+
@Nullable String certConfigPathOverride)
263+
throws IOException {
264+
// 1. Check explicit developer override
265+
if (certConfigPathOverride != null) {
266+
File certConfigFile = new File(certConfigPathOverride);
267+
if (!certConfigFile.isFile()) {
268+
throw new CertificateSourceUnavailableException(
269+
"Certificate configuration file does not exist or is not a file: "
270+
+ certConfigFile.getAbsolutePath());
271+
}
272+
return certConfigFile;
273+
}
274+
275+
// 2. Check explicit environment variable
276+
String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
277+
if (!Strings.isNullOrEmpty(envPath)) {
278+
File certConfigFile = new File(envPath);
279+
if (!certConfigFile.isFile()) {
280+
throw new CertificateSourceUnavailableException(
281+
"Certificate configuration file does not exist or is not a file: "
282+
+ certConfigFile.getAbsolutePath());
283+
}
284+
return certConfigFile;
285+
}
286+
287+
// 3. Check optional well-known automatic provisioning location
288+
try {
289+
File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
290+
if (wellKnownConfig.isFile()) {
291+
return wellKnownConfig;
292+
}
293+
} catch (IOException e) {
294+
LOGGER.info(
295+
"Could not get the mutual TLS (mTLS) client certificate configuration. The library will fall back to making standard non-mTLS requests.");
296+
}
297+
298+
return null;
299+
}
300+
301+
/**
302+
* Returns the current mutual TLS endpoint usage policy.
303+
*
304+
* @param envProvider the environment provider to use for resolving environment variables
305+
* @return the MtlsEndpointUsagePolicy enum value
306+
*/
307+
public static MtlsEndpointUsagePolicy getMtlsEndpointUsagePolicy(
308+
EnvironmentProvider envProvider) {
309+
String mtlsEndpointUsagePolicy = envProvider.getEnv("GOOGLE_API_USE_MTLS_ENDPOINT");
310+
if ("never".equalsIgnoreCase(mtlsEndpointUsagePolicy)) {
311+
return MtlsEndpointUsagePolicy.NEVER;
312+
} else if ("always".equalsIgnoreCase(mtlsEndpointUsagePolicy)) {
313+
return MtlsEndpointUsagePolicy.ALWAYS;
314+
}
315+
return MtlsEndpointUsagePolicy.AUTO;
316+
}
317+
318+
/**
319+
* Prepares and upgrades the HTTP transport factory for mutual TLS (mTLS) if applicable.
320+
*
321+
* @param baseTransportFactory the base HTTP transport factory to upgrade
322+
* @param envProvider the environment provider to use for resolving environment variables
323+
* @param propProvider the property provider to use for resolving system properties
324+
* @param certConfigPathOverride optional override path for the configuration file
325+
* @return the mTLS-configured HTTP transport factory, or the base factory if mTLS is not enabled
326+
* @throws IOException if mTLS is required/enabled but certificate initialization fails or an
327+
* incompatible transport factory was provided
328+
*/
329+
public static @Nullable HttpTransportFactory prepareTransportFactoryIfMtlsEnabled(
330+
@Nullable HttpTransportFactory baseTransportFactory,
331+
EnvironmentProvider envProvider,
332+
PropertyProvider propProvider,
333+
@Nullable String certConfigPathOverride)
334+
throws IOException {
335+
336+
if (baseTransportFactory == null) {
337+
return null;
338+
}
339+
340+
if (!canBeEnabled(envProvider, propProvider, certConfigPathOverride)) {
341+
return baseTransportFactory;
342+
}
343+
344+
if (baseTransportFactory != OAuth2Utils.HTTP_TRANSPORT_FACTORY) {
345+
// A user configured HttpTransportFactory was explicitly injected.
346+
// Trust the developer's custom factory and return it as-is.
347+
return baseTransportFactory;
348+
}
349+
350+
try {
351+
// This is the default HttpTransportFactory assigned by credentials.
352+
// Automatically discover and load client certificates to construct an mTLS factory.
353+
X509Provider x509Provider =
354+
new X509Provider(envProvider, propProvider, certConfigPathOverride);
355+
KeyStore mtlsKeyStore = x509Provider.getKeyStore();
356+
return new MtlsHttpTransportFactory(mtlsKeyStore);
357+
} catch (Exception e) {
358+
LOGGER.warning(
359+
"mTLS transport factory initialization failed, falling back to non-mTLS transport: "
360+
+ e.getMessage());
361+
// Graceful fallback to standard transport if mTLS initialization fails
362+
return baseTransportFactory;
363+
}
364+
}
142365
}

google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public X509Provider() {
103103
*
104104
* <ul>
105105
* <li>The certificate config override path, if set.
106-
* <li>The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable
106+
* <li>The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable.
107107
* <li>The well known gcloud location for the certificate configuration file.
108108
* </ul>
109109
*
@@ -114,33 +114,27 @@ public X509Provider() {
114114
*/
115115
@Override
116116
public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
117-
WorkloadCertificateConfiguration workloadCertConfig =
118-
MtlsUtils.getWorkloadCertificateConfiguration(
119-
envProvider, propProvider, certConfigPathOverride);
120-
121-
// Read the certificate and private key file paths into streams.
122-
try (InputStream certStream = new FileInputStream(new File(workloadCertConfig.getCertPath()));
123-
InputStream privateKeyStream =
124-
new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
125-
SequenceInputStream certAndPrivateKeyStream =
126-
new SequenceInputStream(certStream, privateKeyStream)) {
117+
try {
118+
// Attempt to load from resolved Config File
119+
WorkloadCertificateConfiguration workloadCertConfig =
120+
MtlsUtils.getWorkloadCertificateConfiguration(
121+
envProvider, propProvider, certConfigPathOverride);
127122

128-
// Build a key store using the combined stream.
129-
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
123+
try (InputStream certStream =
124+
new FileInputStream(new File(workloadCertConfig.getCertPath()));
125+
InputStream privateKeyStream =
126+
new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
127+
SequenceInputStream certAndPrivateKeyStream =
128+
new SequenceInputStream(certStream, privateKeyStream)) {
129+
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
130+
}
130131
} catch (CertificateSourceUnavailableException e) {
131-
// Throw the CertificateSourceUnavailableException without wrapping.
132132
throw e;
133133
} catch (Exception e) {
134-
// Wrap all other exception types to an IOException.
135-
throw new IOException("X509Provider: Unexpected IOException:", e);
134+
throw new IOException("X509Provider: Unexpected error loading from config file:", e);
136135
}
137136
}
138137

139-
/**
140-
* Returns true if the X509 mTLS provider is available.
141-
*
142-
* @throws IOException if a general I/O error occurs while determining availability.
143-
*/
144138
@Override
145139
public boolean isAvailable() throws IOException {
146140
try {

0 commit comments

Comments
 (0)