Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -92,10 +92,15 @@
* deserialization), with an empty KeyStore, or with a KeyStore containing only trusted CA
* certificates (without a private key entry and certificate chain) will return {@code false}.
*/
public boolean hasKeyStore() {

Check warning on line 95 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java

View workflow job for this annotation

GitHub Actions / bom-content-test

Check warning on line 95 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

return this.hasKeyStore;
}

/** Returns the {@link KeyStore} used by this factory, or {@code null} if none was configured. */
public @Nullable KeyStore getKeyStore() {

Check warning on line 100 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java

View workflow job for this annotation

GitHub Actions / bom-content-test

Check warning on line 100 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

return this.mtlsKeyStore;
}

private static boolean checkHasKeyStore(@Nullable KeyStore keyStore) {
if (keyStore == null) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ public class AwsCredentials extends ExternalAccountCredentials {

@Override
public AccessToken refreshAccessToken() throws IOException {
return refreshAccessToken(this.transportFactory);
}

@Override
AccessToken refreshAccessToken(HttpTransportFactory cycleTransportFactory) throws IOException {
ImpersonatedCredentials impersonated = getImpersonatedCredentials();
if (impersonated != null) {
return impersonated.refreshAccessToken(null);
}

StsTokenExchangeRequest.Builder stsTokenExchangeRequest =
StsTokenExchangeRequest.newBuilder(retrieveSubjectToken(), getSubjectTokenType())
.setAudience(getAudience());
Expand All @@ -130,7 +140,8 @@ public AccessToken refreshAccessToken() throws IOException {
stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes));
}

return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build());
return exchangeExternalCredentialForAccessToken(
stsTokenExchangeRequest.build(), cycleTransportFactory);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,21 @@
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} by reading an
Expand Down Expand Up @@ -96,6 +100,35 @@ private static String loadAndEncodeLeafCertificate(String path) throws IOExcepti
}
}

private static @Nullable String extractAndEncodeLeafCertificate(@Nullable KeyStore keyStore)
throws IOException {
if (keyStore == null) {
return null;
}
try {
Enumeration<String> aliases = keyStore.aliases();
if (aliases == null) {
return null;
}
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (keyStore.isKeyEntry(alias)) {
Certificate[] chain = keyStore.getCertificateChain(alias);
if (chain != null && chain.length > 0 && chain[0] instanceof X509Certificate) {
return encodeCert((X509Certificate) chain[0]);
}
Certificate cert = keyStore.getCertificate(alias);
if (cert instanceof X509Certificate) {
return encodeCert((X509Certificate) cert);
}
}
}
return null;
} catch (KeyStoreException | CertificateEncodingException e) {
throw new IOException("Failed to extract leaf certificate from pinned KeyStore", e);
}
}

@VisibleForTesting
static X509Certificate parseCertificate(byte[] certData) throws CertificateException {
if (certData == null || certData.length == 0) {
Expand Down Expand Up @@ -135,14 +168,27 @@ private static String encodeCert(X509Certificate certificate)
*/
@Override
public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException {
String leafCertPath = credentialSource.getCredentialLocation();
return getSubjectToken(context, null);
}

/**
* Retrieves the X509 subject token, extracting the leaf certificate directly from {@code
* pinnedKeyStore} when provided so that the TLS client certificate and {@code subject_token} are
* pinned to the exact same certificate snapshot.
*/
String getSubjectToken(ExternalAccountSupplierContext context, @Nullable KeyStore pinnedKeyStore)
throws IOException {
String trustChainPath = null;
if (credentialSource.getCertificateConfig() != null) {
trustChainPath = credentialSource.getCertificateConfig().getTrustChainPath();
}

// Load and encode the leaf certificate.
String encodedLeafCert = loadAndEncodeLeafCertificate(leafCertPath);
// Extract the leaf certificate from the pinned KeyStore if present; otherwise read from disk.
String encodedLeafCert = extractAndEncodeLeafCertificate(pinnedKeyStore);
if (encodedLeafCert == null) {
String leafCertPath = credentialSource.getCredentialLocation();
encodedLeafCert = loadAndEncodeLeafCertificate(leafCertPath);
}

// Initialize the certificate chain for the subject token. The Security Token Service (STS)
// requires that the leaf certificate (the one used for authenticating this workload) must be
Expand Down Expand Up @@ -222,7 +268,8 @@ private void populateCertChainFromTrustChain(
// elsewhere in the chain.
if (encodedCurrentCert.equals(encodedLeafCert)) {
throw new IllegalArgumentException(
"The leaf certificate should only appear at the beginning of the trust chain file, or be omitted entirely.");
"The leaf certificate should only appear at the beginning of the trust chain file, or"
+ " be omitted entirely.");
}

// Add the current certificate to the chain.
Expand All @@ -241,7 +288,7 @@ private void populateCertChainFromTrustChain(
* @throws CertificateException If an error occurs while parsing a certificate.
*/
@VisibleForTesting
static List<X509Certificate> readTrustChain(String trustChainPath)
static List<X509Certificate> readTrustChain(@Nullable String trustChainPath)
throws IOException, CertificateException {
List<X509Certificate> certificateTrustChain = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import java.io.ObjectInputStream;
import java.math.BigDecimal;
import java.net.URI;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -85,6 +86,7 @@

private final @Nullable String tokenInfoUrl;
private final @Nullable String serviceAccountImpersonationUrl;
private final @Nullable String targetServiceAccountEmail;
private final @Nullable String clientId;
private final @Nullable String clientSecret;

Expand All @@ -95,7 +97,7 @@

protected transient HttpTransportFactory transportFactory;

protected @Nullable ImpersonatedCredentials impersonatedCredentials;
protected transient volatile @Nullable ImpersonatedCredentials impersonatedCredentials;

private final EnvironmentProvider environmentProvider;
private final PropertyProvider propertyProvider;
Expand Down Expand Up @@ -196,6 +198,7 @@
this.credentialSource = checkNotNull(credentialSource);
this.tokenInfoUrl = tokenInfoUrl;
this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
this.targetServiceAccountEmail = null;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.scopes =
Expand Down Expand Up @@ -236,6 +239,7 @@
this.credentialSource = builder.credentialSource;
this.tokenInfoUrl = builder.tokenInfoUrl;
this.serviceAccountImpersonationUrl = builder.serviceAccountImpersonationUrl;
this.targetServiceAccountEmail = builder.targetServiceAccountEmail;
this.clientId = builder.clientId;
this.clientSecret = builder.clientSecret;

Expand Down Expand Up @@ -286,27 +290,22 @@
if (serviceAccountImpersonationUrl == null) {
return null;
}
String targetPrincipal =
ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
// Create a copy of this instance without service account impersonation.
ExternalAccountCredentials sourceCredentials;
ExternalAccountCredentials.Builder sourceBuilder;
if (this instanceof AwsCredentials) {
sourceCredentials =
AwsCredentials.newBuilder((AwsCredentials) this)
.setServiceAccountImpersonationUrl(null)
.build();
sourceBuilder = AwsCredentials.newBuilder((AwsCredentials) this);
} else if (this instanceof PluggableAuthCredentials) {
sourceCredentials =
PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) this)
.setServiceAccountImpersonationUrl(null)
.build();
sourceBuilder = PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) this);
} else {
sourceCredentials =
IdentityPoolCredentials.newBuilder((IdentityPoolCredentials) this)
.setServiceAccountImpersonationUrl(null)
.build();
sourceBuilder = IdentityPoolCredentials.newBuilder((IdentityPoolCredentials) this);
}

String targetPrincipal =
ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
sourceBuilder
.setServiceAccountImpersonationUrl(null)
.setScopes(Collections.singletonList(OAuth2Utils.CLOUD_PLATFORM_SCOPE));
sourceBuilder.targetServiceAccountEmail = targetPrincipal;
ExternalAccountCredentials sourceCredentials = sourceBuilder.build();
return ImpersonatedCredentials.newBuilder()
.setSourceCredentials(sourceCredentials)
.setHttpTransportFactory(transportFactory)
Expand Down Expand Up @@ -363,7 +362,7 @@
* external source for authentication to Google Cloud Platform, you must validate it before
* providing it to any Google API or library. Providing an unvalidated credential configuration to
* Google APIs can compromise the security of your systems and data. For more information, refer
* to {@see <a

Check failure on line 365 in google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java

View workflow job for this annotation

GitHub Actions / bom-content-test

no tag name after @

Check failure on line 365 in google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

no tag name after @
* href="https://cloud.google.com/docs/authentication/external/externally-sourced-credentials">documentation</a>}.
*
* @param credentialsStream the stream with the credential definition
Expand All @@ -384,7 +383,7 @@
* external source for authentication to Google Cloud Platform, you must validate it before
* providing it to any Google API or library. Providing an unvalidated credential configuration to
* Google APIs can compromise the security of your systems and data. For more information, refer
* to {@see <a

Check failure on line 386 in google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java

View workflow job for this annotation

GitHub Actions / bom-content-test

no tag name after @

Check failure on line 386 in google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

no tag name after @
* href="https://cloud.google.com/docs/authentication/external/externally-sourced-credentials">documentation</a>}.
*
* @param credentialsStream the stream with the credential definition
Expand Down Expand Up @@ -522,8 +521,42 @@
&& ((String) credentialSource.get("environment_id")).startsWith("aws");
}

private boolean shouldBuildImpersonatedCredential() {
return this.serviceAccountImpersonationUrl != null && this.impersonatedCredentials == null;
@Nullable ImpersonatedCredentials getImpersonatedCredentials() {
if (this.serviceAccountImpersonationUrl == null) {
return null;
}
ImpersonatedCredentials local = this.impersonatedCredentials;
if (local == null) {
synchronized (this) {
local = this.impersonatedCredentials;
if (local == null) {
local = this.buildImpersonatedCredentials();
this.impersonatedCredentials = local;
}
}
}
return local;
}

/**
* Refreshes the access token using the specified transport factory for per-cycle transport
* pinning. {@link AwsCredentials} and {@link PluggableAuthCredentials} delegate {@link
* #refreshAccessToken()} into this method, while {@link IdentityPoolCredentials} coordinates
* per-cycle transport pinning and retries directly. This default implementation delegates back to
* {@link #refreshAccessToken()} for any custom subclasses that do not override this method.
*
* @param cycleTransportFactory the HTTP transport factory to use for this refresh cycle
* @return the refreshed access token
* @throws IOException if the token refresh fails
*/
AccessToken refreshAccessToken(HttpTransportFactory cycleTransportFactory) throws IOException {
return refreshAccessToken();
}

AccessToken refreshAccessToken(
HttpTransportFactory cycleTransportFactory, @Nullable KeyStore pinnedKeyStore)
throws IOException {
return refreshAccessToken(cycleTransportFactory);
}

/**
Expand Down Expand Up @@ -552,11 +585,10 @@
StsTokenExchangeRequest stsTokenExchangeRequest, HttpTransportFactory cycleTransportFactory)
throws IOException {
// Handle service account impersonation if necessary.
if (this.shouldBuildImpersonatedCredential()) {
this.impersonatedCredentials = this.buildImpersonatedCredentials();
}
if (this.impersonatedCredentials != null) {
return this.impersonatedCredentials.refreshAccessToken();
ImpersonatedCredentials impersonated = getImpersonatedCredentials();
if (impersonated != null) {
return impersonated.refreshAccessToken(
cycleTransportFactory == this.transportFactory ? null : cycleTransportFactory);
}

StsRequestHandler.Builder requestHandler =
Expand Down Expand Up @@ -637,7 +669,7 @@
*/
public @Nullable String getServiceAccountEmail() {
if (serviceAccountImpersonationUrl == null || serviceAccountImpersonationUrl.isEmpty()) {
return null;
return targetServiceAccountEmail;
}
return ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
}
Expand Down Expand Up @@ -789,6 +821,7 @@
protected @Nullable HttpTransportFactory transportFactory;

protected @Nullable String serviceAccountImpersonationUrl;
private @Nullable String targetServiceAccountEmail;
protected @Nullable String clientId;
protected @Nullable String clientSecret;
protected @Nullable Collection<String> scopes;
Expand All @@ -813,6 +846,7 @@
this.tokenUrl = credentials.tokenUrl;
this.tokenInfoUrl = credentials.tokenInfoUrl;
this.serviceAccountImpersonationUrl = credentials.serviceAccountImpersonationUrl;
this.targetServiceAccountEmail = credentials.targetServiceAccountEmail;
this.credentialSource = credentials.credentialSource;
this.clientId = credentials.clientId;
this.clientSecret = credentials.clientSecret;
Expand Down Expand Up @@ -911,6 +945,7 @@
public Builder setServiceAccountImpersonationUrl(
@Nullable String serviceAccountImpersonationUrl) {
this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
this.targetServiceAccountEmail = null;
return this;
}

Expand Down
Loading
Loading