diff --git a/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java b/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java
new file mode 100644
index 000000000..fe4c14209
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025, 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.mtls;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.auth.http.HttpTransportFactory;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.Objects;
+
+/**
+ * An HttpTransportFactory that creates {@link NetHttpTransport} instances configured for mTLS
+ * (mutual TLS) using a specific {@link KeyStore} containing the client's certificate and private
+ * key.
+ *
+ *
Warning: This class is considered internal and is not intended for direct use by
+ * library consumers. Its API and behavior may change without notice.
+ */
+public class MtlsHttpTransportFactory implements HttpTransportFactory {
+ private final KeyStore mtlsKeyStore;
+
+ /**
+ * Constructs a factory for mTLS transports.
+ *
+ * @param mtlsKeyStore The {@link KeyStore} containing the client's X509 certificate and private
+ * key. This {@link KeyStore} is used for client authentication during the TLS handshake. Must
+ * not be null.
+ */
+ public MtlsHttpTransportFactory(KeyStore mtlsKeyStore) {
+ this.mtlsKeyStore = Objects.requireNonNull(mtlsKeyStore, "mtlsKeyStore cannot be null");
+ }
+
+ @Override
+ public NetHttpTransport create() {
+ try {
+ // Build the mTLS transport using the provided KeyStore.
+ return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build();
+ } catch (GeneralSecurityException e) {
+ // Wrap the checked exception in a RuntimeException because the HttpTransportFactory
+ // interface's create() method doesn't allow throwing checked exceptions.
+ throw new RuntimeException("Failed to initialize mTLS transport.", e);
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/mtls/X509Provider.java b/oauth2_http/java/com/google/auth/mtls/X509Provider.java
index 704f85bdd..cb08c2229 100644
--- a/oauth2_http/java/com/google/auth/mtls/X509Provider.java
+++ b/oauth2_http/java/com/google/auth/mtls/X509Provider.java
@@ -53,7 +53,7 @@ public class X509Provider {
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
- private String certConfigPathOverride;
+ private final String certConfigPathOverride;
/**
* Creates an X509 provider with an override path for the certificate configuration, bypassing the
@@ -75,6 +75,29 @@ public X509Provider() {
this(null);
}
+ /**
+ * Returns the path to the client certificate file specified by the loaded workload certificate
+ * configuration.
+ *
+ *
If the configuration has not been loaded yet (e.g., if {@link #getKeyStore()} has not been
+ * called), this method will attempt to load it first by searching the override path, environment
+ * variable, and well-known locations.
+ *
+ * @return The path to the certificate file.
+ * @throws IOException if the certificate configuration cannot be found or loaded, or if the
+ * configuration file does not specify a certificate path.
+ * @throws CertificateSourceUnavailableException if the configuration file is not found.
+ */
+ public String getCertificatePath() throws IOException {
+ String certPath = getWorkloadCertificateConfiguration().getCertPath();
+ if (Strings.isNullOrEmpty(certPath)) {
+ // Ensure the loaded configuration actually contains the required path.
+ throw new CertificateSourceUnavailableException(
+ "Certificate configuration loaded successfully, but does not contain a 'certificate_file' path.");
+ }
+ return certPath;
+ }
+
/**
* Finds the certificate configuration file, then builds a Keystore using the X.509 certificate
* and private key pointed to by the configuration. This will check the following locations in
@@ -90,9 +113,7 @@ public X509Provider() {
* @throws IOException if there is an error retrieving the certificate configuration.
*/
public KeyStore getKeyStore() throws IOException {
-
WorkloadCertificateConfiguration workloadCertConfig = getWorkloadCertificateConfiguration();
-
InputStream certStream = null;
InputStream privateKeyStream = null;
SequenceInputStream certAndPrivateKeyStream = null;
diff --git a/oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java b/oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java
new file mode 100644
index 000000000..4a33a6479
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 LLC 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 static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+
+/**
+ * Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} by reading an
+ * X.509 certificate from the filesystem. The certificate file (e.g., PEM or DER encoded) is read,
+ * the leaf certificate is base64-encoded (DER format), wrapped in a JSON array, and used as the
+ * subject token for STS exchange.
+ */
+public class CertificateIdentityPoolSubjectTokenSupplier
+ implements IdentityPoolSubjectTokenSupplier {
+
+ private final IdentityPoolCredentialSource credentialSource;
+
+ CertificateIdentityPoolSubjectTokenSupplier(IdentityPoolCredentialSource credentialSource) {
+ this.credentialSource = checkNotNull(credentialSource, "credentialSource cannot be null");
+ // This check ensures that the credential source was intended for certificate usage.
+ // IdentityPoolCredentials logic should guarantee credentialLocation is set in this case.
+ checkNotNull(
+ credentialSource.getCertificateConfig(),
+ "credentialSource.certificateConfig cannot be null when creating"
+ + " CertificateIdentityPoolSubjectTokenSupplier");
+ }
+
+ private static X509Certificate loadLeafCertificate(String path)
+ throws IOException, CertificateException {
+ byte[] leafCertBytes = Files.readAllBytes(Paths.get(path));
+ return parseCertificate(leafCertBytes);
+ }
+
+ @VisibleForTesting
+ static X509Certificate parseCertificate(byte[] certData) throws CertificateException {
+ if (certData == null || certData.length == 0) {
+ throw new IllegalArgumentException(
+ "Invalid certificate data: Certificate file is empty or null.");
+ }
+
+ try {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ InputStream certificateStream = new ByteArrayInputStream(certData);
+ return (X509Certificate) certificateFactory.generateCertificate(certificateStream);
+ } catch (CertificateException e) {
+ // Catch the original exception to add context about the operation being performed.
+ // This helps pinpoint the failure point during debugging.
+ throw new CertificateException("Failed to parse X.509 certificate data.", e);
+ }
+ }
+
+ private static String encodeCert(X509Certificate certificate)
+ throws CertificateEncodingException {
+ return Base64.getEncoder().encodeToString(certificate.getEncoded());
+ }
+
+ /**
+ * Retrieves the X509 subject token. This method loads the leaf certificate specified by the
+ * {@code credentialSource.credentialLocation}. The subject token is constructed as a JSON array
+ * containing the base64-encoded (DER format) leaf certificate. This JSON array serves as the
+ * subject token for mTLS authentication.
+ *
+ * @param context The external account supplier context. This parameter is currently not used in
+ * this implementation.
+ * @return The JSON string representation of the base64-encoded leaf certificate in a JSON array.
+ * @throws IOException If an I/O error occurs while reading the certificate file.
+ */
+ @Override
+ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException {
+ try {
+ // credentialSource.credentialLocation is expected to be non-null here,
+ // set during IdentityPoolCredentials construction for certificate type.
+ X509Certificate leafCert = loadLeafCertificate(credentialSource.getCredentialLocation());
+ String encodedLeafCert = encodeCert(leafCert);
+
+ java.util.List certChain = new java.util.ArrayList<>();
+ certChain.add(encodedLeafCert);
+
+ return OAuth2Utils.JSON_FACTORY.toString(certChain);
+ } catch (CertificateException e) {
+ // Catch CertificateException to provide a more specific error message including
+ // the path of the file that failed to parse, and re-throw as IOException
+ // as expected by the getSubjectToken method signature for I/O related issues.
+ throw new IOException(
+ "Failed to parse certificate(s) from: " + credentialSource.getCredentialLocation(), e);
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java b/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java
index 392d8e5ee..a939507d4 100644
--- a/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java
+++ b/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java
@@ -37,7 +37,6 @@
import com.google.common.io.CharStreams;
import java.io.BufferedReader;
import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -47,8 +46,8 @@
import java.nio.file.Paths;
/**
- * Internal provider for retrieving subject tokens for {@link IdentityPoolCredentials} to exchange
- * for GCP access tokens via a local file.
+ * Internal provider for retrieving the subject tokens for {@link IdentityPoolCredentials} to
+ * exchange for GCP access tokens via a local file.
*/
class FileIdentityPoolSubjectTokenSupplier implements IdentityPoolSubjectTokenSupplier {
@@ -67,14 +66,15 @@ class FileIdentityPoolSubjectTokenSupplier implements IdentityPoolSubjectTokenSu
@Override
public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException {
- String credentialFilePath = this.credentialSource.credentialLocation;
+ String credentialFilePath = this.credentialSource.getCredentialLocation();
if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) {
throw new IOException(
String.format(
"Invalid credential location. The file at %s does not exist.", credentialFilePath));
}
try {
- return parseToken(new FileInputStream(new File(credentialFilePath)), this.credentialSource);
+ return parseToken(
+ Files.newInputStream(new File(credentialFilePath).toPath()), this.credentialSource);
} catch (IOException e) {
throw new IOException(
"Error when attempting to read the subject token from the credential file.", e);
diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentialSource.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentialSource.java
index 6fa9e6f41..4ab63b8ed 100644
--- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentialSource.java
+++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentialSource.java
@@ -31,6 +31,8 @@
package com.google.auth.oauth2;
+import static com.google.common.base.Preconditions.checkArgument;
+
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -45,9 +47,193 @@ public class IdentityPoolCredentialSource extends ExternalAccountCredentials.Cre
private static final long serialVersionUID = -745855247050085694L;
IdentityPoolCredentialSourceType credentialSourceType;
CredentialFormatType credentialFormatType;
- String credentialLocation;
+ private String credentialLocation;
@Nullable String subjectTokenFieldName;
@Nullable Map headers;
+ @Nullable private CertificateConfig certificateConfig;
+
+ /**
+ * Gets the location of the credential source. This could be a file path or a URL, depending on
+ * the {@link IdentityPoolCredentialSourceType}.
+ *
+ * @return The location of the credential source.
+ */
+ public String getCredentialLocation() {
+ return credentialLocation;
+ }
+
+ /**
+ * Sets the location of the credential source. This method should be used to update the credential
+ * location.
+ *
+ * @param credentialLocation The new location of the credential source.
+ */
+ public void setCredentialLocation(String credentialLocation) {
+ this.credentialLocation = credentialLocation;
+ }
+
+ /**
+ * Gets the configuration for X.509-based workload credentials (mTLS), if configured.
+ *
+ * @return The {@link CertificateConfig} object, or {@code null} if not configured for
+ * certificate-based credentials.
+ */
+ @Nullable
+ public CertificateConfig getCertificateConfig() {
+ return certificateConfig;
+ }
+
+ /**
+ * Extracts and configures the {@link CertificateConfig} from the provided credential source.
+ *
+ * @param credentialSourceMap A map containing the certificate configuration.
+ * @return A new {@link CertificateConfig} instance.
+ * @throws IllegalArgumentException if the 'certificate' entry is not a Map or if required fields
+ * within the certificate configuration have invalid types.
+ */
+ private CertificateConfig certificateConfigFromSourceMap(
+ Map credentialSourceMap) {
+ Object certValue = credentialSourceMap.get("certificate");
+ if (!(certValue instanceof Map)) {
+ throw new IllegalArgumentException(
+ "The 'certificate' credential source must be a JSON object (Map).");
+ }
+ Map certificateMap = (Map) certValue;
+
+ Boolean useDefaultCertificateConfig =
+ getOptionalBoolean(certificateMap, "use_default_certificate_config");
+ String trustChain = getOptionalString(certificateMap, "trust_chain_path");
+ String certificateConfigLocation =
+ getOptionalString(certificateMap, "certificate_config_location");
+
+ return new CertificateConfig(
+ useDefaultCertificateConfig, certificateConfigLocation, trustChain);
+ }
+
+ /**
+ * Retrieves an optional boolean value from a map.
+ *
+ * @param map The map to retrieve from.
+ * @param key The key of the boolean value.
+ * @return The boolean value if present and of the correct type, otherwise null.
+ * @throws IllegalArgumentException if the value is present but not a boolean.
+ */
+ private @Nullable Boolean getOptionalBoolean(Map map, String key) {
+ Object value = map.get(key);
+ if (value == null) {
+ return null;
+ }
+ if (!(value instanceof Boolean)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Invalid type for '%s' in certificate configuration: expected Boolean, got %s.",
+ key, value.getClass().getSimpleName()));
+ }
+ return (Boolean) value;
+ }
+
+ /**
+ * Retrieves an optional string value from a map.
+ *
+ * @param map The map to retrieve from.
+ * @param key The key of the string value.
+ * @return The string value if present and of the correct type, otherwise null.
+ * @throws IllegalArgumentException if the value is present but not a string.
+ */
+ private @Nullable String getOptionalString(Map map, String key) {
+ Object value = map.get(key);
+ if (value == null) {
+ return null;
+ }
+ if (!(value instanceof String)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Invalid type for '%s' in certificate configuration: expected String, got %s.",
+ key, value.getClass().getSimpleName()));
+ }
+ return (String) value;
+ }
+
+ /**
+ * Represents the configuration options for X.509-based workload credentials (mTLS). It specifies
+ * how to locate and use the client certificate, private key, and optional trust chain for mutual
+ * TLS authentication.
+ */
+ public static class CertificateConfig implements java.io.Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * If true, attempts to load the default certificate configuration. It checks the
+ * GOOGLE_API_CERTIFICATE_CONFIG environment variable first, then a conventional default file
+ * location. Cannot be true if {@code certificateConfigLocation} is set.
+ */
+ private final boolean useDefaultCertificateConfig;
+
+ /**
+ * Specifies the path to the client certificate and private key file. This is used when {@code
+ * useDefaultCertificateConfig} is false or unset. Must be set if {@code
+ * useDefaultCertificateConfig} is false.
+ */
+ @Nullable private final String certificateConfigLocation;
+
+ /**
+ * Specifies the path to a PEM-formatted file containing the X.509 certificate trust chain. This
+ * file should contain any intermediate certificates required to complete the trust chain
+ * between the leaf certificate (used for mTLS) and the root certificate(s) in your workload
+ * identity pool's trust store. The leaf certificate and any certificates already present in the
+ * workload identity pool's trust store are optional in this file. Certificates should be
+ * ordered with the leaf certificate (or the certificate which signed the leaf) first.
+ */
+ @Nullable private final String trustChainPath;
+
+ /**
+ * Constructor for {@code CertificateConfig}.
+ *
+ * @param useDefaultCertificateConfig Whether to use the default certificate configuration.
+ * @param certificateConfigLocation Path to the client certificate and private key file.
+ * @param trustChainPath Path to the trust chain file.
+ * @throws IllegalArgumentException if the configuration is invalid (e.g., neither default nor
+ * location is specified, or both are specified).
+ */
+ CertificateConfig(
+ @Nullable Boolean useDefaultCertificateConfig,
+ @Nullable String certificateConfigLocation,
+ @Nullable String trustChainPath) {
+
+ boolean useDefault = useDefaultCertificateConfig != null && useDefaultCertificateConfig;
+ boolean locationIsPresent =
+ certificateConfigLocation != null && !certificateConfigLocation.isEmpty();
+
+ checkArgument(
+ (useDefault || locationIsPresent),
+ "Invalid 'certificate' configuration in credential source: Must specify either 'certificate_config_location' or set 'use_default_certificate_config' to true.");
+
+ checkArgument(
+ !(useDefault && locationIsPresent),
+ "Invalid 'certificate' configuration in credential source: Cannot specify both 'certificate_config_location' and set 'use_default_certificate_config' to true.");
+
+ this.useDefaultCertificateConfig = useDefault;
+ this.certificateConfigLocation = certificateConfigLocation;
+ this.trustChainPath = trustChainPath;
+ }
+
+ /** Returns whether the default certificate configuration should be used. */
+ public boolean useDefaultCertificateConfig() {
+ return useDefaultCertificateConfig;
+ }
+
+ /** Returns the path to the client certificate file, or null if not set. */
+ @Nullable
+ public String getCertificateConfigLocation() {
+ return certificateConfigLocation;
+ }
+
+ /** Returns the path to the trust chain file, or null if not set. */
+ @Nullable
+ public String getTrustChainPath() {
+ return trustChainPath;
+ }
+ }
/**
* The source of the 3P credential.
@@ -69,20 +255,29 @@ public class IdentityPoolCredentialSource extends ExternalAccountCredentials.Cre
public IdentityPoolCredentialSource(Map credentialSourceMap) {
super(credentialSourceMap);
- if (credentialSourceMap.containsKey("file") && credentialSourceMap.containsKey("url")) {
+ boolean filePresent = credentialSourceMap.containsKey("file");
+ boolean urlPresent = credentialSourceMap.containsKey("url");
+ boolean certificatePresent = credentialSourceMap.containsKey("certificate");
+
+ if ((filePresent && urlPresent)
+ || (filePresent && certificatePresent)
+ || (urlPresent && certificatePresent)) {
throw new IllegalArgumentException(
- "Only one credential source type can be set, either file or url.");
+ "Only one credential source type can be set: 'file', 'url', or 'certificate'.");
}
- if (credentialSourceMap.containsKey("file")) {
+ if (filePresent) {
credentialLocation = (String) credentialSourceMap.get("file");
credentialSourceType = IdentityPoolCredentialSourceType.FILE;
- } else if (credentialSourceMap.containsKey("url")) {
+ } else if (urlPresent) {
credentialLocation = (String) credentialSourceMap.get("url");
credentialSourceType = IdentityPoolCredentialSourceType.URL;
+ } else if (certificatePresent) {
+ credentialSourceType = IdentityPoolCredentialSourceType.CERTIFICATE;
+ this.certificateConfig = certificateConfigFromSourceMap(credentialSourceMap);
} else {
throw new IllegalArgumentException(
- "Missing credential source file location or URL. At least one must be specified.");
+ "Missing credential source file location, URL, or certificate. At least one must be specified.");
}
Map headersMap = (Map) credentialSourceMap.get("headers");
@@ -121,7 +316,8 @@ boolean hasHeaders() {
enum IdentityPoolCredentialSourceType {
FILE,
- URL
+ URL,
+ CERTIFICATE
}
enum CredentialFormatType {
diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
index 4ab4761e8..ada5b765e 100644
--- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
@@ -32,9 +32,13 @@
package com.google.auth.oauth2;
import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.mtls.MtlsHttpTransportFactory;
+import com.google.auth.mtls.X509Provider;
+import com.google.auth.oauth2.IdentityPoolCredentialSource.IdentityPoolCredentialSourceType;
import com.google.common.annotations.VisibleForTesting;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
+import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
@@ -48,6 +52,8 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials {
static final String FILE_METRICS_HEADER_VALUE = "file";
static final String URL_METRICS_HEADER_VALUE = "url";
+ static final String CERTIFICATE_METRICS_HEADER_VALUE = "certificate";
+
private static final long serialVersionUID = 2471046175477275881L;
private final IdentityPoolSubjectTokenSupplier subjectTokenSupplier;
private final ExternalAccountSupplierContext supplierContext;
@@ -63,6 +69,7 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials {
.setAudience(this.getAudience())
.setSubjectTokenType(this.getSubjectTokenType())
.build();
+
// Check that one and only one of supplier or credential source are provided.
if (builder.subjectTokenSupplier != null && credentialSource != null) {
throw new IllegalArgumentException(
@@ -72,17 +79,33 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials {
throw new IllegalArgumentException(
"A subjectTokenSupplier or a credentialSource must be provided.");
}
+
+ // Initialize based on the source type
if (builder.subjectTokenSupplier != null) {
this.subjectTokenSupplier = builder.subjectTokenSupplier;
this.metricsHeaderValue = PROGRAMMATIC_METRICS_HEADER_VALUE;
- } else if (credentialSource.credentialSourceType
- == IdentityPoolCredentialSource.IdentityPoolCredentialSourceType.FILE) {
+ } else if (credentialSource.credentialSourceType == IdentityPoolCredentialSourceType.FILE) {
this.subjectTokenSupplier = new FileIdentityPoolSubjectTokenSupplier(credentialSource);
this.metricsHeaderValue = FILE_METRICS_HEADER_VALUE;
- } else {
+ } else if (credentialSource.credentialSourceType == IdentityPoolCredentialSourceType.URL) {
this.subjectTokenSupplier =
new UrlIdentityPoolSubjectTokenSupplier(credentialSource, this.transportFactory);
this.metricsHeaderValue = URL_METRICS_HEADER_VALUE;
+ } else if (credentialSource.credentialSourceType
+ == IdentityPoolCredentialSourceType.CERTIFICATE) {
+ try {
+ this.subjectTokenSupplier =
+ createCertificateSubjectTokenSupplier(builder, credentialSource);
+ } catch (IOException e) {
+ throw new RuntimeException(
+ // Wrap IOException in RuntimeException because constructors cannot throw checked
+ // exceptions.
+ "Failed to initialize IdentityPoolCredentials from certificate source due to an I/O error.",
+ e);
+ }
+ this.metricsHeaderValue = CERTIFICATE_METRICS_HEADER_VALUE;
+ } else {
+ throw new IllegalArgumentException("Source type not supported.");
}
}
@@ -119,8 +142,7 @@ IdentityPoolSubjectTokenSupplier getIdentityPoolSubjectTokenSupplier() {
/** Clones the IdentityPoolCredentials with the specified scopes. */
@Override
public IdentityPoolCredentials createScoped(Collection newScopes) {
- return new IdentityPoolCredentials(
- (IdentityPoolCredentials.Builder) newBuilder(this).setScopes(newScopes));
+ return new IdentityPoolCredentials(newBuilder(this).setScopes(newScopes));
}
public static Builder newBuilder() {
@@ -131,9 +153,40 @@ public static Builder newBuilder(IdentityPoolCredentials identityPoolCredentials
return new Builder(identityPoolCredentials);
}
+ private IdentityPoolSubjectTokenSupplier createCertificateSubjectTokenSupplier(
+ Builder builder, IdentityPoolCredentialSource credentialSource) throws IOException {
+ // Configure the mTLS transport with the x509 keystore.
+ X509Provider x509Provider = getX509Provider(builder, credentialSource);
+ KeyStore mtlsKeyStore = x509Provider.getKeyStore();
+ this.transportFactory = new MtlsHttpTransportFactory(mtlsKeyStore);
+
+ // Initialize the subject token supplier with the certificate path.
+ credentialSource.setCredentialLocation(x509Provider.getCertificatePath());
+ return new CertificateIdentityPoolSubjectTokenSupplier(credentialSource);
+ }
+
+ private X509Provider getX509Provider(
+ Builder builder, IdentityPoolCredentialSource credentialSource) {
+ final IdentityPoolCredentialSource.CertificateConfig certConfig =
+ credentialSource.getCertificateConfig();
+
+ // Use the provided X509Provider if available, otherwise initialize a default one.
+ X509Provider x509Provider = builder.x509Provider;
+ if (x509Provider == null) {
+ // Determine the certificate path based on the configuration.
+ String explicitCertConfigPath =
+ certConfig.useDefaultCertificateConfig()
+ ? null
+ : certConfig.getCertificateConfigLocation();
+ x509Provider = new X509Provider(explicitCertConfigPath);
+ }
+ return x509Provider;
+ }
+
public static class Builder extends ExternalAccountCredentials.Builder {
private IdentityPoolSubjectTokenSupplier subjectTokenSupplier;
+ private X509Provider x509Provider;
Builder() {}
@@ -144,6 +197,21 @@ public static class Builder extends ExternalAccountCredentials.Builder {
}
}
+ /**
+ * Sets a custom {@link X509Provider} to manage the client certificate and private key for mTLS.
+ * If set, this provider will be used instead of the default behavior which initializes an
+ * {@code X509Provider} based on the {@code certificateConfigLocation} or default paths found in
+ * the {@code credentialSource}. This is primarily used for testing.
+ *
+ * @param x509Provider the custom X509 provider to use.
+ * @return this {@code Builder} object
+ */
+ @CanIgnoreReturnValue
+ Builder setX509Provider(X509Provider x509Provider) {
+ this.x509Provider = x509Provider;
+ return this;
+ }
+
/**
* Sets the subject token supplier. The supplier should return a valid subject token string.
*
diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolSubjectTokenSupplier.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolSubjectTokenSupplier.java
index 2e2920c1a..01477f8bb 100644
--- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolSubjectTokenSupplier.java
+++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolSubjectTokenSupplier.java
@@ -36,8 +36,8 @@
@FunctionalInterface
/**
- * Provider for retrieving subject tokens for {@Link IdentityPoolCredentials} to exchange for GCP
- * access tokens.
+ * Provider for retrieving the subject tokens for {@Link IdentityPoolCredentials} to exchange for
+ * GCP access tokens.
*/
public interface IdentityPoolSubjectTokenSupplier extends Serializable {
diff --git a/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java b/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java
index 788911a6c..3df49ee49 100644
--- a/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java
+++ b/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java
@@ -42,8 +42,8 @@
import java.io.IOException;
/**
- * Provider for retrieving subject tokens for {@link IdentityPoolCredentials} to exchange for GCP
- * access tokens. The subject token is retrieved by calling a URL that returns the token.
+ * Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} to exchange for
+ * GCP access tokens. The subject token is retrieved by calling a URL that returns the token.
*/
class UrlIdentityPoolSubjectTokenSupplier implements IdentityPoolSubjectTokenSupplier {
@@ -70,7 +70,7 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE
transportFactory
.create()
.createRequestFactory()
- .buildGetRequest(new GenericUrl(credentialSource.credentialLocation));
+ .buildGetRequest(new GenericUrl(credentialSource.getCredentialLocation()));
request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY));
if (credentialSource.hasHeaders()) {
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplierTest.java
new file mode 100644
index 000000000..18856d23b
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplierTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2025, Google LLC
+ *
+ * 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 LLC 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 static org.junit.Assert.*;
+import static org.mockito.Mockito.when;
+
+import com.google.auth.oauth2.IdentityPoolCredentialSource.CertificateConfig;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for {@link CertificateIdentityPoolSubjectTokenSupplier}. */
+@RunWith(JUnit4.class)
+public class CertificateIdentityPoolSubjectTokenSupplierTest {
+
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ @Mock private IdentityPoolCredentialSource mockCredentialSource;
+ @Mock private CertificateConfig mockCertificateConfig;
+ @Mock private ExternalAccountSupplierContext mockContext;
+
+ private CertificateIdentityPoolSubjectTokenSupplier supplier;
+
+ private static final byte[] INVALID_CERT_BYTES =
+ "invalid certificate data".getBytes(StandardCharsets.UTF_8);
+
+ private byte[] testCertBytesFromFile;
+
+ @Before
+ public void setUp() throws IOException, URISyntaxException {
+ ClassLoader classLoader = getClass().getClassLoader();
+ URL leafCertUrl = classLoader.getResource("x509_leaf_certificate.pem");
+ assertNotNull("Test leaf certificate file not found!", leafCertUrl);
+ File testCertFile = new File(leafCertUrl.getFile());
+
+ when(mockCertificateConfig.useDefaultCertificateConfig()).thenReturn(false);
+ when(mockCertificateConfig.getCertificateConfigLocation())
+ .thenReturn(testCertFile.getAbsolutePath());
+
+ when(mockCredentialSource.getCertificateConfig()).thenReturn(mockCertificateConfig);
+ when(mockCredentialSource.getCredentialLocation()).thenReturn(testCertFile.getAbsolutePath());
+
+ supplier = new CertificateIdentityPoolSubjectTokenSupplier(mockCredentialSource);
+ testCertBytesFromFile = Files.readAllBytes(Paths.get(leafCertUrl.toURI()));
+ }
+
+ @Test
+ public void parseCertificate_validData_returnsCertificate() throws Exception {
+ X509Certificate cert =
+ CertificateIdentityPoolSubjectTokenSupplier.parseCertificate(testCertBytesFromFile);
+ assertNotNull(cert);
+ }
+
+ @Test
+ public void parseCertificate_emptyData_throwsIllegalArgumentException() {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> CertificateIdentityPoolSubjectTokenSupplier.parseCertificate(new byte[0]));
+ assertEquals(
+ "Invalid certificate data: Certificate file is empty or null.", exception.getMessage());
+ }
+
+ @Test
+ public void parseCertificate_nullData_throwsIllegalArgumentException() {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> CertificateIdentityPoolSubjectTokenSupplier.parseCertificate(null));
+ assertEquals(
+ "Invalid certificate data: Certificate file is empty or null.", exception.getMessage());
+ }
+
+ @Test
+ public void parseCertificate_invalidData_throwsCertificateException() {
+ CertificateException exception =
+ assertThrows(
+ CertificateException.class,
+ () -> CertificateIdentityPoolSubjectTokenSupplier.parseCertificate(INVALID_CERT_BYTES));
+ assertEquals("Failed to parse X.509 certificate data.", exception.getMessage());
+ }
+
+ @Test
+ public void getSubjectToken_success() throws Exception {
+ // Calculate expected result based on the file content.
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ X509Certificate expectedCert =
+ (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(testCertBytesFromFile));
+ String expectedEncodedLeaf = Base64.getEncoder().encodeToString(expectedCert.getEncoded());
+ String[] expectedCertChainArray = new String[] {expectedEncodedLeaf};
+ String expectedSubjectToken = OAuth2Utils.JSON_FACTORY.toString(expectedCertChainArray);
+
+ // Execute
+ String actualSubjectToken = supplier.getSubjectToken(mockContext);
+
+ // Verify
+ assertEquals(expectedSubjectToken, actualSubjectToken);
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsSourceTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsSourceTest.java
new file mode 100644
index 000000000..2ee80c3d3
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsSourceTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 LLC 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 static org.junit.Assert.*;
+
+import com.google.auth.oauth2.IdentityPoolCredentialSource.IdentityPoolCredentialSourceType;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link IdentityPoolCredentialSource}. */
+@RunWith(JUnit4.class)
+public class IdentityPoolCredentialsSourceTest {
+
+ @Test
+ public void constructor_certificateConfig() {
+ Map certificateMap = new HashMap<>();
+ certificateMap.put("certificate_config_location", "/path/to/certificate");
+
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("certificate", certificateMap);
+
+ IdentityPoolCredentialSource credentialSource =
+ new IdentityPoolCredentialSource(credentialSourceMap);
+ assertEquals(
+ IdentityPoolCredentialSourceType.CERTIFICATE, credentialSource.credentialSourceType);
+ assertNotNull(credentialSource.getCertificateConfig());
+ assertFalse(credentialSource.getCertificateConfig().useDefaultCertificateConfig());
+ assertEquals(
+ "/path/to/certificate",
+ credentialSource.getCertificateConfig().getCertificateConfigLocation());
+ }
+
+ @Test
+ public void constructor_certificateConfig_useDefault() {
+ Map certificateMap = new HashMap<>();
+ certificateMap.put("use_default_certificate_config", true);
+
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("certificate", certificateMap);
+
+ IdentityPoolCredentialSource credentialSource =
+ new IdentityPoolCredentialSource(credentialSourceMap);
+ assertEquals(
+ IdentityPoolCredentialSourceType.CERTIFICATE, credentialSource.credentialSourceType);
+ assertNotNull(credentialSource.getCertificateConfig());
+ assertTrue(credentialSource.getCertificateConfig().useDefaultCertificateConfig());
+ }
+
+ @Test
+ public void constructor_certificateConfig_missingRequiredFields_throws() {
+ Map certificateMap = new HashMap<>();
+ // Missing both use_default_certificate_config and certificate_config_location.
+ certificateMap.put("trust_chain_path", "path/to/trust/chain");
+
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("certificate", certificateMap);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new IdentityPoolCredentialSource(credentialSourceMap));
+ assertEquals(
+ "Invalid 'certificate' configuration in credential source: Must specify either 'certificate_config_location' or set 'use_default_certificate_config' to true.",
+ exception.getMessage());
+ }
+
+ @Test
+ public void constructor_certificateConfig_bothFieldsSet_throws() {
+ Map certificateMap = new HashMap<>();
+ certificateMap.put("use_default_certificate_config", true);
+ certificateMap.put("certificate_config_location", "/path/to/certificate");
+
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("certificate", certificateMap);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new IdentityPoolCredentialSource(credentialSourceMap));
+
+ assertEquals(
+ "Invalid 'certificate' configuration in credential source: Cannot specify both 'certificate_config_location' and set 'use_default_certificate_config' to true.",
+ exception.getMessage());
+ }
+
+ @Test
+ public void constructor_certificateConfig_trustChainPath() {
+ Map certificateMap = new HashMap<>();
+ certificateMap.put("use_default_certificate_config", true);
+ certificateMap.put("trust_chain_path", "path/to/trust/chain");
+
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("certificate", certificateMap);
+
+ IdentityPoolCredentialSource credentialSource =
+ new IdentityPoolCredentialSource(credentialSourceMap);
+ assertEquals(
+ IdentityPoolCredentialSourceType.CERTIFICATE, credentialSource.credentialSourceType);
+ assertNotNull(credentialSource.getCertificateConfig());
+ assertEquals(
+ "path/to/trust/chain", credentialSource.getCertificateConfig().getTrustChainPath());
+ }
+
+ @Test
+ public void constructor_certificateConfig_invalidType_throws() {
+ Map certificateMap = new HashMap<>();
+ certificateMap.put("use_default_certificate_config", "invalid-type");
+
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("certificate", certificateMap);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new IdentityPoolCredentialSource(credentialSourceMap));
+
+ assertEquals(
+ "Invalid type for 'use_default_certificate_config' in certificate configuration: expected Boolean, got String.",
+ exception.getMessage());
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
index d6ca66013..9dfa69c1a 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
@@ -35,17 +35,24 @@
import static com.google.auth.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL;
import static com.google.auth.oauth2.OAuth2Utils.JSON_FACTORY;
import static org.junit.Assert.*;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
import com.google.api.client.util.Clock;
import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.mtls.X509Provider;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -53,56 +60,21 @@
import javax.annotation.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.mockito.junit.MockitoJUnitRunner;
/** Tests for {@link IdentityPoolCredentials}. */
-@RunWith(JUnit4.class)
+@RunWith(MockitoJUnitRunner.class)
public class IdentityPoolCredentialsTest extends BaseSerializationTest {
private static final String STS_URL = "https://sts.googleapis.com/v1/token";
- private static final Map FILE_CREDENTIAL_SOURCE_MAP =
- new HashMap() {
- {
- put("file", "file");
- }
- };
-
- private static final IdentityPoolCredentialSource FILE_CREDENTIAL_SOURCE =
- new IdentityPoolCredentialSource(FILE_CREDENTIAL_SOURCE_MAP);
-
- private static final IdentityPoolCredentials FILE_SOURCED_CREDENTIAL =
- IdentityPoolCredentials.newBuilder()
- .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
- .setAudience(
- "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
- .setSubjectTokenType("subjectTokenType")
- .setTokenUrl(STS_URL)
- .setTokenInfoUrl("tokenInfoUrl")
- .setCredentialSource(FILE_CREDENTIAL_SOURCE)
- .build();
-
private static final IdentityPoolSubjectTokenSupplier testProvider =
(ExternalAccountSupplierContext context) -> "testSubjectToken";
- private static final ExternalAccountSupplierContext emptyContext =
- ExternalAccountSupplierContext.newBuilder().setAudience("").setSubjectTokenType("").build();
-
- static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory {
-
- MockExternalAccountCredentialsTransport transport =
- new MockExternalAccountCredentialsTransport();
-
- @Override
- public HttpTransport create() {
- return transport;
- }
- }
-
@Test
public void createdScoped_clonedCredentialWithAddedScopes() throws IOException {
IdentityPoolCredentials credentials =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
.setQuotaProjectId("quotaProjectId")
.setClientId("clientId")
@@ -147,7 +119,7 @@ public void retrieveSubjectToken_fileSourced() throws IOException {
new IdentityPoolCredentialSource(credentialSourceMap);
IdentityPoolCredentials credentials =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setCredentialSource(credentialSource)
.build();
@@ -187,7 +159,7 @@ public void retrieveSubjectToken_fileSourcedWithJsonFormat() throws IOException
file.getAbsolutePath());
IdentityPoolCredentials credential =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setHttpTransportFactory(transportFactory)
.setCredentialSource(credentialSource)
.build();
@@ -227,7 +199,7 @@ public void retrieveSubjectToken_noFile_throws() {
new IdentityPoolCredentialSource(credentialSourceMap);
IdentityPoolCredentials credentials =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setCredentialSource(credentialSource)
.build();
@@ -247,7 +219,7 @@ public void retrieveSubjectToken_urlSourced() throws IOException {
new MockExternalAccountCredentialsTransportFactory();
IdentityPoolCredentials credential =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setHttpTransportFactory(transportFactory)
.setCredentialSource(
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
@@ -273,7 +245,7 @@ public void retrieveSubjectToken_urlSourcedWithJsonFormat() throws IOException {
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl(), formatMap);
IdentityPoolCredentials credential =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setHttpTransportFactory(transportFactory)
.setCredentialSource(credentialSource)
.build();
@@ -292,7 +264,7 @@ public void retrieveSubjectToken_urlSourcedCredential_throws() {
transportFactory.transport.addResponseErrorSequence(response);
IdentityPoolCredentials credential =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setHttpTransportFactory(transportFactory)
.setCredentialSource(
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
@@ -311,9 +283,10 @@ public void retrieveSubjectToken_urlSourcedCredential_throws() {
@Test
public void retrieveSubjectToken_provider() throws IOException {
-
+ ExternalAccountSupplierContext emptyContext =
+ ExternalAccountSupplierContext.newBuilder().setAudience("").setSubjectTokenType("").build();
IdentityPoolCredentials credentials =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setCredentialSource(null)
.setSubjectTokenSupplier(testProvider)
.build();
@@ -332,7 +305,7 @@ public void retrieveSubjectToken_providerThrowsError() throws IOException {
throw testException;
};
IdentityPoolCredentials credentials =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setCredentialSource(null)
.setSubjectTokenSupplier(errorProvider)
.build();
@@ -349,8 +322,8 @@ public void retrieveSubjectToken_providerThrowsError() throws IOException {
public void retrieveSubjectToken_supplierPassesContext() throws IOException {
ExternalAccountSupplierContext expectedContext =
ExternalAccountSupplierContext.newBuilder()
- .setAudience(FILE_SOURCED_CREDENTIAL.getAudience())
- .setSubjectTokenType(FILE_SOURCED_CREDENTIAL.getSubjectTokenType())
+ .setAudience(createBaseFileSourcedCredentials().getAudience())
+ .setSubjectTokenType(createBaseFileSourcedCredentials().getSubjectTokenType())
.build();
IdentityPoolSubjectTokenSupplier testSupplier =
@@ -360,7 +333,7 @@ public void retrieveSubjectToken_supplierPassesContext() throws IOException {
return "token";
};
IdentityPoolCredentials credentials =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setCredentialSource(null)
.setSubjectTokenSupplier(testSupplier)
.build();
@@ -379,7 +352,7 @@ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOExc
"//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
.setSubjectTokenType("subjectTokenType")
.setTokenInfoUrl("tokenInfoUrl")
- .setCredentialSource(FILE_CREDENTIAL_SOURCE)
+ .setCredentialSource(createFileCredentialSource())
.setTokenUrl(transportFactory.transport.getStsUrl())
.setHttpTransportFactory(transportFactory)
.setCredentialSource(
@@ -402,7 +375,7 @@ public void refreshAccessToken_internalOptionsSet() throws IOException {
new MockExternalAccountCredentialsTransportFactory();
IdentityPoolCredentials credential =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setWorkforcePoolUserProject("userProject")
.setAudience(
"//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
@@ -565,7 +538,7 @@ public void refreshAccessToken_workforceWithServiceAccountImpersonation() throws
transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
IdentityPoolCredentials credential =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setAudience(
"//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
.setTokenUrl(transportFactory.transport.getStsUrl())
@@ -601,7 +574,7 @@ public void refreshAccessToken_workforceWithServiceAccountImpersonationOptions()
transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
IdentityPoolCredentials credential =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setAudience(
"//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
.setTokenUrl(transportFactory.transport.getStsUrl())
@@ -722,7 +695,7 @@ public void identityPoolCredentialSource_invalidSourceType() {
fail("Should not be able to continue without exception.");
} catch (IllegalArgumentException exception) {
assertEquals(
- "Missing credential source file location or URL. At least one must be specified.",
+ "Missing credential source file location, URL, or certificate. At least one must be specified.",
exception.getMessage());
}
}
@@ -783,6 +756,7 @@ public void identityPoolCredentialSource_subjectTokenFieldNameUnset() {
@Test
public void builder_allFields() throws IOException {
List scopes = Arrays.asList("scope1", "scope2");
+ IdentityPoolCredentialSource credentialSource = createFileCredentialSource();
IdentityPoolCredentials credentials =
IdentityPoolCredentials.newBuilder()
@@ -791,7 +765,7 @@ public void builder_allFields() throws IOException {
.setSubjectTokenType("subjectTokenType")
.setTokenUrl(STS_URL)
.setTokenInfoUrl("tokenInfoUrl")
- .setCredentialSource(FILE_CREDENTIAL_SOURCE)
+ .setCredentialSource(credentialSource)
.setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
.setQuotaProjectId("quotaProjectId")
.setClientId("clientId")
@@ -806,7 +780,7 @@ public void builder_allFields() throws IOException {
assertEquals("tokenInfoUrl", credentials.getTokenInfoUrl());
assertEquals(
SERVICE_ACCOUNT_IMPERSONATION_URL, credentials.getServiceAccountImpersonationUrl());
- assertEquals(FILE_CREDENTIAL_SOURCE, credentials.getCredentialSource());
+ assertEquals(credentialSource, credentials.getCredentialSource());
assertEquals("quotaProjectId", credentials.getQuotaProjectId());
assertEquals("clientId", credentials.getClientId());
assertEquals("clientSecret", credentials.getClientSecret());
@@ -860,7 +834,7 @@ public void builder_invalidWorkforceAudiences_throws() {
.setSubjectTokenType("subjectTokenType")
.setTokenUrl(STS_URL)
.setTokenInfoUrl("tokenInfoUrl")
- .setCredentialSource(FILE_CREDENTIAL_SOURCE)
+ .setCredentialSource(createFileCredentialSource())
.setQuotaProjectId("quotaProjectId")
.build();
fail("Exception should be thrown.");
@@ -884,7 +858,7 @@ public void builder_emptyWorkforceUserProjectWithWorkforceAudience() {
.setSubjectTokenType("subjectTokenType")
.setTokenUrl(STS_URL)
.setTokenInfoUrl("tokenInfoUrl")
- .setCredentialSource(FILE_CREDENTIAL_SOURCE)
+ .setCredentialSource(createFileCredentialSource())
.setQuotaProjectId("quotaProjectId")
.build();
@@ -892,7 +866,7 @@ public void builder_emptyWorkforceUserProjectWithWorkforceAudience() {
}
@Test
- public void builder_supplierAndCredSourceThrows() throws IOException {
+ public void builder_supplierAndCredSourceThrows() {
try {
IdentityPoolCredentials credentials =
IdentityPoolCredentials.newBuilder()
@@ -901,7 +875,7 @@ public void builder_supplierAndCredSourceThrows() throws IOException {
.setAudience("audience")
.setSubjectTokenType("subjectTokenType")
.setTokenUrl(STS_URL)
- .setCredentialSource(FILE_CREDENTIAL_SOURCE)
+ .setCredentialSource(createFileCredentialSource())
.build();
fail("Should not be able to continue without exception.");
} catch (IllegalArgumentException exception) {
@@ -929,8 +903,10 @@ public void builder_noSupplierOrCredSourceThrows() throws IOException {
}
}
+ @Test
public void builder_missingUniverseDomain_defaults() throws IOException {
List scopes = Arrays.asList("scope1", "scope2");
+ IdentityPoolCredentialSource credentialSource = createFileCredentialSource();
IdentityPoolCredentials credentials =
IdentityPoolCredentials.newBuilder()
@@ -939,7 +915,7 @@ public void builder_missingUniverseDomain_defaults() throws IOException {
.setSubjectTokenType("subjectTokenType")
.setTokenUrl(STS_URL)
.setTokenInfoUrl("tokenInfoUrl")
- .setCredentialSource(FILE_CREDENTIAL_SOURCE)
+ .setCredentialSource(credentialSource)
.setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
.setQuotaProjectId("quotaProjectId")
.setClientId("clientId")
@@ -953,7 +929,7 @@ public void builder_missingUniverseDomain_defaults() throws IOException {
assertEquals("tokenInfoUrl", credentials.getTokenInfoUrl());
assertEquals(
SERVICE_ACCOUNT_IMPERSONATION_URL, credentials.getServiceAccountImpersonationUrl());
- assertEquals(FILE_CREDENTIAL_SOURCE, credentials.getCredentialSource());
+ assertEquals(credentialSource, credentials.getCredentialSource());
assertEquals("quotaProjectId", credentials.getQuotaProjectId());
assertEquals("clientId", credentials.getClientId());
assertEquals("clientSecret", credentials.getClientSecret());
@@ -974,7 +950,7 @@ public void newBuilder_allFields() throws IOException {
.setSubjectTokenType("subjectTokenType")
.setTokenUrl(STS_URL)
.setTokenInfoUrl("tokenInfoUrl")
- .setCredentialSource(FILE_CREDENTIAL_SOURCE)
+ .setCredentialSource(createFileCredentialSource())
.setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
.setQuotaProjectId("quotaProjectId")
.setClientId("clientId")
@@ -1016,7 +992,7 @@ public void newBuilder_noUniverseDomain_defaults() throws IOException {
.setSubjectTokenType("subjectTokenType")
.setTokenUrl(STS_URL)
.setTokenInfoUrl("tokenInfoUrl")
- .setCredentialSource(FILE_CREDENTIAL_SOURCE)
+ .setCredentialSource(createFileCredentialSource())
.setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
.setQuotaProjectId("quotaProjectId")
.setClientId("clientId")
@@ -1048,7 +1024,7 @@ public void newBuilder_noUniverseDomain_defaults() throws IOException {
@Test
public void serialize() throws IOException, ClassNotFoundException {
IdentityPoolCredentials testCredentials =
- IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL)
+ IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
.setQuotaProjectId("quotaProjectId")
.setClientId("clientId")
@@ -1060,7 +1036,154 @@ public void serialize() throws IOException, ClassNotFoundException {
assertEquals(testCredentials, deserializedCredentials);
assertEquals(testCredentials.hashCode(), deserializedCredentials.hashCode());
assertEquals(testCredentials.toString(), deserializedCredentials.toString());
- assertSame(deserializedCredentials.clock, Clock.SYSTEM);
+ assertSame(Clock.SYSTEM, deserializedCredentials.clock);
+ }
+
+ @Test
+ public void build_withCertificateSourceAndCustomX509Provider_success()
+ throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
+ // Create an empty KeyStore and a spy on a custom X509Provider.
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ keyStore.load(null, null);
+ TestX509Provider x509Provider =
+ spy(new TestX509Provider(keyStore, "/path/to/certificate.json"));
+
+ // Set up credential source for certificate type.
+ Map certificateMap = new HashMap<>();
+ certificateMap.put("use_default_certificate_config", true);
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("certificate", certificateMap);
+ IdentityPoolCredentialSource credentialSource =
+ new IdentityPoolCredentialSource(credentialSourceMap);
+ MockExternalAccountCredentialsTransportFactory mockTransportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ // Build credentials with the custom provider.
+ IdentityPoolCredentials credentials =
+ IdentityPoolCredentials.newBuilder()
+ .setX509Provider(x509Provider)
+ .setHttpTransportFactory(mockTransportFactory)
+ .setAudience("test-audience")
+ .setSubjectTokenType("test-token-type")
+ .setCredentialSource(credentialSource)
+ .build();
+
+ // Verify successful creation and correct internal setup.
+ assertNotNull("Credentials should be successfully created", credentials);
+ assertTrue(
+ "Subject token supplier should be for certificates",
+ credentials.getIdentityPoolSubjectTokenSupplier()
+ instanceof CertificateIdentityPoolSubjectTokenSupplier);
+ assertEquals(
+ "Metrics header should indicate certificate source",
+ IdentityPoolCredentials.CERTIFICATE_METRICS_HEADER_VALUE,
+ credentials.getCredentialSourceType());
+
+ // Verify the custom provider methods were called during build.
+ verify(x509Provider).getKeyStore();
+ verify(x509Provider).getCertificatePath();
+ }
+
+ @Test
+ public void build_withDefaultCertificate_throwsOnTransportInitFailure() {
+ // Setup credential source to use default certificate config.
+ Map certificateMap = new HashMap<>();
+ certificateMap.put("use_default_certificate_config", false);
+ certificateMap.put("certificate_config_location", "/non/existing/path/to/certificate.json");
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("certificate", certificateMap);
+ IdentityPoolCredentialSource credentialSource =
+ new IdentityPoolCredentialSource(credentialSourceMap);
+
+ // Expect RuntimeException during build due to mTLS setup failure because the certificate file
+ // doesn't exist.
+ IdentityPoolCredentials.Builder builder =
+ IdentityPoolCredentials.newBuilder()
+ .setHttpTransportFactory(new MockExternalAccountCredentialsTransportFactory())
+ .setAudience("test-audience")
+ .setSubjectTokenType("test-token-type")
+ .setCredentialSource(credentialSource);
+ RuntimeException exception = assertThrows(RuntimeException.class, builder::build);
+
+ assertEquals(
+ "Failed to initialize IdentityPoolCredentials from certificate source due to an I/O error.",
+ exception.getMessage());
+ }
+
+ @Test
+ public void build_withCustomProvider_throwsOnGetKeyStore()
+ throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
+ // Simulate a scenario where the X509Provider fails to load the KeyStore, typically due to an
+ // IOException when reading the certificate or private key files.
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ keyStore.load(null, null);
+ TestX509Provider x509Provider = new TestX509Provider(keyStore, "/path/to/certificate.json");
+ x509Provider.setShouldThrowOnGetKeyStore(true); // Configure to throw
+
+ Map certificateMap = new HashMap<>();
+ certificateMap.put("certificate_config_location", "/path/to/certificate.json");
+
+ // Expect RuntimeException because the constructor wraps the IOException.
+ RuntimeException exception =
+ assertThrows(
+ RuntimeException.class,
+ () -> createCredentialsWithCertificate(x509Provider, certificateMap));
+
+ // Verify the cause is the expected IOException from the mock.
+ assertNotNull(exception.getCause());
+ assertTrue(exception.getCause() instanceof IOException);
+ assertEquals("Simulated IOException on get keystore", exception.getCause().getMessage());
+
+ // Verify the wrapper exception message
+ assertEquals(
+ "Failed to initialize IdentityPoolCredentials from certificate source due to an I/O error.",
+ exception.getMessage());
+ }
+
+ @Test
+ public void build_withCustomProvider_throwsOnGetCertificatePath()
+ throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
+ // Simulate a scenario where the X509Provider cannot access or read the certificate
+ // configuration file needed to determine the certificate path, resulting in an IOException.
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ keyStore.load(null, null);
+ TestX509Provider x509Provider = new TestX509Provider(keyStore, "/path/to/certificate.json");
+ x509Provider.setShouldThrowOnGetCertificatePath(true); // Configure to throw
+
+ Map certificateMap = new HashMap<>();
+ certificateMap.put("certificate_config_location", "/path/to/certificate.json");
+
+ // Expect RuntimeException because the constructor wraps the IOException.
+ RuntimeException exception =
+ assertThrows(
+ RuntimeException.class,
+ () -> createCredentialsWithCertificate(x509Provider, certificateMap));
+
+ // Verify the cause is the expected IOException from the mock.
+ assertNotNull(exception.getCause());
+ assertTrue(exception.getCause() instanceof IOException);
+ assertEquals("Simulated IOException on certificate path", exception.getCause().getMessage());
+
+ // Verify the wrapper exception message
+ assertEquals(
+ "Failed to initialize IdentityPoolCredentials from certificate source due to an I/O error.",
+ exception.getMessage());
+ }
+
+ private void createCredentialsWithCertificate(
+ X509Provider x509Provider, Map certificateMap) {
+ Map credentialSourceMap = new HashMap<>();
+ credentialSourceMap.put("certificate", certificateMap);
+ IdentityPoolCredentialSource credentialSource =
+ new IdentityPoolCredentialSource(credentialSourceMap);
+
+ IdentityPoolCredentials.newBuilder()
+ .setX509Provider(x509Provider)
+ .setHttpTransportFactory(new MockExternalAccountCredentialsTransportFactory())
+ .setAudience("")
+ .setSubjectTokenType("")
+ .setCredentialSource(credentialSource)
+ .build();
}
static InputStream writeIdentityPoolCredentialsStream(
@@ -1109,4 +1232,75 @@ private static IdentityPoolCredentialSource buildUrlBasedCredentialSource(
return new IdentityPoolCredentialSource(credentialSourceMap);
}
+
+ private IdentityPoolCredentials createBaseFileSourcedCredentials() {
+ Map fileCredentialSourceMap = new HashMap<>();
+ fileCredentialSourceMap.put("file", "file"); // Consider using a real temp file setup if needed
+ IdentityPoolCredentialSource identityPoolCredentialSource =
+ new IdentityPoolCredentialSource(fileCredentialSourceMap);
+
+ return IdentityPoolCredentials.newBuilder()
+ .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
+ .setAudience(
+ "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl(STS_URL)
+ .setTokenInfoUrl("tokenInfoUrl")
+ .setCredentialSource(identityPoolCredentialSource)
+ .build();
+ }
+
+ private IdentityPoolCredentialSource createFileCredentialSource() {
+ Map fileCredentialSourceMap = new HashMap<>();
+ fileCredentialSourceMap.put("file", "file");
+ return new IdentityPoolCredentialSource(fileCredentialSourceMap);
+ }
+
+ static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory {
+
+ MockExternalAccountCredentialsTransport transport =
+ new MockExternalAccountCredentialsTransport();
+
+ @Override
+ public HttpTransport create() {
+ return transport;
+ }
+ }
+
+ private static class TestX509Provider extends X509Provider {
+ private final KeyStore keyStore;
+ private final String certificatePath;
+ private boolean shouldThrowOnGetKeyStore = false;
+ private boolean shouldThrowOnGetCertificatePath = false;
+
+ TestX509Provider(KeyStore keyStore, String certificatePath) {
+ super();
+ this.keyStore = keyStore;
+ this.certificatePath = certificatePath;
+ }
+
+ @Override
+ public KeyStore getKeyStore() throws IOException {
+ if (shouldThrowOnGetKeyStore) {
+ throw new IOException("Simulated IOException on get keystore");
+ }
+ return keyStore;
+ }
+
+ @Override
+ public String getCertificatePath() throws IOException {
+ if (shouldThrowOnGetCertificatePath) {
+ throw new IOException("Simulated IOException on certificate path");
+ }
+ return certificatePath;
+ }
+
+ void setShouldThrowOnGetKeyStore(boolean shouldThrow) {
+ this.shouldThrowOnGetKeyStore = shouldThrow;
+ }
+
+ void setShouldThrowOnGetCertificatePath(boolean shouldThrow) {
+ this.shouldThrowOnGetCertificatePath = shouldThrow;
+ }
+ }
}
diff --git a/oauth2_http/testresources/x509_leaf_certificate.pem b/oauth2_http/testresources/x509_leaf_certificate.pem
new file mode 100644
index 000000000..4219c2971
--- /dev/null
+++ b/oauth2_http/testresources/x509_leaf_certificate.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV
+BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV
+MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM
+7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer
+uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp
+gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4
++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3
+ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O
+gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh
+GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD
+AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr
+odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk
++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9
+ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql
+ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT
+cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB
+-----END CERTIFICATE-----
\ No newline at end of file