Skip to content

Commit c03d5d5

Browse files
committed
fix(auth): fix serialization gaps in DownscopedCredentials and UrlIdentityPoolSubjectTokenSupplier
- Closes remaining serialization gaps tracked in #12580 where transient HttpTransportFactory fields were not restored upon deserialization, causing NullPointerExceptions during token refresh or subject token retrieval. - Implements readObject in UrlIdentityPoolSubjectTokenSupplier and DownscopedCredentials to reconstruct the transportFactory from transportFactoryClassName. - Implements Serializable in CredentialAccessBoundary and its enclosing rule and condition classes so DownscopedCredentials can be serialized. - Adds unit tests verifying serialization, deserialization, and token refresh using in-memory mock transports without network calls.
1 parent 193536d commit c03d5d5

6 files changed

Lines changed: 181 additions & 8 deletions

File tree

‎google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/CredentialAccessBoundary.java‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
import com.google.api.client.json.GenericJson;
3838
import com.google.errorprone.annotations.CanIgnoreReturnValue;
39+
import java.io.Serializable;
3940
import java.util.ArrayList;
4041
import java.util.List;
4142
import org.jspecify.annotations.NullMarked;
@@ -49,7 +50,9 @@
4950
* information.</a>
5051
*/
5152
@NullMarked
52-
public final class CredentialAccessBoundary {
53+
public final class CredentialAccessBoundary implements Serializable {
54+
55+
private static final long serialVersionUID = 1L;
5356

5457
private static final int RULES_SIZE_LIMIT = 10;
5558

@@ -63,7 +66,7 @@ public final class CredentialAccessBoundary {
6366
accessBoundaryRules.size() < RULES_SIZE_LIMIT,
6467
String.format(
6568
"The provided list has more than %s access boundary rules.", RULES_SIZE_LIMIT));
66-
this.accessBoundaryRules = accessBoundaryRules;
69+
this.accessBoundaryRules = new ArrayList<>(accessBoundaryRules);
6770
}
6871

6972
/**
@@ -161,7 +164,9 @@ public CredentialAccessBoundary build() {
161164
* .build();
162165
* </code></pre>
163166
*/
164-
public static final class AccessBoundaryRule {
167+
public static final class AccessBoundaryRule implements Serializable {
168+
169+
private static final long serialVersionUID = 1L;
165170

166171
private final String availableResource;
167172
private final List<String> availablePermissions;
@@ -293,7 +298,10 @@ public AccessBoundaryRule build() {
293298
* .build();
294299
* </code></pre>
295300
*/
296-
public static final class AvailabilityCondition {
301+
public static final class AvailabilityCondition implements Serializable {
302+
303+
private static final long serialVersionUID = 1L;
304+
297305
private final String expression;
298306

299307
@Nullable private final String title;

‎google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import com.google.common.annotations.VisibleForTesting;
4141
import com.google.errorprone.annotations.CanIgnoreReturnValue;
4242
import java.io.IOException;
43+
import java.io.ObjectInputStream;
4344
import org.jspecify.annotations.NullMarked;
4445
import org.jspecify.annotations.Nullable;
4546

@@ -99,11 +100,14 @@
99100
@NullMarked
100101
public final class DownscopedCredentials extends OAuth2Credentials {
101102

103+
private static final long serialVersionUID = 3159528426501345486L;
104+
102105
private final GoogleCredentials sourceCredential;
103106
private final CredentialAccessBoundary credentialAccessBoundary;
104107
private final String universeDomain;
108+
private final @Nullable String transportFactoryClassName;
105109

106-
private final transient HttpTransportFactory transportFactory;
110+
private transient HttpTransportFactory transportFactory;
107111

108112
private final String tokenExchangeEndpoint;
109113

@@ -113,6 +117,8 @@ private DownscopedCredentials(Builder builder) {
113117
firstNonNull(
114118
builder.transportFactory,
115119
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
120+
this.transportFactoryClassName =
121+
this.transportFactory != null ? this.transportFactory.getClass().getName() : null;
116122
this.sourceCredential = checkNotNull(builder.sourceCredential);
117123
this.credentialAccessBoundary = checkNotNull(builder.credentialAccessBoundary);
118124

@@ -199,6 +205,11 @@ HttpTransportFactory getTransportFactory() {
199205
return transportFactory;
200206
}
201207

208+
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
209+
input.defaultReadObject();
210+
transportFactory = newInstance(transportFactoryClassName);
211+
}
212+
202213
public static Builder newBuilder() {
203214
return new Builder();
204215
}

‎google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/UrlIdentityPoolSubjectTokenSupplier.java‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@
3939
import com.google.api.client.http.HttpResponse;
4040
import com.google.api.client.json.JsonObjectParser;
4141
import com.google.auth.http.HttpTransportFactory;
42+
import com.google.common.annotations.VisibleForTesting;
4243
import java.io.IOException;
44+
import java.io.ObjectInputStream;
4345
import org.jspecify.annotations.NullMarked;
46+
import org.jspecify.annotations.Nullable;
4447

4548
/**
4649
* Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} to exchange for
@@ -55,7 +58,8 @@ class UrlIdentityPoolSubjectTokenSupplier implements IdentityPoolSubjectTokenSup
5558
private static final long serialVersionUID = 4964578313468011844L;
5659

5760
private final IdentityPoolCredentialSource credentialSource;
58-
private final transient HttpTransportFactory transportFactory;
61+
private final @Nullable String transportFactoryClassName;
62+
private transient HttpTransportFactory transportFactory;
5963

6064
/**
6165
* Constructor for UrlIdentityPoolSubjectTokenProvider.
@@ -67,6 +71,8 @@ class UrlIdentityPoolSubjectTokenSupplier implements IdentityPoolSubjectTokenSup
6771
IdentityPoolCredentialSource credentialSource, HttpTransportFactory transportFactory) {
6872
this.credentialSource = credentialSource;
6973
this.transportFactory = transportFactory;
74+
this.transportFactoryClassName =
75+
this.transportFactory != null ? this.transportFactory.getClass().getName() : null;
7076
}
7177

7278
@Override
@@ -100,4 +106,14 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE
100106
String.format("Error getting subject token from metadata server: %s", e.getMessage()), e);
101107
}
102108
}
109+
110+
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
111+
input.defaultReadObject();
112+
transportFactory = OAuth2Credentials.newInstance(transportFactoryClassName);
113+
}
114+
115+
@VisibleForTesting
116+
HttpTransportFactory getTransportFactory() {
117+
return transportFactory;
118+
}
103119
}

‎google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/CredentialAccessBoundaryTest.java‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
import org.junit.jupiter.api.Test;
4444

4545
/** Tests for {@link CredentialAccessBoundary} and encompassing classes. */
46-
class CredentialAccessBoundaryTest {
46+
class CredentialAccessBoundaryTest extends BaseSerializationTest {
4747

4848
@Test
4949
void credentialAccessBoundary() {
@@ -288,4 +288,29 @@ void availabilityCondition_emptyExpression_throws() {
288288
assertThrows(IllegalArgumentException.class, builder::build);
289289
assertEquals("The provided expression is empty.", exception.getMessage());
290290
}
291+
292+
@Test
293+
void serializeAndDeserialize_success() throws Exception {
294+
AvailabilityCondition availabilityCondition =
295+
AvailabilityCondition.newBuilder()
296+
.setExpression("expression")
297+
.setTitle("title")
298+
.setDescription("description")
299+
.build();
300+
301+
AccessBoundaryRule rule =
302+
AccessBoundaryRule.newBuilder()
303+
.setAvailableResource("resource")
304+
.addAvailablePermission("permission")
305+
.setAvailabilityCondition(availabilityCondition)
306+
.build();
307+
308+
CredentialAccessBoundary cab =
309+
CredentialAccessBoundary.newBuilder().addRule(rule).build();
310+
311+
// Verify CredentialAccessBoundary and nested rules/conditions can be serialized and deserialized
312+
// so downscoped credentials containing them do not throw NotSerializableException.
313+
CredentialAccessBoundary deserialized = serializeAndDeserialize(cab);
314+
assertEquals(cab.toJson(), deserialized.toJson());
315+
}
291316
}

‎google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/DownscopedCredentialsTest.java‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
import org.junit.jupiter.api.Test;
4747

4848
/** Tests for {@link DownscopedCredentials}. */
49-
class DownscopedCredentialsTest {
49+
class DownscopedCredentialsTest extends BaseSerializationTest {
5050

5151
private static final String SA_PRIVATE_KEY_PKCS8 =
5252
"-----BEGIN PRIVATE KEY-----\n"
@@ -81,6 +81,51 @@ public HttpTransport create() {
8181
}
8282
}
8383

84+
public static class StatefulMockStsTransportFactory implements HttpTransportFactory {
85+
86+
private static MockStsTransport transport = new MockStsTransport();
87+
88+
public StatefulMockStsTransportFactory() {}
89+
90+
@Override
91+
public HttpTransport create() {
92+
return transport;
93+
}
94+
}
95+
96+
@Test
97+
void refreshAccessToken_reserialized_success() throws Exception {
98+
StatefulMockStsTransportFactory.transport = new MockStsTransport();
99+
StatefulMockStsTransportFactory transportFactory = new StatefulMockStsTransportFactory();
100+
101+
AccessToken sourceAccessToken =
102+
new AccessToken("sourceAccessToken", new Date(System.currentTimeMillis() + 3600_000));
103+
GoogleCredentials sourceCredentials =
104+
((ServiceAccountCredentials) getServiceAccountSourceCredentials(/* canRefresh= */ true))
105+
.toBuilder()
106+
.setAccessToken(sourceAccessToken)
107+
.build();
108+
109+
DownscopedCredentials downscopedCredentials =
110+
DownscopedCredentials.newBuilder()
111+
.setSourceCredential(sourceCredentials)
112+
.setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
113+
.setHttpTransportFactory(transportFactory)
114+
.build();
115+
116+
// Verify deserialization succeeds and reconstructs the transient HTTP transport factory
117+
// so that subsequent token refreshes do not throw a NullPointerException.
118+
DownscopedCredentials deserialized = serializeAndDeserialize(downscopedCredentials);
119+
assertNotNull(deserialized.getTransportFactory());
120+
assertEquals(downscopedCredentials, deserialized);
121+
assertEquals(downscopedCredentials.hashCode(), deserialized.hashCode());
122+
123+
// In-memory mock transport responds with a valid token exchange without network calls.
124+
AccessToken accessToken = deserialized.refreshAccessToken();
125+
assertEquals(
126+
StatefulMockStsTransportFactory.transport.getAccessToken(), accessToken.getTokenValue());
127+
}
128+
84129
@Test
85130
void refreshAccessToken() throws IOException {
86131
MockStsTransportFactory transportFactory = new MockStsTransportFactory();

‎google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,60 @@ void serialize() throws IOException, ClassNotFoundException {
10161016
assertSame(Clock.SYSTEM, deserializedCredentials.clock);
10171017
}
10181018

1019+
@Test
1020+
void serialize_urlSourced_refreshesSuccessfully() throws Exception {
1021+
StatefulMockExternalAccountCredentialsTransportFactory.transport =
1022+
new MockExternalAccountCredentialsTransport();
1023+
StatefulMockExternalAccountCredentialsTransportFactory transportFactory =
1024+
new StatefulMockExternalAccountCredentialsTransportFactory();
1025+
1026+
IdentityPoolCredentials testCredentials =
1027+
IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
1028+
.setHttpTransportFactory(transportFactory)
1029+
.setCredentialSource(
1030+
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
1031+
.build();
1032+
1033+
// Verify deserialization recreates the transient HTTP transport factory inside
1034+
// UrlIdentityPoolSubjectTokenSupplier so subject token retrieval and token refresh succeed.
1035+
IdentityPoolCredentials deserializedCredentials = serializeAndDeserialize(testCredentials);
1036+
assertEquals(testCredentials, deserializedCredentials);
1037+
1038+
// In-memory mock transport serves both the metadata server subject token and the STS exchange
1039+
// response without network calls.
1040+
AccessToken accessToken = deserializedCredentials.refreshAccessToken();
1041+
assertEquals("accessToken", accessToken.getTokenValue());
1042+
}
1043+
1044+
@Test
1045+
void serialize_urlIdentityPoolSubjectTokenSupplier_success() throws Exception {
1046+
StatefulMockExternalAccountCredentialsTransportFactory.transport =
1047+
new MockExternalAccountCredentialsTransport();
1048+
StatefulMockExternalAccountCredentialsTransportFactory transportFactory =
1049+
new StatefulMockExternalAccountCredentialsTransportFactory();
1050+
1051+
IdentityPoolCredentialSource credentialSource =
1052+
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl());
1053+
UrlIdentityPoolSubjectTokenSupplier supplier =
1054+
new UrlIdentityPoolSubjectTokenSupplier(credentialSource, transportFactory);
1055+
1056+
// Verify deserialization recreates the transient HTTP transport factory so that subject token
1057+
// retrieval succeeds without throwing a NullPointerException.
1058+
UrlIdentityPoolSubjectTokenSupplier deserializedSupplier = serializeAndDeserialize(supplier);
1059+
assertNotNull(deserializedSupplier.getTransportFactory());
1060+
1061+
// In-memory mock transport returns the subject token without network calls.
1062+
ExternalAccountSupplierContext context =
1063+
ExternalAccountSupplierContext.newBuilder()
1064+
.setAudience("audience")
1065+
.setSubjectTokenType("subjectTokenType")
1066+
.build();
1067+
String subjectToken = deserializedSupplier.getSubjectToken(context);
1068+
assertEquals(
1069+
StatefulMockExternalAccountCredentialsTransportFactory.transport.getSubjectToken(),
1070+
subjectToken);
1071+
}
1072+
10191073
@Test
10201074
void build_withCertificateSource_succeeds() throws Exception {
10211075
// Set up credential source for certificate type.
@@ -1276,6 +1330,20 @@ public HttpTransport create() {
12761330
}
12771331
}
12781332

1333+
public static class StatefulMockExternalAccountCredentialsTransportFactory
1334+
implements HttpTransportFactory {
1335+
1336+
private static MockExternalAccountCredentialsTransport transport =
1337+
new MockExternalAccountCredentialsTransport();
1338+
1339+
public StatefulMockExternalAccountCredentialsTransportFactory() {}
1340+
1341+
@Override
1342+
public HttpTransport create() {
1343+
return transport;
1344+
}
1345+
}
1346+
12791347
private static class TestX509Provider extends X509Provider {
12801348
private final KeyStore keyStore;
12811349
private final String certificatePath;

0 commit comments

Comments
 (0)