Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a4813a2
feat(oauth2): Extract actor tokens for cert-bound OAuth2 STS exchange
macastelaz Jul 30, 2026
c739b37
fix(oauth2): share parsed JSON cache between subject and actor tokens
macastelaz Jul 30, 2026
1a32e6f
feat(oauth2): fail loudly if actor token requested against non-file s…
macastelaz Jul 30, 2026
c42bd53
feat(oauth2): relax actor token mTLS URL validation to .mtls. for PSC…
macastelaz Jul 30, 2026
803dd53
test(oauth2): add tests for strict actor token exceptions
macastelaz Jul 30, 2026
cbcce28
chore(oauth2): update copyright year to 2026 for new files
macastelaz Jul 30, 2026
745d26b
Address architectural review feedback from paste 5381957298028544
macastelaz Jul 30, 2026
48c29d2
Fix trailing whitespace and formatting in IdentityPoolCredentialsTest
macastelaz Jul 30, 2026
178d71d
test: use real MtlsHttpTransportFactory instead of mock to fix Java 8…
macastelaz Jul 31, 2026
c22c521
Fix line width formatting in ExternalAccountCredentialsTest
macastelaz Jul 31, 2026
1aa5036
fix(oauth2): Address review findings from paste 5644036370202624
macastelaz Aug 6, 2026
474f0d7
fix(oauth2): preserve shared FileIdentityPoolTokenSupplier cache in B…
macastelaz Aug 6, 2026
62b8bd5
test(oauth2): expand unit test coverage across IdentityPoolCredential…
macastelaz Aug 7, 2026
3288350
Address PR #13955 review comments
macastelaz Aug 21, 2026
8dbaa6f
Review session improvements
macastelaz Aug 21, 2026
48ee795
chore: fix google-java-format compliance
macastelaz Aug 22, 2026
728d654
Address review comments for cert-bound OAuth Part 2
macastelaz Aug 24, 2026
483cfe0
test(oauth2): rename refreshAccessToken_useSameCertForStsAndIam to re…
macastelaz Aug 25, 2026
d9b26e4
fix(oauth2): address review comments on PR #13955
macastelaz Aug 28, 2026
2b8a0e7
fix(oauth2): rely on transport mTLS validation rather than URL string…
macastelaz Aug 28, 2026
e6a4797
fix(oauth2): validate plain public endpoints when actor tokens are co…
macastelaz Aug 29, 2026
3f95ff5
fix(oauth2): implement Serializable in MtlsHttpTransportFactory
macastelaz Sep 1, 2026
ce341c7
feat(oauth2): implement IAM impersonation mTLS transport pinning and …
macastelaz Aug 30, 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 @@ -36,8 +36,12 @@
import com.google.auth.http.HttpTransportFactory;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.Objects;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* An HttpTransportFactory that creates {@link NetHttpTransport} instances configured for mTLS
Expand All @@ -49,8 +53,19 @@
*/
@NullMarked
@InternalApi
public class MtlsHttpTransportFactory implements HttpTransportFactory {
private final KeyStore mtlsKeyStore;
public class MtlsHttpTransportFactory implements HttpTransportFactory, java.io.Serializable {
private static final long serialVersionUID = 1L;
@Nullable private final transient KeyStore mtlsKeyStore;

/**
* No-arg constructor required for Java serialization. {@link IdentityPoolCredentials} stores this
* factory in its serializable {@code transportFactory} field, and {@link
* java.io.ObjectInputStream} needs a no-arg constructor to reconstruct it during deserialization.
* Not intended for direct use; callers should use {@link #MtlsHttpTransportFactory(KeyStore)}.
*/
public MtlsHttpTransportFactory() {
this.mtlsKeyStore = null;
}

/**
* Constructs a factory for mTLS transports.
Expand All @@ -63,6 +78,36 @@ public MtlsHttpTransportFactory(KeyStore mtlsKeyStore) {
this.mtlsKeyStore = Objects.requireNonNull(mtlsKeyStore, "mtlsKeyStore cannot be null");
}

/**
* Returns whether this factory was constructed with a non-null {@link KeyStore} containing client
* certificates for mTLS. A factory created via the no-arg constructor (e.g. during
* 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() {
if (this.mtlsKeyStore == null) {
return false;
}
try {
Enumeration<String> aliases = this.mtlsKeyStore.aliases();
if (aliases == null) {
return false;
}
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (this.mtlsKeyStore.isKeyEntry(alias)) {
Certificate[] chain = this.mtlsKeyStore.getCertificateChain(alias);
if (chain != null && chain.length > 0) {
return true;
}
}
}
return false;
} catch (KeyStoreException e) {
return false;
}
}

@Override
public NetHttpTransport create() {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ public class AwsCredentials extends ExternalAccountCredentials {

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

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

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

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)
this.workforcePoolUserProject = builder.workforcePoolUserProject;
if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) {
throw new IllegalArgumentException(
"The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.");
"The workforce_pool_user_project parameter should only be provided for a Workforce Pool"
+ " configuration.");
}

validateTokenUrl(tokenUrl);
Expand Down Expand Up @@ -431,6 +432,7 @@ static ExternalAccountCredentials fromJson(
Map<String, Object> json, HttpTransportFactory transportFactory) {
String audience = (String) json.get("audience");
String subjectTokenType = (String) json.get("subject_token_type");
String actorTokenType = (String) json.get("actor_token_type");
String tokenUrl = (String) json.get("token_url");

Map<String, Object> credentialSourceMap = (Map<String, Object>) json.get("credential_source");
Expand Down Expand Up @@ -487,6 +489,7 @@ static ExternalAccountCredentials fromJson(
.setHttpTransportFactory(transportFactory)
.setAudience(audience)
.setSubjectTokenType(subjectTokenType)
.setActorTokenType(actorTokenType)
.setTokenUrl(tokenUrl)
.setTokenInfoUrl(tokenInfoUrl)
.setCredentialSource(new IdentityPoolCredentialSource(credentialSourceMap))
Expand Down Expand Up @@ -522,6 +525,19 @@ private boolean shouldBuildImpersonatedCredential() {
return this.serviceAccountImpersonationUrl != null && this.impersonatedCredentials == null;
}

/**
* Refreshes the access token using the specified transport factory. Default implementation
* delegates to {@link #refreshAccessToken()}. Subclasses should override this method if they
* support transport pinning per refresh cycle.
*
* @param transportFactory the HTTP transport factory to use for this refresh cycle
* @return the refreshed access token
* @throws IOException if the token refresh fails
*/
public AccessToken refreshAccessToken(HttpTransportFactory transportFactory) throws IOException {
return refreshAccessToken();
}

/**
* Exchanges the external credential for a Google Cloud access token.
*
Expand All @@ -531,17 +547,35 @@ private boolean shouldBuildImpersonatedCredential() {
*/
protected AccessToken exchangeExternalCredentialForAccessToken(
StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException {
return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest, this.transportFactory);
}

/**
* Exchanges the external credential for a Google Cloud access token using the specified transport
* factory. This overload allows callers to provide a per-cycle transport factory, for example one
* pinned to a specific mTLS certificate.
*
* @param stsTokenExchangeRequest the Security Token Service token exchange request
* @param cycleTransportFactory the HTTP transport factory to use for this exchange
* @return the access token returned by the Security Token Service
* @throws OAuthException if the call to the Security Token Service fails
*/
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();
return this.impersonatedCredentials.refreshAccessToken(cycleTransportFactory);
}

StsRequestHandler.Builder requestHandler =
StsRequestHandler.newBuilder(
tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory());
tokenUrl,
stsTokenExchangeRequest,
cycleTransportFactory.create().createRequestFactory());

// If this credential was initialized with a Workforce configuration then the
// workforcePoolUserProject must be passed to the Security Token Service via the internal
Expand Down
Loading
Loading