Skip to content

Commit beea42f

Browse files
authored
fix(auth): restore transportFactory upon deserialization in InternalAwsSecurityCredentialsSupplier (#14340)
Fixes #14050 Part of #12580 In InternalAwsSecurityCredentialsSupplier, transportFactory is marked transient but no transportFactoryClassName was retained and no readObject was defined. Consequently, after Java deserialization (common in distributed frameworks like Apache Flink and Apache Spark), transportFactory is null. When deserialized AwsCredentials attempts to refresh its token, a NullPointerException is thrown when retrieving AWS security credentials or region from the metadata service. This change: 1. Records transportFactoryClassName during construction. 2. Implements readObject to restore transportFactory using OAuth2Credentials.newInstance(transportFactoryClassName), with fallback to OAuth2Utils.HTTP_TRANSPORT_FACTORY for backwards compatibility with previously serialized streams. 3. Adds reproduction and serialization unit tests verifying credentials and region retrieval after deserialization, including token refresh in AwsCredentials.
1 parent 99530c7 commit beea42f

3 files changed

Lines changed: 129 additions & 5 deletions

File tree

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/InternalAwsSecurityCredentialsSupplier.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import com.google.common.annotations.VisibleForTesting;
4545
import com.google.common.collect.ImmutableList;
4646
import java.io.IOException;
47+
import java.io.ObjectInputStream;
4748
import java.util.HashMap;
4849
import java.util.List;
4950
import java.util.Map;
@@ -73,6 +74,7 @@ class InternalAwsSecurityCredentialsSupplier implements AwsSecurityCredentialsSu
7374
private final AwsCredentialSource awsCredentialSource;
7475
private EnvironmentProvider environmentProvider;
7576
private transient HttpTransportFactory transportFactory;
77+
private final String transportFactoryClassName;
7678

7779
/**
7880
* Constructor for InternalAwsSecurityCredentialsProvider
@@ -83,11 +85,25 @@ class InternalAwsSecurityCredentialsSupplier implements AwsSecurityCredentialsSu
8385
*/
8486
InternalAwsSecurityCredentialsSupplier(
8587
AwsCredentialSource awsCredentialSource,
86-
EnvironmentProvider environmentProvider,
87-
HttpTransportFactory transportFactory) {
88-
this.environmentProvider = environmentProvider;
88+
@Nullable EnvironmentProvider environmentProvider,
89+
@Nullable HttpTransportFactory transportFactory) {
90+
this.environmentProvider =
91+
environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider;
8992
this.awsCredentialSource = awsCredentialSource;
90-
this.transportFactory = transportFactory;
93+
this.transportFactory =
94+
transportFactory != null ? transportFactory : OAuth2Utils.HTTP_TRANSPORT_FACTORY;
95+
this.transportFactoryClassName = this.transportFactory.getClass().getName();
96+
}
97+
98+
@SuppressWarnings("unused")
99+
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
100+
input.defaultReadObject();
101+
transportFactory = OAuth2Credentials.newInstance(transportFactoryClassName);
102+
}
103+
104+
@VisibleForTesting
105+
HttpTransportFactory getTransportFactory() {
106+
return transportFactory;
91107
}
92108

93109
@Override

google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,36 @@ void serialize() throws IOException, ClassNotFoundException {
12481248
assertSame(Clock.SYSTEM, deserializedCredentials.clock);
12491249
}
12501250

1251+
/**
1252+
* Verifies that {@link AwsCredentials} can successfully refresh access tokens after being
1253+
* serialized and deserialized.
1254+
*/
1255+
@Test
1256+
void serialize_refreshAccessToken_success() throws IOException, ClassNotFoundException {
1257+
// Uses an in-memory MockHttpTransport (no network calls) that returns canned HTTP responses
1258+
// for both AWS IMDS metadata endpoints and the GCP STS token exchange endpoint.
1259+
MockExternalAccountCredentialsTransportFactory transportFactory =
1260+
new MockExternalAccountCredentialsTransportFactory();
1261+
1262+
// Use an IMDS credential source so that token refresh is forced to retrieve AWS credentials
1263+
// and region from the metadata server via HTTP, exercising the supplier's transportFactory.
1264+
AwsCredentials awsCredential =
1265+
AwsCredentials.newBuilder(AWS_CREDENTIAL)
1266+
.setTokenUrl(transportFactory.transport.getStsUrl())
1267+
.setHttpTransportFactory(transportFactory)
1268+
.setCredentialSource(buildAwsCredentialSource(transportFactory))
1269+
.build();
1270+
1271+
AwsCredentials deserialized = serializeAndDeserialize(awsCredential);
1272+
1273+
// refreshAccessToken() calls getCredentials(), getRegion(), and the STS token endpoint,
1274+
// verifying that the restored transportFactory is used for all HTTP requests.
1275+
AccessToken accessToken = deserialized.refreshAccessToken();
1276+
1277+
// Verifies the access token returned from the simulated STS exchange matches the mock.
1278+
assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
1279+
}
1280+
12511281
private static void ValidateRequest(
12521282
MockLowLevelHttpRequest request, String expectedUrl, Map<String, String> expectedHeaders) {
12531283
assertEquals(expectedUrl, request.getUrl());

google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/InternalAwsSecurityCredentialsSupplierTest.java

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
package com.google.auth.oauth2;
3333

3434
import static com.google.auth.oauth2.AwsCredentialsTest.buildAwsImdsv2CredentialSource;
35+
import static org.junit.jupiter.api.Assertions.assertEquals;
3536
import static org.junit.jupiter.api.Assertions.assertFalse;
37+
import static org.junit.jupiter.api.Assertions.assertNotNull;
38+
import static org.junit.jupiter.api.Assertions.assertSame;
3639
import static org.junit.jupiter.api.Assertions.assertTrue;
3740

3841
import com.google.auth.oauth2.ExternalAccountCredentialsTest.MockExternalAccountCredentialsTransportFactory;
@@ -41,7 +44,7 @@
4144
import org.junit.jupiter.api.Test;
4245

4346
/** Tests for {@link InternalAwsSecurityCredentialsSupplier}. */
44-
class InternalAwsSecurityCredentialsSupplierTest {
47+
class InternalAwsSecurityCredentialsSupplierTest extends BaseSerializationTest {
4548
@Test
4649
void shouldUseMetadataServer_withRequiredEnvironmentVariables() {
4750
MockExternalAccountCredentialsTransportFactory transportFactory =
@@ -159,4 +162,79 @@ void shouldUseMetadataServer_noEnvironmentVars() {
159162
transportFactory);
160163
assertTrue(supplier.shouldUseMetadataServer());
161164
}
165+
166+
/**
167+
* Verifies that {@link InternalAwsSecurityCredentialsSupplier} restores its {@code
168+
* transportFactory} upon deserialization, enabling successful retrieval of AWS security
169+
* credentials and region from the AWS EC2 metadata server.
170+
*/
171+
@Test
172+
void serializeAndDeserialize_retrievesCredentialsAndRegionSuccessfully() throws Exception {
173+
MockExternalAccountCredentialsTransportFactory transportFactory =
174+
new MockExternalAccountCredentialsTransportFactory();
175+
InternalAwsSecurityCredentialsSupplier supplier =
176+
new InternalAwsSecurityCredentialsSupplier(
177+
buildAwsImdsv2CredentialSource(transportFactory),
178+
// Pass null to use the default SystemEnvironmentProvider, which implements Serializable
179+
// (unlike TestEnvironmentProvider).
180+
/* environmentProvider= */ null,
181+
transportFactory);
182+
183+
InternalAwsSecurityCredentialsSupplier deserialized = serializeAndDeserialize(supplier);
184+
assertEquals(
185+
MockExternalAccountCredentialsTransportFactory.class,
186+
deserialized.getTransportFactory().getClass());
187+
188+
// Credentials and region are not serialized fields; they are retrieved on demand via HTTP.
189+
// Calling getCredentials() and getRegion() verifies that the restored transportFactory
190+
// successfully constructs and executes HTTP requests against the mock metadata server
191+
// (rather than failing with a NullPointerException).
192+
AwsSecurityCredentials credentials = deserialized.getCredentials(null);
193+
assertNotNull(credentials);
194+
assertEquals("accessKeyId", credentials.getAccessKeyId());
195+
assertEquals("secretAccessKey", credentials.getSecretAccessKey());
196+
assertEquals("token", credentials.getSessionToken());
197+
198+
String region = deserialized.getRegion(null);
199+
assertEquals("us-east-1", region);
200+
}
201+
202+
/**
203+
* Verifies that {@link InternalAwsSecurityCredentialsSupplier} deserializes cleanly and falls
204+
* back to the default {@link OAuth2Utils#HTTP_TRANSPORT_FACTORY} when no custom transport factory
205+
* was provided.
206+
*/
207+
@Test
208+
void serializeAndDeserialize_defaultTransportFactory_success() throws Exception {
209+
MockExternalAccountCredentialsTransportFactory transportFactory =
210+
new MockExternalAccountCredentialsTransportFactory();
211+
InternalAwsSecurityCredentialsSupplier supplier =
212+
new InternalAwsSecurityCredentialsSupplier(
213+
buildAwsImdsv2CredentialSource(transportFactory),
214+
/* environmentProvider= */ null,
215+
/* transportFactory= */ null);
216+
217+
InternalAwsSecurityCredentialsSupplier deserialized = serializeAndDeserialize(supplier);
218+
assertNotNull(deserialized);
219+
assertSame(OAuth2Utils.HTTP_TRANSPORT_FACTORY, deserialized.getTransportFactory());
220+
}
221+
222+
/**
223+
* Verifies that {@link InternalAwsSecurityCredentialsSupplier} can be serialized and deserialized
224+
* when an explicit {@link EnvironmentProvider} is provided.
225+
*/
226+
@Test
227+
void serializeAndDeserialize_withEnvironmentVariables_success() throws Exception {
228+
MockExternalAccountCredentialsTransportFactory transportFactory =
229+
new MockExternalAccountCredentialsTransportFactory();
230+
SystemEnvironmentProvider environmentProvider = SystemEnvironmentProvider.getInstance();
231+
InternalAwsSecurityCredentialsSupplier supplier =
232+
new InternalAwsSecurityCredentialsSupplier(
233+
buildAwsImdsv2CredentialSource(transportFactory),
234+
environmentProvider,
235+
transportFactory);
236+
237+
InternalAwsSecurityCredentialsSupplier deserialized = serializeAndDeserialize(supplier);
238+
assertNotNull(deserialized);
239+
}
162240
}

0 commit comments

Comments
 (0)