Skip to content

Commit 37a960a

Browse files
authored
Merge branch 'main' into fix-sonatype-complaints
2 parents 6453338 + 0f92593 commit 37a960a

3 files changed

Lines changed: 133 additions & 88 deletions

File tree

oauth2_http/java/com/google/auth/oauth2/IdTokenCredentials.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ public class IdTokenCredentials extends OAuth2Credentials {
105105

106106
private static final long serialVersionUID = -2133257318957588431L;
107107

108-
private IdTokenProvider idTokenProvider;
108+
private final IdTokenProvider idTokenProvider;
109+
private final List<IdTokenProvider.Option> options;
109110
private String targetAudience;
110-
private List<IdTokenProvider.Option> options;
111111

112112
private IdTokenCredentials(Builder builder) {
113113
this.idTokenProvider = Preconditions.checkNotNull(builder.getIdTokenProvider());
@@ -131,7 +131,7 @@ public IdToken getIdToken() {
131131

132132
@Override
133133
public int hashCode() {
134-
return Objects.hash(options, targetAudience);
134+
return Objects.hash(idTokenProvider, options, targetAudience);
135135
}
136136

137137
@Override
@@ -146,7 +146,8 @@ public boolean equals(Object obj) {
146146
}
147147
IdTokenCredentials other = (IdTokenCredentials) obj;
148148
return Objects.equals(this.idTokenProvider, other.idTokenProvider)
149-
&& Objects.equals(this.targetAudience, other.targetAudience);
149+
&& Objects.equals(this.targetAudience, other.targetAudience)
150+
&& Objects.equals(this.options, other.options);
150151
}
151152

152153
@Override
@@ -169,16 +170,29 @@ public static class Builder extends OAuth2Credentials.Builder {
169170

170171
protected Builder() {}
171172

173+
/**
174+
* Sets the provider for the ID token.
175+
*
176+
* @param idTokenProvider the provider for the ID token, cannot be null
177+
* @return the builder object
178+
*/
172179
@CanIgnoreReturnValue
173180
public Builder setIdTokenProvider(IdTokenProvider idTokenProvider) {
174-
this.idTokenProvider = idTokenProvider;
181+
this.idTokenProvider = Preconditions.checkNotNull(idTokenProvider);
175182
return this;
176183
}
177184

178185
public IdTokenProvider getIdTokenProvider() {
179186
return this.idTokenProvider;
180187
}
181188

189+
/**
190+
* Sets the target audience for the ID token.
191+
*
192+
* @param targetAudience the target audience, cannot be null for non-UserCredentials. If set for
193+
* UserCredentials, the value will be ignored.
194+
* @return the builder object
195+
*/
182196
@CanIgnoreReturnValue
183197
public Builder setTargetAudience(String targetAudience) {
184198
this.targetAudience = targetAudience;
@@ -189,6 +203,12 @@ public String getTargetAudience() {
189203
return this.targetAudience;
190204
}
191205

206+
/**
207+
* Sets the options for the ID token.
208+
*
209+
* @param options list of options, can be null or empty if no options are needed.
210+
* @return the builder object
211+
*/
192212
@CanIgnoreReturnValue
193213
public Builder setOptions(List<IdTokenProvider.Option> options) {
194214
this.options = options;

oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import com.google.api.client.json.JsonObjectParser;
4646
import com.google.api.client.util.GenericData;
4747
import com.google.api.core.InternalApi;
48+
import com.google.api.core.ObsoleteApi;
4849
import com.google.auth.CredentialTypeForMetrics;
4950
import com.google.auth.ServiceAccountSigner;
5051
import com.google.auth.http.HttpCredentialsAdapter;
@@ -59,9 +60,9 @@
5960
import java.io.IOException;
6061
import java.io.InputStream;
6162
import java.io.ObjectInputStream;
62-
import java.text.DateFormat;
63-
import java.text.ParseException;
64-
import java.text.SimpleDateFormat;
63+
import java.time.DateTimeException;
64+
import java.time.Instant;
65+
import java.time.format.DateTimeFormatter;
6566
import java.util.ArrayList;
6667
import java.util.Calendar;
6768
import java.util.Collection;
@@ -101,7 +102,6 @@ public class ImpersonatedCredentials extends GoogleCredentials
101102
implements ServiceAccountSigner, IdTokenProvider {
102103

103104
private static final long serialVersionUID = -2133257318957488431L;
104-
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";
105105
private static final int TWELVE_HOURS_IN_SECONDS = 43200;
106106
private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600;
107107
private GoogleCredentials sourceCredentials;
@@ -510,12 +510,16 @@ public CredentialTypeForMetrics getMetricsCredentialType() {
510510
}
511511

512512
/**
513-
* Clones the impersonated credentials with a new calendar.
513+
* This method is marked obsolete. There is no alternative to setting a custom calendar for the
514+
* Credential.
515+
*
516+
* <p>Clones the impersonated credentials with a new calendar.
514517
*
515518
* @param calendar the calendar that will be used by the new ImpersonatedCredentials instance when
516519
* parsing the received expiration time of the refreshed access token
517520
* @return the cloned impersonated credentials with the given custom calendar
518521
*/
522+
@ObsoleteApi("This method is obsolete and will be removed in a future release.")
519523
public ImpersonatedCredentials createWithCustomCalendar(Calendar calendar) {
520524
return toBuilder()
521525
.setScopes(this.scopes)
@@ -660,14 +664,23 @@ public AccessToken refreshAccessToken() throws IOException {
660664
String expireTime =
661665
OAuth2Utils.validateString(responseData, "expireTime", "Expected to find an expireTime");
662666

663-
DateFormat format = new SimpleDateFormat(RFC3339);
664-
format.setCalendar(calendar);
667+
Instant expirationInstant;
665668
try {
666-
Date date = format.parse(expireTime);
667-
return new AccessToken(accessToken, date);
668-
} catch (ParseException pe) {
669-
throw new IOException("Error parsing expireTime: " + pe.getMessage());
669+
if (calendar != null) {
670+
// For backward compatibility, if a custom calendar is set, use its timezone
671+
// and convert it to an Instant
672+
expirationInstant =
673+
Instant.from(
674+
DateTimeFormatter.ISO_INSTANT
675+
.withZone(calendar.getTimeZone().toZoneId())
676+
.parse(expireTime));
677+
} else {
678+
expirationInstant = Instant.parse(expireTime);
679+
}
680+
} catch (DateTimeException e) {
681+
throw new IOException("Error parsing expireTime: " + expireTime, e);
670682
}
683+
return new AccessToken(accessToken, Date.from(expirationInstant));
671684
}
672685

673686
/**
@@ -883,7 +896,17 @@ public Builder setIamEndpointOverride(String iamEndpointOverride) {
883896
return this;
884897
}
885898

899+
/**
900+
* This method is marked obsolete. There is no alternative to setting a custom calendar for the
901+
* Credential.
902+
*
903+
* <p>Sets the calendar to be used for parsing the expiration time.
904+
*
905+
* @param calendar the calendar to use
906+
* @return the builder
907+
*/
886908
@CanIgnoreReturnValue
909+
@ObsoleteApi("This method is obsolete and will be removed in a future release.")
887910
public Builder setCalendar(Calendar calendar) {
888911
this.calendar = calendar;
889912
return this;
@@ -903,6 +926,15 @@ public Builder setReadTimeout(int readTimeout) {
903926
return this;
904927
}
905928

929+
/**
930+
* This method is marked obsolete. There is no alternative to getting a custom calendar for the
931+
* Credential.
932+
*
933+
* <p>Returns the calendar to be used for parsing the expiration time.
934+
*
935+
* @return the calendar
936+
*/
937+
@ObsoleteApi("This method is obsolete and will be removed in a future release.")
906938
public Calendar getCalendar() {
907939
return this.calendar;
908940
}

oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java

Lines changed: 65 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import static org.mockito.Mockito.when;
4545

4646
import com.google.api.client.http.HttpStatusCodes;
47+
import com.google.api.client.http.HttpTransport;
4748
import com.google.api.client.json.GenericJson;
4849
import com.google.api.client.json.JsonFactory;
4950
import com.google.api.client.json.JsonGenerator;
@@ -55,23 +56,22 @@
5556
import com.google.auth.Credentials;
5657
import com.google.auth.ServiceAccountSigner.SigningException;
5758
import com.google.auth.TestUtils;
59+
import com.google.auth.http.HttpTransportFactory;
5860
import com.google.common.collect.ImmutableList;
5961
import com.google.common.collect.ImmutableSet;
6062
import java.io.ByteArrayOutputStream;
6163
import java.io.IOException;
6264
import java.io.InputStream;
6365
import java.nio.charset.Charset;
6466
import java.security.PrivateKey;
65-
import java.text.DateFormat;
66-
import java.text.SimpleDateFormat;
67+
import java.time.Instant;
6768
import java.time.temporal.ChronoUnit;
6869
import java.util.ArrayList;
6970
import java.util.Arrays;
7071
import java.util.Calendar;
7172
import java.util.Date;
7273
import java.util.List;
7374
import java.util.Map;
74-
import java.util.TimeZone;
7575
import org.junit.jupiter.api.BeforeEach;
7676
import org.junit.jupiter.api.Test;
7777

@@ -123,8 +123,6 @@ class ImpersonatedCredentialsTest extends BaseSerializationTest {
123123
private static final int INVALID_LIFETIME = 43210;
124124
private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
125125

126-
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";
127-
128126
private static final String TEST_UNIVERSE_DOMAIN = "test.xyz";
129127
public static final String DEFAULT_IMPERSONATION_URL =
130128
String.format(
@@ -631,61 +629,9 @@ void refreshAccessToken_delegates_success() throws IOException, IllegalStateExce
631629
assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue());
632630
}
633631

634-
@Test
635-
void refreshAccessToken_GMT_dateParsedCorrectly() throws IOException, IllegalStateException {
636-
Calendar c = Calendar.getInstance();
637-
c.add(Calendar.SECOND, VALID_LIFETIME);
638-
639-
mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
640-
mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN);
641-
mockTransportFactory.getTransport().setExpireTime(getFormattedTime(c.getTime()));
642-
mockTransportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
643-
ImpersonatedCredentials targetCredentials =
644-
ImpersonatedCredentials.create(
645-
sourceCredentials,
646-
IMPERSONATED_CLIENT_EMAIL,
647-
null,
648-
IMMUTABLE_SCOPES_LIST,
649-
VALID_LIFETIME,
650-
mockTransportFactory)
651-
.createWithCustomCalendar(
652-
// Set system timezone to GMT
653-
Calendar.getInstance(TimeZone.getTimeZone("GMT")));
654-
655-
assertEquals(
656-
c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli(),
657-
(long) targetCredentials.refreshAccessToken().getExpirationTimeMillis());
658-
}
659-
660-
@Test
661-
void refreshAccessToken_nonGMT_dateParsedCorrectly() throws IOException, IllegalStateException {
662-
Calendar c = Calendar.getInstance();
663-
c.add(Calendar.SECOND, VALID_LIFETIME);
664-
665-
mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
666-
mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN);
667-
mockTransportFactory.getTransport().setExpireTime(getFormattedTime(c.getTime()));
668-
mockTransportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
669-
ImpersonatedCredentials targetCredentials =
670-
ImpersonatedCredentials.create(
671-
sourceCredentials,
672-
IMPERSONATED_CLIENT_EMAIL,
673-
null,
674-
IMMUTABLE_SCOPES_LIST,
675-
VALID_LIFETIME,
676-
mockTransportFactory)
677-
.createWithCustomCalendar(
678-
// Set system timezone to one different than GMT
679-
Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles")));
680-
681-
assertEquals(
682-
c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli(),
683-
(long) targetCredentials.refreshAccessToken().getExpirationTimeMillis());
684-
}
685-
686632
@Test
687633
void refreshAccessToken_invalidDate() throws IllegalStateException {
688-
String expectedMessage = "Unparseable date";
634+
String expectedMessage = "Error parsing expireTime: ";
689635
mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
690636
mockTransportFactory.getTransport().setAccessToken("foo");
691637
mockTransportFactory.getTransport().setExpireTime("1973-09-29T15:01:23");
@@ -1253,22 +1199,69 @@ void serialize() throws IOException, ClassNotFoundException {
12531199
assertSame(Clock.SYSTEM, deserializedCredentials.clock);
12541200
}
12551201

1256-
public static String getDefaultExpireTime() {
1257-
Calendar c = Calendar.getInstance();
1258-
c.add(Calendar.SECOND, VALID_LIFETIME);
1259-
return getFormattedTime(c.getTime());
1260-
}
1261-
12621202
/**
1263-
* Given a {@link Date}, it will return a string of the date formatted like
1264-
* <b>yyyy-MM-dd'T'HH:mm:ss'Z'</b>
1203+
* A stateful {@link HttpTransportFactory} that provides a shared {@link
1204+
* MockIAMCredentialsServiceTransport} instance.
1205+
*
1206+
* <p>This is necessary for serialization tests because {@link ImpersonatedCredentials} stores the
1207+
* factory's class name and re-instantiates it via reflection during deserialization. A standard
1208+
* factory would create a fresh, unconfigured transport upon re-instantiation, causing refreshed
1209+
* token requests to fail. Using a static transport ensures the mock configuration persists across
1210+
* serialization boundaries.
12651211
*/
1266-
private static String getFormattedTime(final Date date) {
1267-
// Set timezone to GMT since that's the TZ used in the response from the service impersonation
1268-
// token exchange
1269-
final DateFormat formatter = new SimpleDateFormat(RFC3339);
1270-
formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
1271-
return formatter.format(date);
1212+
public static class StatefulMockIAMTransportFactory implements HttpTransportFactory {
1213+
private static final MockIAMCredentialsServiceTransport TRANSPORT =
1214+
new MockIAMCredentialsServiceTransport(GoogleCredentials.GOOGLE_DEFAULT_UNIVERSE);
1215+
1216+
@Override
1217+
public HttpTransport create() {
1218+
return TRANSPORT;
1219+
}
1220+
1221+
public static MockIAMCredentialsServiceTransport getTransport() {
1222+
return TRANSPORT;
1223+
}
1224+
}
1225+
1226+
@Test
1227+
void refreshAccessToken_afterSerialization_success() throws IOException, ClassNotFoundException {
1228+
// This test ensures that credentials can still refresh after being serialized.
1229+
// ImpersonatedCredentials only serializes the transport factory's class name.
1230+
// Upon deserialization, it creates a new instance of that factory via reflection.
1231+
// StatefulMockIAMTransportFactory uses a static transport instance so that the
1232+
// configuration we set here (token, expiration) is available to the new factory instance.
1233+
MockIAMCredentialsServiceTransport transport = StatefulMockIAMTransportFactory.getTransport();
1234+
transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
1235+
transport.setAccessToken(ACCESS_TOKEN);
1236+
1237+
transport.setExpireTime(getDefaultExpireTime());
1238+
transport.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "", true);
1239+
1240+
// Use a source credential that doesn't need refresh
1241+
AccessToken sourceToken =
1242+
new AccessToken("source-token", new Date(System.currentTimeMillis() + 3600000));
1243+
GoogleCredentials sourceCredentials = GoogleCredentials.create(sourceToken);
1244+
1245+
ImpersonatedCredentials targetCredentials =
1246+
ImpersonatedCredentials.create(
1247+
sourceCredentials,
1248+
IMPERSONATED_CLIENT_EMAIL,
1249+
null,
1250+
IMMUTABLE_SCOPES_LIST,
1251+
VALID_LIFETIME,
1252+
new StatefulMockIAMTransportFactory());
1253+
1254+
ImpersonatedCredentials deserializedCredentials = serializeAndDeserialize(targetCredentials);
1255+
1256+
// This should not throw NPE. The transient 'calendar' field being null after
1257+
// deserialization is now handled by using java.time.Instant for parsing.
1258+
AccessToken token = deserializedCredentials.refreshAccessToken();
1259+
assertNotNull(token);
1260+
assertEquals(ACCESS_TOKEN, token.getTokenValue());
1261+
}
1262+
1263+
public static String getDefaultExpireTime() {
1264+
return Instant.now().plusSeconds(VALID_LIFETIME).truncatedTo(ChronoUnit.SECONDS).toString();
12721265
}
12731266

12741267
private String generateErrorJson(

0 commit comments

Comments
 (0)