feat(oauth2): support non-mTLS token URLs and unbound actor tokens in IdentityPoolCredentials - #14430
macastelaz wants to merge 11 commits into
Conversation
… IdentityPoolCredentials
There was a problem hiding this comment.
Code Review
This pull request removes the restriction requiring mTLS endpoints and transport configuration for actor token exchanges in IdentityPoolCredentials. Validation checks enforcing mTLS are removed, and unit tests are updated to verify that configuring actor tokens without mTLS now succeeds. Feedback is provided regarding a cross-platform issue in a new test where unescaped backslashes in a file path can cause JSON parsing failures on Windows.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
| return this.x509Provider != null | ||
| || (this.transportFactory instanceof MtlsHttpTransportFactory | ||
| && ((MtlsHttpTransportFactory) this.transportFactory).hasKeyStore()); | ||
| } |
There was a problem hiding this comment.
Now that isMtlsConfigured() is removed here, MtlsHttpTransportFactory.hasKeyStore() and checkHasKeyStore(KeyStore) have no production callers left in the repo, but new MtlsHttpTransportFactory(keyStore) still scans keyStore.aliases() and certificate chains on every refresh and 401 retry. Should we remove hasKeyStore() and the duplicate mtlsHttpTransportFactory_hasKeyStore_* tests in IdentityPoolCredentialsTest?
There was a problem hiding this comment.
Removed the 3 duplicate mtlsHttpTransportFactory_hasKeyStore_* tests from IdentityPoolCredentialsTest since they are already covered in MtlsHttpTransportFactoryTest. Note, however, that I kept MtlsHttpTransportFactory.hasKeyStore() itself because PR #14212 actively calls ((MtlsHttpTransportFactory) this.transportFactory).hasKeyStore() in IdentityPoolCredentials.shouldUseMtlsTransportFactory()
and readObject().
| + " source or MtlsHttpTransportFactory.", | ||
| e.getMessage()); | ||
| assertNotNull(credentials); | ||
| assertSame(actorSupplier, credentials.getIdentityPoolActorTokenSupplier()); |
There was a problem hiding this comment.
In Builder(IdentityPoolCredentials) at line 391, this.actorTokenSupplier is only copied when this.credentialSource == null, while this.actorTokenType is copied unconditionally. For a credential built like this test with both credentialSource and .setActorTokenSupplier(actorSupplier), calling credentials.createScoped(...) or credentials.toBuilder().build() drops actorTokenSupplier and throws IllegalArgumentException. Should Builder(IdentityPoolCredentials) preserve credentials.actorTokenSupplier whenever credentials.actorTokenSupplier != credentials.subjectTokenSupplier?
There was a problem hiding this comment.
Great catch! Updated Builder(IdentityPoolCredentials) to preserve credentials.actorTokenSupplier when credentials.actorTokenSupplier != credentials.subjectTokenSupplier (even when credentialSource != null), and added a credentials.createScoped(...) assertion to builder_actorTokenWithNonMtlsTransportFactory_succeeds to verify both actorTokenSupplier and actorTokenType are preserved.
| .build(); | ||
| assertNotNull(cred); | ||
| assertEquals("urn:ietf:params:oauth:token-type:jwt", cred.getActorTokenType()); | ||
| assertEquals(MockExternalAccountCredentialsTransport.STS_MTLS_URL, cred.getTokenUrl()); |
There was a problem hiding this comment.
Can we assert cred.getServiceAccountImpersonationUrl() here? The test configures the impersonation URL and verifies tokenUrl and actorTokenType, but it omits checking the impersonation URL on the built instance.
There was a problem hiding this comment.
Added assertEquals for cred.getServiceAccountImpersonationUrl().
|
|
||
| // Verify Java serialization/deserialization round-trip preserves actor token config | ||
| IdentityPoolCredentials deserialized = serializeAndDeserialize(idp); | ||
| assertEquals("urn:ietf:params:oauth:token-type:jwt", deserialized.getActorTokenType()); |
There was a problem hiding this comment.
Can we add assertNull(deserialized.getX509Provider()) and assertFalse(deserialized.getTransportFactory() instanceof MtlsHttpTransportFactory) to this deserialization check? We should confirm readObject() did not trigger unintended mTLS reconstruction on non-mTLS credentials.
There was a problem hiding this comment.
Added assertNull(deserialized.getX509Provider()) and assertFalse(deserialized.getTransportFactory() instanceof MtlsHttpTransportFactory) after serializeAndDeserialize(idp).
| void builder_actorTokenWithNoArgMtlsFactory_throws() throws Exception { | ||
| // A no-arg MtlsHttpTransportFactory (e.g. from deserialization) has no KeyStore, | ||
| // so isMtlsConfigured() should return false and building should fail. | ||
| void builder_actorTokenWithNoArgMtlsFactory_succeeds() throws Exception { |
There was a problem hiding this comment.
Can we assert assertSame(factory, credentials.getTransportFactory()) in this test and builder_actorTokenWithEmptyMtlsFactory_succeeds to verify the configured factory was preserved? We can also drop throws Exception from the method signature.
There was a problem hiding this comment.
Added assertSame(noArgFactory, credentials.getTransportFactory()) (and dropped throws Exception) in builder_actorTokenWithNoArgMtlsFactory_succeeds, and added assertSame(emptyFactory, credentials.getTransportFactory()) in builder_actorTokenWithEmptyMtlsFactory_succeeds.
| void builder_actorTokenWithNonMtlsTransportFactory_succeeds() { | ||
| IdentityPoolCredentialSource credentialSource = createFileCredentialSource(); | ||
| IdentityPoolActorTokenSupplier actorSupplier = | ||
| new IdentityPoolActorTokenSupplier() { |
There was a problem hiding this comment.
Nit: Can we use a lambda like context -> "token" or testActorSupplier here instead of the anonymous inner class?
There was a problem hiding this comment.
Replaced the anonymous inner class with IdentityPoolActorTokenSupplier actorSupplier = context -> "token";.
| .setHttpTransportFactory(transportFactory); | ||
|
|
||
| TestableIdentityPoolCredentials testable = | ||
| new TestableIdentityPoolCredentials(builder, true, false); |
There was a problem hiding this comment.
Nit: Can we just use new TestableIdentityPoolCredentials(builder, true) here? The two-argument constructor defaults failOnAllExchanges to false already. We can also pass OAuth2Utils.HTTP_TRANSPORT_FACTORY directly instead of instantiating MockExternalAccountCredentialsTransportFactory since TestableIdentityPoolCredentials mocks the exchange.
There was a problem hiding this comment.
Updated refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRetry to pass OAuth2Utils.HTTP_TRANSPORT_FACTORY directly and use the two-argument new TestableIdentityPoolCredentials(builder, true) constructor.
|
Thanks for the updates I think there are a few issues to think about:
|
…sonation caching, and custom transport deserialization
Thanks for the thorough review! Addressed all three issues:
|
| } | ||
|
|
||
| @Test | ||
| void refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRetry() { |
There was a problem hiding this comment.
This builder never sets an X509Provider, so the 401 check in IdentityPoolCredentials short-circuits on this.x509Provider != null before shouldUseMtlsTransportFactory runs, and OAuth2Utils.HTTP_TRANSPORT_FACTORY is treated as mTLS-eligible by isDefaultOrMtlsTransportFactory. Can we configure a TestX509Provider and a custom non-mTLS transport factory here so shouldUseMtlsTransportFactory is what stops the 401 retry?
There was a problem hiding this comment.
Done — the test now sets a TestX509Provider and a custom non-mTLS MockExternalAccountCredentialsTransportFactory, so shouldUseMtlsTransportFactory() is what stops the 401 retry.
| } | ||
| @VisibleForTesting | ||
| boolean shouldUseMtlsTransportFactory() { | ||
| return this.useMtlsTransportFactory |
There was a problem hiding this comment.
For streams serialized before useMtlsTransportFactory existed, transportFactoryClassName was captured as DefaultHttpTransportFactory before initializeMtlsTransport swapped in MtlsHttpTransportFactory, so useMtlsTransportFactory deserializes as false and readObject skips mTLS reconstruction. Should this return this.useMtlsTransportFactory || isDefaultOrMtlsTransportFactory(this.transportFactory) instead?
There was a problem hiding this comment.
Done — shouldUseMtlsTransportFactory() now returns this.useMtlsTransportFactory || isDefaultOrMtlsTransportFactory(this.transportFactory).
| if (this.serviceAccountImpersonationUrl == null) { | ||
| return null; | ||
| } | ||
| ImpersonatedCredentials local = this.impersonatedCredentials; |
There was a problem hiding this comment.
Should impersonatedCredentials be declared volatile? Without volatile, the first check in this double-checked locking block can let a concurrent refreshAccessToken caller observe a non-null reference before its constructor writes finish.
There was a problem hiding this comment.
Done — declared impersonatedCredentials as volatile.
| @CanIgnoreReturnValue | ||
| public Builder setCredentialSource(IdentityPoolCredentialSource credentialSource) { | ||
| super.setCredentialSource(credentialSource); | ||
| this.isClonedTransportInitialized = false; |
There was a problem hiding this comment.
Nit: toBuilder copies x509Provider and getX509Provider prefers that field, so calling setCredentialSource with a new source on a cloned builder still reloads the keystore from the old source's x509Provider. Should setCredentialSource also clear this.x509Provider when the credential source changes?
There was a problem hiding this comment.
Done — setCredentialSource now clears x509Provider when the builder already has a different credential source, so a new source on a cloned builder builds its own provider. Added toBuilder_setCredentialSource_withNewSource_clearsCopiedX509Provider to cover it.
| if (actorToken == null || actorToken.trim().isEmpty()) { | ||
| throw new IOException("The provided actor token cannot be null or empty."); | ||
| } | ||
| if (this.actorTokenType != null) { |
There was a problem hiding this comment.
Nit: The constructor already checks that actorTokenType is not null or empty whenever actorTokenSupplier is set, so this inner if is dead code and would silently drop the acting party if that invariant ever broke. Can we pass Objects.requireNonNull(this.actorTokenType) to ActingParty instead?
There was a problem hiding this comment.
Done — removed the inner if and now pass Objects.requireNonNull(this.actorTokenType) to ActingParty.
| Collections.singletonList("https://www.googleapis.com/auth/cloud-platform")); | ||
|
|
||
| assertEquals(2, getKeyStoreCount.get()); | ||
| assertEquals(1, getKeyStoreCount.get()); |
There was a problem hiding this comment.
Nit: Now that this test asserts getKeyStoreCount stays at 1 and the transport factory instance is reused across createScoped, can we rename the method to createScoped_withCertificateConfig_preservesMtlsTransportSnapshot?
There was a problem hiding this comment.
Done — renamed to createScoped_withCertificateConfig_preservesMtlsTransportSnapshot.
| + " endpoint. Please use an mTLS endpoint (e.g. containing '.mtls.') or Private" | ||
| + " Service Connect (containing '.p.')."); | ||
| } | ||
| @VisibleForTesting |
There was a problem hiding this comment.
Nit: No test calls shouldUseMtlsTransportFactory, hasInitializedMtlsTransport, or createMtlsTransportFactory. Can we drop @VisibleForTesting and make these helpers private?
There was a problem hiding this comment.
Done — dropped @VisibleForTesting and made shouldUseMtlsTransportFactory, hasInitializedMtlsTransport, and createMtlsTransportFactory private.
| } | ||
|
|
||
| @Test | ||
| void readTokens_emptyOrWhitespaceActorField_throwsIOException(@TempDir Path tempDir) |
There was a problem hiding this comment.
Nit: Since extractField now rejects whitespace-only values for subject tokens too, can we add a sibling test for an empty or whitespace subject token field?
There was a problem hiding this comment.
Done — added readTokens_emptyOrWhitespaceSubjectField_throwsIOException.
…sport helpers, x509Provider cloning, and actor token tests
|
|
||
| ImpersonatedCredentials impersonated = getImpersonatedCredentials(); | ||
| if (impersonated != null) { | ||
| return impersonated.refreshAccessToken(); |
There was a problem hiding this comment.
When impersonation is configured and hasInitializedMtlsTransport() is already true, the block at lines 198 to 204 reloads the keystore from disk and allocates cycleTransportFactory, then immediately discards it when impersonated.refreshAccessToken() returns here. Should we move the per-cycle cycleTransportFactory snapshot below this if (impersonated != null) block and only run the !hasInitializedMtlsTransport() lazy initialization before getImpersonatedCredentials()?
There was a problem hiding this comment.
Good catch, done. The lazy initialization still runs before getImpersonatedCredentials(),
so the impersonated credential is built with the mTLS factory. The per-cycle snapshot now
runs only on the direct STS path, after the impersonation return. On the first cycle, the
snapshot reuses the KeyStore that the lazy initialization just loaded, so each refresh
reads it at most once.
Added two tests:
- refreshAccessToken_withImpersonation_doesNotReloadKeyStoreAfterLazyInit: the first
refresh loads the KeyStore once and getImpersonatedCredentials() sees an
MtlsHttpTransportFactory; the second refresh doesn't load it again. - refreshAccessToken_withoutImpersonation_readsKeyStoreOncePerCycle: the first cycle loads
the KeyStore once and the exchange reuses that factory; each later cycle takes one new
snapshot.
| new MockExternalAccountCredentialsTransportFactory(); | ||
|
|
||
| Map<String, Object> certMap = new HashMap<>(); | ||
| certMap.put("use_default_certificate_config", true); |
There was a problem hiding this comment.
Because x509Provider is transient, readObject() constructs a new X509Provider with a null path when use_default_certificate_config is true, which throws IOException on getKeyStore() and hits the catch block before this.transportFactory is touched. That makes this test pass even if the shouldUseMtlsTransportFactory() check in readObject() is removed. Can we set use_default_certificate_config to false and point certificate_config_location to "testresources/mtls/certificate_config.json" so getKeyStore() succeeds during deserialization?
There was a problem hiding this comment.
The test now sets use_default_certificate_config to false and points
certificate_config_location at testresources/mtls/certificate_config.json, so
getKeyStore() succeeds during deserialization. I confirmed the test now fails if the
shouldUseMtlsTransportFactory() check in readObject() is bypassed.
| || transportFactory instanceof OAuth2Utils.DefaultHttpTransportFactory | ||
| || transportFactory.getClass() == MtlsHttpTransportFactory.class | ||
| || (transportFactory instanceof MtlsHttpTransportFactory | ||
| && !((MtlsHttpTransportFactory) transportFactory).hasKeyStore()); |
There was a problem hiding this comment.
Nit: OAuth2Utils.HTTP_TRANSPORT_FACTORY is already an instance of OAuth2Utils.DefaultHttpTransportFactory, and transportFactory.getClass() == MtlsHttpTransportFactory.class short-circuits the hasKeyStore() check since MtlsHttpTransportFactory has no subclasses. Can we simplify this to check transportFactory == null || transportFactory instanceof OAuth2Utils.DefaultHttpTransportFactory || transportFactory instanceof MtlsHttpTransportFactory?
There was a problem hiding this comment.
Done, simplified to the null / DefaultHttpTransportFactory / MtlsHttpTransportFactory instanceof checks.
| tokenJson.put("subject_token", "testSubjectToken"); | ||
| OAuth2Utils.writeInputStreamToFile( | ||
| new ByteArrayInputStream(tokenJson.toPrettyString().getBytes(StandardCharsets.UTF_8)), | ||
| tokenFile.toString()); |
There was a problem hiding this comment.
Nit: Since this test only builds the credentials and never reads from disk, can we drop @TempDir and pass "credential.json" directly like serialize_deserialize_withCustomTransportFactoryAndCertConfig_preservesCustomFactory does at line 3300?
There was a problem hiding this comment.
Done. I dropped @tempdir and the token-file write, and the test now passes "credential.json" directly.
- Skip the per-cycle KeyStore snapshot on the impersonation path; reuse the lazily initialized KeyStore on the first direct cycle. - Simplify isDefaultOrMtlsTransportFactory. - Make the custom-factory serialization test load a real cert config. - Drop the unused TempDir from the toBuilder X509Provider test.
The new constructor and getHttpStatusCode() change the computed serialVersionUID, so instances serialized by released versions would fail to load. Same change as in PR googleapis#14212.
| return ks; | ||
| } | ||
| }; | ||
| ImpersonatedCredentials impersonated = Mockito.mock(ImpersonatedCredentials.class); |
There was a problem hiding this comment.
Mocking ImpersonatedCredentials without Mockito.withSettings().withoutAnnotations() fails on the JDK 8 CI job with ArrayStoreException: sun.reflect.annotation.EnumConstantNotPresentExceptionProxy because ImpersonatedCredentials is annotated with @NullMarked. Pass Mockito.withSettings().withoutAnnotations() here like IamUtilsTest does.
There was a problem hiding this comment.
Updated to pass Mockito.withSettings().withoutAnnotations().
| static boolean isDefaultOrMtlsTransportFactory(@Nullable HttpTransportFactory transportFactory) { | ||
| return transportFactory == null | ||
| || transportFactory instanceof OAuth2Utils.DefaultHttpTransportFactory | ||
| || transportFactory instanceof MtlsHttpTransportFactory; |
There was a problem hiding this comment.
Using transportFactory instanceof MtlsHttpTransportFactory here causes shouldUseMtlsTransportFactory() to return true for caller supplied MtlsHttpTransportFactory subclasses, which overwrites custom subclasses with a plain MtlsHttpTransportFactory and conflicts with CustomMtlsHttpTransportFactory in #14212. Check transportFactory.getClass() == MtlsHttpTransportFactory.class || (transportFactory instanceof MtlsHttpTransportFactory && !((MtlsHttpTransportFactory) transportFactory).hasKeyStore()) instead so custom subclasses are preserved.
There was a problem hiding this comment.
Updated isDefaultOrMtlsTransportFactory to check transportFactory.getClass() == MtlsHttpTransportFactory.class || (transportFactory instanceof MtlsHttpTransportFactory && !((MtlsHttpTransportFactory) transportFactory).hasKeyStore()), matching #14212.
| KeyStore freshKeyStore = this.x509Provider.getKeyStore(); | ||
| HttpTransportFactory retryTransportFactory = new MtlsHttpTransportFactory(freshKeyStore); | ||
| HttpTransportFactory retryTransportFactory = createMtlsTransportFactory(freshKeyStore); | ||
| if (!hasInitializedMtlsTransport()) { |
There was a problem hiding this comment.
Is if (!hasInitializedMtlsTransport()) ever true here? The lazy init check at the top of refreshAccessToken() already initializes this.transportFactory under the same this.x509Provider != null && shouldUseMtlsTransportFactory() condition before the STS call runs.
There was a problem hiding this comment.
Good catch! Since this.transportFactory is already initialized at the start of refreshAccessToken() whenever !hasInitializedMtlsTransport() is true, this check in the catch block was unreachable. Removed
| + " Service Connect (containing '.p.')."); | ||
| } | ||
| private boolean shouldUseMtlsTransportFactory() { | ||
| return this.useMtlsTransportFactory || isDefaultOrMtlsTransportFactory(this.transportFactory); |
There was a problem hiding this comment.
Can we add a test for the || isDefaultOrMtlsTransportFactory(this.transportFactory) deserialization fallback? Right now serializeAndDeserialize only runs on instances built on this branch where useMtlsTransportFactory is already true, so setting useMtlsTransportFactory to false via reflection before round tripping would cover older serialized streams.
There was a problem hiding this comment.
Added CustomMtlsHttpTransportFactory and customMtlsHttpTransportFactorySubclass_preservedInConstructorAndRefresh_rebuiltOnDeserialization (matching #14212).
Passing .setHttpTransportFactory(new CustomMtlsHttpTransportFactory(ks)) sets useMtlsTransportFactory = false, and upon deserialization ServiceCredentials.readObject() instantiates a keyless CustomMtlsHttpTransportFactory() via newInstance(), exercising the || isDefaultOrMtlsTransportFactory(this.transportFactory) fallback branch in shouldUseMtlsTransportFactory().
| @CanIgnoreReturnValue | ||
| public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { | ||
| super.setHttpTransportFactory(transportFactory); | ||
| this.useMtlsTransportFactory = isDefaultOrMtlsTransportFactory(transportFactory); |
There was a problem hiding this comment.
Can we add a test for credential.toBuilder().setHttpTransportFactory(customFactory).build() on a credential with a certificate config? Nothing currently tests that Builder.setHttpTransportFactory updates useMtlsTransportFactory so the custom transport factory is not overwritten by lazy mTLS init.
There was a problem hiding this comment.
Added toBuilder_setHttpTransportFactory_withCustomFactoryAndCertConfig_preservesCustomFactory to verify that overriding the transport factory via toBuilder().setHttpTransportFactory(customFactory) preserves customFactory both on build() and across refreshAccessToken().
|
|
||
| assertNotNull(rebuilt.getX509Provider()); | ||
| assertNotSame(trackingProvider, rebuilt.getX509Provider()); | ||
| assertEquals(1, getKeyStoreCount.get()); |
There was a problem hiding this comment.
Can we also assert assertNotSame(credential.getTransportFactory(), rebuilt.getTransportFactory()) here to verify that setCredentialSource resets isClonedTransportInitialized?
There was a problem hiding this comment.
Added assertNotSame(credential.getTransportFactory(), rebuilt.getTransportFactory()).
| .setAudience("audience") | ||
| .setSubjectTokenType("urn:ietf:params:oauth:token-type:jwt") | ||
| .setTokenUrl("https://sts.googleapis.com/v1/token") | ||
| .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) |
There was a problem hiding this comment.
Use MockExternalAccountCredentialsTransportFactory and STS_URL in both builders in this test instead of OAuth2Utils.HTTP_TRANSPORT_FACTORY with https://sts.googleapis.com/v1/token so a validation regression does not make a live network call.
There was a problem hiding this comment.
Updated refreshAccessToken_withNullOrWhitespaceActorToken_throwsIOException to use MockExternalAccountCredentialsTransportFactory and STS_URL.
| * Sets the actor token supplier used for certificate-bound OAuth 2.0 token exchanges. The | ||
| * supplier provides an actor token representing the entity on whose behalf the subject is | ||
| * acting. | ||
| * Sets the actor token supplier used for OAuth 2.0 token exchanges. The supplier provides an |
There was a problem hiding this comment.
In RFC 8693 Section 2.1, the actor token represents the acting party that acts on behalf of the subject rather than the entity on whose behalf the subject is acting. Should we update this Javadoc wording to match?
There was a problem hiding this comment.
updated the Javadoc to match RFC 8693 Section 2.1 terminology
|
|
||
| @CanIgnoreReturnValue | ||
| public Builder setCredentialSource(IdentityPoolCredentialSource credentialSource) { | ||
| if (this.credentialSource != null && this.credentialSource != credentialSource) { |
There was a problem hiding this comment.
This clears x509Provider, but this.transportFactory still holds the old credential's keyed MtlsHttpTransportFactory, so switching to a credential source without a certificate config leaves the rebuilt credential pinned to the old client certificate. Should we also reset this.transportFactory to null here when useMtlsTransportFactory is true, and add a test for setting a new source without a certificate config?
There was a problem hiding this comment.
Done - setCredentialSource now also resets this.transportFactory = null when this.useMtlsTransportFactory is true, and toBuilder_setCredentialSource_withNewSource_clearsCopiedX509Provider now verifies that switching to a credential source without a certificate config resets both x509Provider and MtlsHttpTransportFactory.
| + value.getClass().getName()); | ||
| } | ||
| return (String) value; | ||
| return extractField(fileContents, targetFieldName); |
There was a problem hiding this comment.
Since parseToken now delegates to extractField and rejects blank JSON token values for URL and single-token file sources too, can we add a test for a blank token field on parseToken or getSubjectToken? Right now only readTokens is covered.
There was a problem hiding this comment.
Added parseToken_jsonFormat_emptyOrWhitespaceField_throws covering both supplier.getSubjectToken(null) and FileIdentityPoolSubjectTokenSupplier.parseToken(...) when the JSON token field is whitespace.
| validateMtlsEndpoint(getServiceAccountImpersonationUrl(), "serviceAccountImpersonationUrl"); | ||
| } | ||
| } | ||
| static boolean isDefaultOrMtlsTransportFactory(@Nullable HttpTransportFactory transportFactory) { |
There was a problem hiding this comment.
Nit: Nothing outside IdentityPoolCredentials calls isDefaultOrMtlsTransportFactory. Can we make it private static?
There was a problem hiding this comment.
Done — changed isDefaultOrMtlsTransportFactory to private static.
| private @Nullable IdentityPoolActorTokenSupplier actorTokenSupplier; | ||
| private @Nullable String actorTokenType; | ||
| private @Nullable X509Provider x509Provider; | ||
| private @Nullable Boolean useMtlsTransportFactory; |
There was a problem hiding this comment.
Nit: Can we declare this as private boolean useMtlsTransportFactory = true; instead of a nullable Boolean? It is only null on a fresh builder when setHttpTransportFactory was never called, where the constructor fallback always evaluates to true anyway.
There was a problem hiding this comment.
Done — changed Builder.useMtlsTransportFactory to private boolean useMtlsTransportFactory = true; and simplified the constructor assignment to this.useMtlsTransportFactory = builder.useMtlsTransportFactory;.
| Mockito.mock(ImpersonatedCredentials.class, Mockito.withSettings().withoutAnnotations()); | ||
| Mockito.when(impersonated.refreshAccessToken()) | ||
| .thenReturn(new AccessToken("impersonatedAccessToken", null)); | ||
| List<HttpTransportFactory> factoriesSeenByImpersonation = new java.util.ArrayList<>(); |
There was a problem hiding this comment.
Nit: Can we use new ArrayList<>() here since java.util.ArrayList is already imported at line 71?
There was a problem hiding this comment.
Updated to new ArrayList<>()
Summary
Allows
actor_tokenandactor_token_typeto be used inIdentityPoolCredentialswith standard (non-mTLS) STS and IAM impersonation endpoints and without requiring client certificate (certificate_config) configuration.Context & Rationale
In #13955, client-side guardrails (
isMtlsConfigured()andvalidateMtlsEndpoint()) were enforced in theIdentityPoolCredentialsconstructor because Google STS initially required actor tokens to be paired with certificate-bound tokens over mTLS. Per the original design discussion, this was intentionally designed as a one-way door that could be loosened in a non-breaking manner once backend support for non-mTLS actor token exchanges was ready.Changes
isMtlsConfigured()check inIdentityPoolCredentials(Builder)so actor tokens can be configured with standardHttpTransportFactoryinstances and without acertificateblock.validateMtlsEndpoint()checks ontokenUrlandserviceAccountImpersonationUrl, allowing standard public endpoints (e.g.,https://sts.googleapis.com/v1/token) to be used with actor tokens.actorTokenSupplierandactorTokenType, as well as JSON format checks for file-based actor token extraction.x509Provider == null(non-mTLS credentials),refreshAccessToken()uses standard transport without snapshotting aKeyStore, and401 Unauthorizederrors propagate immediately without retry.x509Provider != null && transportFactory instanceof MtlsHttpTransportFactory(mTLS credentials), per-cycle certificate pinning and single-retry cert reload on401remain unchanged.IdentityPoolCredentials.*_succeeds).refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRetryandfromStream_fileCredentialSource_withoutCertificateConfig_andActorToken_withNonMtlsUrl_refreshesSuccessfullyto verify end-to-end non-mTLS actor token exchanges.Verification
oauth2_httpmodule (including all 88 tests inIdentityPoolCredentialsTest).fmt-maven-plugin:2.25:check.