IdentityPoolCredentialSource.java
/*
* Copyright 2023 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.checkArgument;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
/**
* The IdentityPool credential source. Dictates the retrieval method of the external credential,
* which can either be through a metadata server or a local file.
*/
public class IdentityPoolCredentialSource extends ExternalAccountCredentials.CredentialSource {
private static final long serialVersionUID = -745855247050085694L;
IdentityPoolCredentialSourceType credentialSourceType;
CredentialFormatType credentialFormatType;
private String credentialLocation;
@Nullable String subjectTokenFieldName;
@Nullable Map<String, String> 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<String, Object> credentialSourceMap) {
Object certValue = credentialSourceMap.get("certificate");
if (!(certValue instanceof Map)) {
throw new IllegalArgumentException(
"The 'certificate' credential source must be a JSON object (Map).");
}
Map<String, Object> certificateMap = (Map<String, Object>) 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<String, Object> 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<String, Object> 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.
*
* <p>If this is a file based 3P credential, the credentials file can be retrieved using the
* `file` key.
*
* <p>If this is URL-based 3p credential, the metadata server URL can be retrieved using the `url`
* key.
*
* <p>The third party credential can be provided in different formats, such as text or JSON. The
* format can be specified using the `format` header, which returns a map with keys `type` and
* `subject_token_field_name`. If the `type` is json, the `subject_token_field_name` must be
* provided. If no format is provided, we expect the token to be in the raw text format.
*
* <p>Optional headers can be present, and should be keyed by `headers`.
*/
@SuppressWarnings("unchecked")
public IdentityPoolCredentialSource(Map<String, Object> credentialSourceMap) {
super(credentialSourceMap);
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: 'file', 'url', or 'certificate'.");
}
if (filePresent) {
credentialLocation = (String) credentialSourceMap.get("file");
credentialSourceType = IdentityPoolCredentialSourceType.FILE;
} 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, URL, or certificate. At least one must be specified.");
}
Map<String, String> headersMap = (Map<String, String>) credentialSourceMap.get("headers");
if (headersMap != null && !headersMap.isEmpty()) {
headers = new HashMap<>();
headers.putAll(headersMap);
}
// If the format is not provided, we expect the token to be in the raw text format.
credentialFormatType = CredentialFormatType.TEXT;
Map<String, String> formatMap = (Map<String, String>) credentialSourceMap.get("format");
if (formatMap != null && formatMap.containsKey("type")) {
String type = formatMap.get("type");
if (type != null && "json".equals(type.toLowerCase(Locale.US))) {
// For JSON, the subject_token field name must be provided.
if (!formatMap.containsKey("subject_token_field_name")) {
throw new IllegalArgumentException(
"When specifying a JSON credential type, the subject_token_field_name must be set.");
}
credentialFormatType = CredentialFormatType.JSON;
subjectTokenFieldName = formatMap.get("subject_token_field_name");
} else if (type != null && "text".equals(type.toLowerCase(Locale.US))) {
credentialFormatType = CredentialFormatType.TEXT;
} else {
throw new IllegalArgumentException(
String.format("Invalid credential source format type: %s.", type));
}
}
}
boolean hasHeaders() {
return headers != null && !headers.isEmpty();
}
enum IdentityPoolCredentialSourceType {
FILE,
URL,
CERTIFICATE
}
enum CredentialFormatType {
TEXT,
JSON
}
}