Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
88d72e6
Added changes to enable cert-discovery and mtls by default in the aut…
vverman May 31, 2026
9744fbc
Added changes for RAB mtls endpoint.
vverman May 31, 2026
0f854b2
Removed redundant call to canMtlsbeEnabled.
vverman Jun 4, 2026
e39f22b
Added GOOGLE_API_USE_MTLS_ENDPOINT check to canMtlsBeEnabled.
vverman Jun 4, 2026
cc55e6f
RAB lookup url replacement to mtls is made more robust.
vverman Jun 4, 2026
f277608
Added changes to always call mTLS RAB endpoint. Removed SPIFFE fallback.
vverman Jun 11, 2026
c9bd60b
nit Fixes.
vverman Jun 11, 2026
4ba1d2f
Moved cert-discovery to a common function. Addressed PR comments.
vverman Jun 12, 2026
9bd60fd
cache userMtlsPolicy so we read only once other comment addressals.
vverman Jun 13, 2026
e6d3be9
lint fixes.
vverman Jun 13, 2026
dcb4c18
canMtlsBeEnabled changes.
vverman Jun 24, 2026
afde0d3
userMtlsPolicy DCL lock check for concurrency.
vverman Jun 24, 2026
b65c895
fix: address technical review findings for regional access boundary m…
vverman Jul 1, 2026
579f4bf
lint fixes.
vverman Jul 1, 2026
76a22bb
Changed check to enable mTLS so we only do it once every ~5 hours.
vverman Jul 1, 2026
3fed9f7
lint fixes.
vverman Jul 1, 2026
d6331d9
feat(mtls): support custom HttpTransportFactory with mTLS and improve…
vverman Jul 8, 2026
13c69af
test fix.
vverman Jul 13, 2026
ba2d3ca
Improves regional access boundary cooldown on task submission failure…
vverman Jul 14, 2026
bcc2932
removed test which needed static mocking.
vverman Jul 15, 2026
99d19a0
Added double check when refreshing RAB to ensure no duplicated refres…
vverman Jul 15, 2026
161f288
decouple mTLS endpoint selection and certificate availability checks …
vverman Jul 16, 2026
dfbe814
restore X509Provider.isAvailable to check certificate store availabil…
vverman Jul 16, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

package com.google.auth.mtls;

import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.core.InternalApi;
import com.google.auth.http.HttpTransportFactory;
Expand Down Expand Up @@ -62,7 +63,7 @@ public MtlsHttpTransportFactory(KeyStore mtlsKeyStore) {
}

@Override
public NetHttpTransport create() {
public HttpTransport create() {
Comment thread
vverman marked this conversation as resolved.
Comment thread
lsirac marked this conversation as resolved.
try {
// Build the mTLS transport using the provided KeyStore.
return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,19 @@
package com.google.auth.mtls;

import com.google.api.core.InternalApi;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.EnvironmentProvider;
import com.google.auth.oauth2.OAuth2Utils;
import com.google.auth.oauth2.PropertyProvider;
import com.google.common.base.Strings;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.Locale;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
* Utility class for mTLS related operations.
Expand All @@ -47,10 +52,27 @@
*/
@InternalApi
public class MtlsUtils {
private static final Logger LOGGER = Logger.getLogger(MtlsUtils.class.getName());

static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG";
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";

/**
* The policy determining when to use mutual TLS (mTLS) endpoints.
*
* <p>See <a href="https://google.aip.dev/auth/4114">AIP-4114</a> for the specification on mTLS
* endpoint usage.
*/
public enum MtlsEndpointUsagePolicy {
/** Always use the mTLS endpoint, and fail if client certificates are not configured. */
ALWAYS,
/** Never use the mTLS endpoint. */
NEVER,
/** Use the mTLS endpoint if client certificates are configured (auto-discovery). */
AUTO
}
Comment thread
vverman marked this conversation as resolved.

private MtlsUtils() {
// Prevent instantiation for Utility class
}
Expand All @@ -76,39 +98,36 @@ public static String getCertificatePath(
}

/**
* Resolves and loads the workload certificate configuration.
* Resolves and parses the workload certificate configuration.
*
* <p>The configuration file is resolved in the following order of precedence: 1. The provided
* certConfigPathOverride (if not null). 2. The path specified by the
* GOOGLE_API_CERTIFICATE_CONFIG environment variable. 3. The well-known certificate configuration
* file in the gcloud config directory.
* <p>This locates the certificate configuration file via {@link #resolveCertificateConfigFile}
* and parses its contents into a {@link WorkloadCertificateConfiguration}.
*
* @param envProvider the environment provider to use for resolving environment variables
* @param propProvider the property provider to use for resolving system properties
* @param certConfigPathOverride optional override path for the configuration file
* @return the loaded WorkloadCertificateConfiguration
* @throws IOException if the configuration file cannot be found, read, or parsed
* @throws IOException if the configuration file cannot be resolved, read, or parsed
*/
static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration(
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
throws IOException {
File certConfig;
if (certConfigPathOverride != null) {
certConfig = new File(certConfigPathOverride);
} else {
String envCredentialsPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
if (!Strings.isNullOrEmpty(envCredentialsPath)) {
certConfig = new File(envCredentialsPath);
} else {
certConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
File certConfig =
resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride);
if (certConfig == null) {
try {
File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
throw new CertificateSourceUnavailableException(
"Certificate configuration file does not exist or is not a file: "
+ wellKnownConfig.getAbsolutePath());
} catch (IOException e) {
if (e instanceof CertificateSourceUnavailableException) {
throw (CertificateSourceUnavailableException) e;
}
throw new CertificateSourceUnavailableException(
"Failed to get well-known certificate config file path", e);
}
}

if (!certConfig.isFile()) {
throw new CertificateSourceUnavailableException(
"Certificate configuration file does not exist or is not a file: "
+ certConfig.getAbsolutePath());
}
try (InputStream certConfigStream = new FileInputStream(certConfig)) {
return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream);
}
Expand Down Expand Up @@ -137,4 +156,195 @@ private static File getWellKnownCertificateConfigFile(
}
return new File(cloudConfigPath, WELL_KNOWN_CERTIFICATE_CONFIG_FILE);
}

/**
* Centralized helper method to determine if mutual TLS (mTLS) can be enabled.
*
* @param envProvider the environment provider to use for resolving environment variables
* @param propProvider the property provider to use for resolving system properties
* @param certConfigPathOverride optional override path for the configuration file
* @return true if mTLS should be enabled, false otherwise
* @throws IOException if the configuration file is present but contains missing or malformed
* files
*/
public static boolean canBeEnabled(
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
throws IOException {

// Check if client certificate usage is allowed
String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
if ("false".equalsIgnoreCase(useClientCertificate)) {

@lsirac lsirac Jul 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AIP-4114 says:

"If user specifies an mTLS endpoint override but device certificate is not available, do not fail-fast, but let server return error when connecting."

So which endpoint to call, and whether to attach a cert should be evaluated separately.

@vverman vverman Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch! I have updated the implementation to decouple the mTLS endpoint resolution from the client certificate availability check, in accordance with AIP-4114.

Under the always policy, the library will now still target the mTLS endpoint URL but will gracefully fall back to the standard transport factory (with no certificate attached) instead of throwing fail-fast exceptions during initialization. This allows the client to successfully boot up and let the connection fail downstream at connection time as required by the specification.

P.S.: we check for the mtls usage policy = never within canBeEnabled as, per AIP-4114

GOOGLE_API_USE_MTLS_ENDPOINT never: "The client library MUST NOT use an mTLS endpoint or a device certificate."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It doens't look like 4114 is being followed here currently. e.g. auto should use mTLS only when a cert is attached, and always can still use it without one.

return false;
}

MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider);
if (policy == MtlsEndpointUsagePolicy.NEVER) {
return false;
}
Comment thread
vverman marked this conversation as resolved.

File certConfigFile =
resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride);
return certConfigFile != null;
}

/**
* Returns whether the mutual TLS (mTLS) endpoint should be used.
*
* @param envProvider the environment provider to use for resolving environment variables
* @param propProvider the property provider to use for resolving system properties
* @param certConfigPathOverride optional override path for the configuration file
* @return true if the mTLS endpoint should be used, false otherwise
*/
public static boolean shouldMtlsEndpointBeUsed(
EnvironmentProvider envProvider,
PropertyProvider propProvider,
String certConfigPathOverride) {
MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider);
if (policy == MtlsEndpointUsagePolicy.ALWAYS) {
return true;
}
if (policy == MtlsEndpointUsagePolicy.NEVER) {
return false;
}
// policy is AUTO: use mTLS endpoint if client certificate can be enabled
try {
return canBeEnabled(envProvider, propProvider, certConfigPathOverride);
} catch (IOException e) {
return false;
}
}

/**
* Resolves the mutual TLS (mTLS) certificate configuration file.
*
* <p>The configuration file is resolved in the following order of precedence:
*
* <ol>
* <li>The developer-provided {@code certConfigPathOverride} (if not null).
* <li>The path specified by the {@code GOOGLE_API_CERTIFICATE_CONFIG} environment variable.
* <li>The well-known automatic gcloud workload identity provisioning location (via {@link
* #getWellKnownCertificateConfigFile}).
* </ol>
*
* <p>If an explicit configuration file is specified (via override or environment variable) and it
* is missing or invalid, an exception is thrown. If no explicit file is specified and the default
* well-known file is missing, {@code null} is returned.
*
* @param envProvider the environment provider to use for resolving environment variables
* @param propProvider the property provider to use for resolving system properties
* @param certConfigPathOverride optional override path for the configuration file
* @return the resolved File object, or null if no configuration was found
* @throws IOException if an explicit configuration file is missing or malformed
*/
@Nullable
static File resolveCertificateConfigFile(
EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride)
throws IOException {
// 1. Check explicit developer override
if (certConfigPathOverride != null) {
File certConfigFile = new File(certConfigPathOverride);
if (!certConfigFile.isFile()) {
throw new CertificateSourceUnavailableException(
"Certificate configuration file does not exist or is not a file: "
+ certConfigFile.getAbsolutePath());
}
return certConfigFile;
}

// 2. Check explicit environment variable
String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
if (!Strings.isNullOrEmpty(envPath)) {
File certConfigFile = new File(envPath);
if (!certConfigFile.isFile()) {
throw new CertificateSourceUnavailableException(
"Certificate configuration file does not exist or is not a file: "
+ certConfigFile.getAbsolutePath());
}
return certConfigFile;
}

// 3. Check optional well-known automatic provisioning location
try {
File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider);
if (wellKnownConfig.isFile()) {
return wellKnownConfig;
}
} catch (IOException e) {
LOGGER.info(
"Could not get the mutual TLS (mTLS) client certificate configuration. The library will fall back to making standard non-mTLS requests.");
}

return null;
}

/**
* Returns the current mutual TLS endpoint usage policy.
*
* @param envProvider the environment provider to use for resolving environment variables
* @return the MtlsEndpointUsagePolicy enum value
*/
public static MtlsEndpointUsagePolicy getMtlsEndpointUsagePolicy(
EnvironmentProvider envProvider) {
String mtlsEndpointUsagePolicy = envProvider.getEnv("GOOGLE_API_USE_MTLS_ENDPOINT");
if ("never".equalsIgnoreCase(mtlsEndpointUsagePolicy)) {
return MtlsEndpointUsagePolicy.NEVER;
} else if ("always".equalsIgnoreCase(mtlsEndpointUsagePolicy)) {
return MtlsEndpointUsagePolicy.ALWAYS;
}
return MtlsEndpointUsagePolicy.AUTO;
}

/**
* Prepares and upgrades the HTTP transport factory for mutual TLS (mTLS) if applicable.
*
* @param baseTransportFactory the base HTTP transport factory to upgrade
* @param envProvider the environment provider to use for resolving environment variables
* @param propProvider the property provider to use for resolving system properties
* @param certConfigPathOverride optional override path for the configuration file
* @return the mTLS-configured HTTP transport factory, or the base factory if mTLS is not enabled
* @throws IOException if mTLS is required/enabled but certificate initialization fails or an
* incompatible transport factory was provided
*/
public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled(
HttpTransportFactory baseTransportFactory,
EnvironmentProvider envProvider,
PropertyProvider propProvider,
String certConfigPathOverride)
throws IOException {

if (baseTransportFactory == null) {
return null;
}
Comment on lines +315 to +317

@lqiu96 lqiu96 Jul 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can you check if we still need this null check from the RAB flow?

I think refreshRegionalAccessBoundaryIfExpired() does an early return if the transportFactory is null (can we assert that transportFactory will always be non-null)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is technically a mtls util function which RAB is utilizing. In the future mWLID or bound tokens could also rely on this to get their transporter.

Hence I think this null check still remains valid and we should keep it.


if (baseTransportFactory instanceof MtlsHttpTransportFactory) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What if GOOGLE_API_USE_CLIENT_CERTIFICATE=false?

// A custom MtlsHttpTransportFactory was already pre-configured by the user.
// Keep using it as-is without re-initializing.
return baseTransportFactory;
}

if (!canBeEnabled(envProvider, propProvider, certConfigPathOverride)) {
return baseTransportFactory;
}

@lsirac lsirac Jul 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Currently the RAB manager assumes any non-default transport supports mTLS and switches to the mTLS endpoint.

I think this is worth a discussion - what are we doing in other languages? At least here we don't know if the custom transport carries a cert.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done! The logic now ensures the library will only switch the endpoint URL to the mTLS variant (MTLS_IAM_ENDPOINT) if the upgraded transport factory is successfully created and is not the standard non-mTLS transport (OAuth2Utils.HTTP_TRANSPORT_FACTORY).

@vverman vverman Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The changes were discussed in this thread.

The idea is that a user's overriden transport will always be trusted. If they do provide a transport which doesn't support mtls, the RAB lookup errors out and goes into cooldown to avoid overwhelming the lookup.

@nbayati curious to hear if python does the same.


if (baseTransportFactory != OAuth2Utils.HTTP_TRANSPORT_FACTORY) {
Comment thread
vverman marked this conversation as resolved.
// A user configured HttpTransportFactory was explicitly injected.
// Trust the developer's custom factory and return it as-is.
return baseTransportFactory;
}

try {
// This is the default HttpTransportFactory assigned by credentials.
// Automatically discover and load client certificates to construct an mTLS factory.
X509Provider x509Provider =
new X509Provider(envProvider, propProvider, certConfigPathOverride);
KeyStore mtlsKeyStore = x509Provider.getKeyStore();
return new MtlsHttpTransportFactory(mtlsKeyStore);
} catch (Exception e) {
LOGGER.warning(
"mTLS transport factory initialization failed, falling back to non-mTLS transport: "
+ e.getMessage());
// Graceful fallback to standard transport if mTLS initialization fails
return baseTransportFactory;
}
}
Comment thread
vverman marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public X509Provider() {
*
* <ul>
* <li>The certificate config override path, if set.
* <li>The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable
* <li>The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable.
* <li>The well known gcloud location for the certificate configuration file.
* </ul>
*
Expand All @@ -111,33 +111,27 @@ public X509Provider() {
*/
@Override
public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
WorkloadCertificateConfiguration workloadCertConfig =
MtlsUtils.getWorkloadCertificateConfiguration(
envProvider, propProvider, certConfigPathOverride);

// Read the certificate and private key file paths into streams.
try (InputStream certStream = new FileInputStream(new File(workloadCertConfig.getCertPath()));
InputStream privateKeyStream =
new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
SequenceInputStream certAndPrivateKeyStream =
new SequenceInputStream(certStream, privateKeyStream)) {
try {
// Attempt to load from resolved Config File
WorkloadCertificateConfiguration workloadCertConfig =
MtlsUtils.getWorkloadCertificateConfiguration(
envProvider, propProvider, certConfigPathOverride);

// Build a key store using the combined stream.
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
try (InputStream certStream =
new FileInputStream(new File(workloadCertConfig.getCertPath()));
InputStream privateKeyStream =
new FileInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
SequenceInputStream certAndPrivateKeyStream =
new SequenceInputStream(certStream, privateKeyStream)) {
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
}
} catch (CertificateSourceUnavailableException e) {
// Throw the CertificateSourceUnavailableException without wrapping.
throw e;
} catch (Exception e) {
// Wrap all other exception types to an IOException.
throw new IOException("X509Provider: Unexpected IOException:", e);
throw new IOException("X509Provider: Unexpected error loading from config file:", e);
}
}
Comment thread
vverman marked this conversation as resolved.

/**
* Returns true if the X509 mTLS provider is available.
*
* @throws IOException if a general I/O error occurs while determining availability.
*/
@Override
public boolean isAvailable() throws IOException {
try {
Expand Down
Loading
Loading