Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dadeda6
feat(oauth2): implement IAM impersonation mTLS transport pinning and …
macastelaz Sep 17, 2026
5cc87e0
fix(oauth2): enforce CLOUD_PLATFORM_SCOPE on impersonation source cre…
macastelaz Sep 17, 2026
73c5a0a
fix(oauth2): address PR review feedback for IAM mTLS transport pinning
macastelaz Sep 17, 2026
8034525
fix(oauth2): address post-review audit edge cases for mTLS pinning
macastelaz Sep 17, 2026
17aae1f
fix(oauth2): preserve exact class check for MtlsHttpTransportFactory …
macastelaz Sep 17, 2026
f2d752e
test(oauth2): add coverage for custom MtlsHttpTransportFactory subcla…
macastelaz Sep 17, 2026
a5e69ed
fix(oauth2): annotate refreshAccessToken(HttpTransportFactory) with @…
macastelaz Sep 18, 2026
13e8be0
fix(oauth2): address review feedback on mTLS transport pinning and 40…
macastelaz Sep 21, 2026
44ca6d7
fix(oauth2): address PR #14212 round 2 review feedback
macastelaz Sep 22, 2026
9313161
fix(oauth2): pin leaf subject token from KeyStore, cache STS token wi…
macastelaz Sep 22, 2026
6e5f7fe
fix(oauth2): support mTLS pinning for standalone ImpersonatedCredenti…
macastelaz Sep 22, 2026
ddd5ea8
fix(oauth2): handle mid-rotation invalid_grant, cached STS 401 retry,…
macastelaz Sep 23, 2026
b47ab2a
fix(auth): harden concurrency and nullability in ImpersonatedCredenti…
macastelaz Sep 23, 2026
1f062f2
fix(oauth2): address review feedback on cached STS token retry and mT…
macastelaz Sep 25, 2026
4e06ff0
test(oauth2): assert pinned KeyStore per refresh cycle in cert rotati…
macastelaz Sep 25, 2026
d5317c2
fix(oauth2): address follow-up review feedback on transport visibilit…
macastelaz Sep 25, 2026
091a91b
fix(auth): address PR #14212 follow-up review comments
macastelaz Sep 28, 2026
4bf0fdd
fix(oauth2): extract refreshWithRetry helpers and clarify mTLS/impers…
macastelaz Oct 14, 2026
fe93d90
chore(oauth2_http): simplify IdentityPoolCredentials retry flow and s…
macastelaz Oct 14, 2026
a9acfb5
Merge branch 'oauth2-bound-tokens' into cert-bound-oauth-iam-pinning
macastelaz Oct 15, 2026
16716d0
fix(auth): return null on KeyStore error and unconditionally clear sc…
macastelaz Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ public boolean hasKeyStore() {
return this.hasKeyStore;
}

/** Returns the {@link KeyStore} used by this factory, or {@code null} if none was configured. */
public @Nullable KeyStore getKeyStore() {
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,17 @@ 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(
cycleTransportFactory == this.transportFactory ? null : cycleTransportFactory);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not entirely sure I follow what is going on here. This looks to set refreshAccessToken with the existing transportFactory? There would not be any "cycle" here. This also seems to be the case for PluggableAuth and Aws, but IdentityPool is different.

Does it need this overload then for refreshAccessToken(HttpTransportFactory cycleTransportFactory)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point β€” neither AwsCredentials nor PluggableAuthCredentials uses per-cycle transport pinning, so they don't need the refreshAccessToken(HttpTransportFactory) overload. Removed the overload from ExternalAccountCredentials, AwsCredentials, and PluggableAuthCredentials (keeping it only on IdentityPoolCredentials), while retaining the early getImpersonatedCredentials() delegation inside refreshAccessToken() so the subject token isn't fetched twice when service account impersonation is configured.


StsTokenExchangeRequest.Builder stsTokenExchangeRequest =
StsTokenExchangeRequest.newBuilder(retrieveSubjectToken(), getSubjectTokenType())
.setAudience(getAudience());
Expand All @@ -130,7 +141,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 All @@ -64,6 +68,9 @@
public class CertificateIdentityPoolSubjectTokenSupplier
implements IdentityPoolSubjectTokenSupplier {

// Matches the computed value in released versions so previously serialized instances still load.
private static final long serialVersionUID = -7726329500407809830L;

private final IdentityPoolCredentialSource credentialSource;

private static final Pattern PEM_CERT_PATTERN =
Expand Down Expand Up @@ -96,6 +103,35 @@ private static String loadAndEncodeLeafCertificate(String path) throws IOExcepti
}
}

private static @Nullable String extractAndEncodeLeafCertificate(@Nullable KeyStore keyStore)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

note for us in a future PR: looks like parsing through the keystore is a repeated operation in multiple spots (I think I recall seeing this in mtls PR before). Maybe we can extract some common functionality

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed! Once both PRs land, we can extract the KeyStore leaf-certificate lookup into a shared utility method in a follow-up PR.

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);
Comment thread
lqiu96 marked this conversation as resolved.
Outdated
}
}

@VisibleForTesting
static X509Certificate parseCertificate(byte[] certData) throws CertificateException {
if (certData == null || certData.length == 0) {
Expand Down Expand Up @@ -135,14 +171,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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new non-private overload changes the computed serialVersionUID, so the previously saved cert credentials fail to load with InvalidClassException.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

CertificateIdentityPoolSubjectTokenSupplier now declares serialVersionUID = -7726329500407809830L, which is the computed value in the released jars (identical from 1.50.0 through 1.53.0). I verified it end to end: a cert-sourced IdentityPoolCredentials serialized with 1.53.0 now deserializes with this PR's classes, and fails with InvalidClassException without the pin. Added a test that asserts the value.

I checked the other serializable classes this PR touches against 1.53.0. The same issue applied to OAuthException (new constructor and getHttpStatusCode()), so it's now pinned to its released value (-5276727039237496975L) with a test too. PluggableAuthCredentials' computed value also changed, but it isn't serializable in released versions (the handler field), so there's nothing to keep compatible. The rest keep their released values.

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 +271,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 +291,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 @@ public abstract class ExternalAccountCredentials extends GoogleCredentials {

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 @@ -93,9 +95,9 @@ public abstract class ExternalAccountCredentials extends GoogleCredentials {
// Service.
private final @Nullable String workforcePoolUserProject;

protected transient HttpTransportFactory transportFactory;
protected transient volatile 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 @@ protected ExternalAccountCredentials(
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 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)
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,26 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)
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);
}
Comment on lines +295 to 302

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can this just be simplified to sourceBuilder = toBuilder()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ExternalAccountCredentials inherits the concrete public GoogleCredentials.Builder toBuilder() from GoogleCredentials and doesn't override toBuilder() (since ExternalAccountCredentials.Builder is abstract), so toBuilder() here has return type GoogleCredentials.Builder rather than ExternalAccountCredentials.Builder. Re-declaring @Override public abstract Builder toBuilder() on ExternalAccountCredentials would turn an inherited concrete method into an abstract method on a public class (which is a breaking binary/source compatibility change in Clirr), and casting (ExternalAccountCredentials.Builder) toBuilder() would throw ClassCastException at runtime if a custom subclass didn't override toBuilder(), so we kept the explicit instanceof branches here.


String targetPrincipal =
ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
// The intermediate STS access token on sourceCredentials is used to authenticate to the IAM
// Credentials generateAccessToken endpoint, which requires the cloud-platform scope (whereas
// this.scopes are passed to ImpersonatedCredentials below for the final impersonated token).
sourceBuilder
.setServiceAccountImpersonationUrl(null)
.setScopes(Collections.singletonList(OAuth2Utils.CLOUD_PLATFORM_SCOPE));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can you add a note to explain why we add this scope?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done β€” added a comment explaining that sourceCredentials needs CLOUD_PLATFORM_SCOPE to authenticate to the IAM generateAccessToken endpoint, while this.scopes are passed to ImpersonatedCredentials for the final impersonated token.

sourceBuilder.targetServiceAccountEmail = targetPrincipal;
sourceBuilder.setAccessToken(null);
ExternalAccountCredentials sourceCredentials = sourceBuilder.build();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we be clearing the copied access token before building the source credentials?

e.g. after serialization, a forced refresh rebuilds the impersonation credentials but copies the cached service account token which IAM will receive, potentially causing issues (e.g. a token with only a Storage scope won't work).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch.

impersonatedCredentials is now transient and rebuilt lazily, so after deserialization newBuilder(this) copied the outer credential's cached service account token into the source. On the non-pinned path the source then looked fresh, skipped STS, and sent that token to IAM. ensureExternalSourceScoped() didn't catch it because the source is always built with cloud-platform scope. buildImpersonatedCredentials() now clears the copied access token before building the source.

Added two tests:

  • getImpersonatedCredentials_outerHasCachedToken_sourceDoesNotCopyIt
  • refreshAccessToken_withImpersonationAfterDeserialization_sendsStsTokenToIam: refreshes,
    serializes/deserializes, refreshes again, and asserts a new STS exchange happens and IAM
    receives the STS token rather than the service account token.

Both fail without the fix.

return ImpersonatedCredentials.newBuilder()
.setSourceCredentials(sourceCredentials)
.setHttpTransportFactory(transportFactory)
Expand Down Expand Up @@ -522,8 +525,55 @@ private static boolean isAwsCredential(Map<String, Object> credentialSource) {
&& ((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.
*
* <p>When {@code serviceAccountImpersonationUrl} is configured, the outer {@link
* ExternalAccountCredentials} delegates token refresh to its internal {@link
* ImpersonatedCredentials}, which in turn calls back into its cloned {@code sourceCredentials}
* (typed as {@link ExternalAccountCredentials} with {@code serviceAccountImpersonationUrl ==
* null}) via {@link #refreshAccessToken(HttpTransportFactory, KeyStore)} so the intermediate STS
* token exchange and the IAM {@code generateAccessToken} request share the same pinned transport
* and {@link KeyStore}.
*
* <p>{@link AwsCredentials} and {@link PluggableAuthCredentials} delegate {@link
* #refreshAccessToken()} into this method, while {@link IdentityPoolCredentials} coordinates
* per-cycle transport pinning and retries directly. Because {@link ExternalAccountCredentials} is
* a {@code public abstract} class with {@code protected} constructors and these hooks are
* intentionally package-private, a default implementation delegating to {@link
* #refreshAccessToken()} is provided so external subclasses outside this package remain
* compilable.
*
* @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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

qq, it's not super clear to me why we need this override. Is this due to the funky behavior with 3p creds and impersonation. IIRC, it seems to call each other in some weird way that ends up being somewhat of a cycle?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One more note: can these methods just be abstract methods that the subclasses override?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, spot on β€” when ExternalAccountCredentials is configured with service_account_impersonation_url, it builds an internal ImpersonatedCredentials whose sourceCredentials is a cloned copy of the ExternalAccountCredentials (with serviceAccountImpersonationUrl = null). During a refresh:

  1. The outer IdentityPoolCredentials.refreshAccessToken() snapshots pinnedKeyStore and cycleTransportFactory and delegates to impersonatedCredentials.refreshAccessToken(cycleTransportFactory, pinnedKeyStore).
  2. ImpersonatedCredentials then calls back into its sourceCredentials (typed as ExternalAccountCredentials) via externalSource.refreshAccessToken(effectiveTransportFactory, pinnedKeyStore) to exchange the 3P subject token at STS using that same pinned mTLS transport and KeyStore, before calling IAM generateAccessToken.

On making them abstract: because ExternalAccountCredentials is a public abstract class with protected constructors and these overloads are intentionally package-private (per #14212 (comment)), making a package-private method abstract in Java would prevent any class outside com.google.auth.oauth2 from subclassing ExternalAccountCredentials (and making them protected abstract would be a breaking public API change). Expanded the Javadoc here to explain both the impersonation call flow and why the default fallback is provided.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks primarily really only used/ needed for IdentityPoolCredentials. Can this just be put into that class?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done β€” removed refreshAccessToken(HttpTransportFactory) and refreshAccessToken(HttpTransportFactory, KeyStore) from ExternalAccountCredentials (and AwsCredentials / PluggableAuthCredentials) so they live solely on IdentityPoolCredentials.

}

/**
Expand Down Expand Up @@ -552,11 +602,10 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
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 +686,7 @@ private void readObject(ObjectInputStream input) throws IOException, ClassNotFou
*/
public @Nullable String getServiceAccountEmail() {
if (serviceAccountImpersonationUrl == null || serviceAccountImpersonationUrl.isEmpty()) {
return null;
return targetServiceAccountEmail;
Comment thread
lqiu96 marked this conversation as resolved.
}
return ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
}
Expand Down Expand Up @@ -789,6 +838,7 @@ public abstract static class Builder extends GoogleCredentials.Builder {
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 +863,7 @@ protected Builder(ExternalAccountCredentials credentials) {
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 +962,7 @@ public Builder setCredentialSource(CredentialSource credentialSource) {
public Builder setServiceAccountImpersonationUrl(
@Nullable String serviceAccountImpersonationUrl) {
this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
this.targetServiceAccountEmail = null;
return this;
}

Expand Down
Loading
Loading