Skip to content

Commit 73af08a

Browse files
authored
fix: expiration time of the ImpersonatedCredentials token depending on the current host's timezone (googleapis#932)
Fix bug in `ImpersonatedCredentials` class where the date received as `expirationTime` was being incorrectly parsed, assuming the date was in the host's timezone instead of reading from the date string what timezone it was. Fixes googleapis#931 ☕️
1 parent acc1ce3 commit 73af08a

File tree

2 files changed

+102
-3
lines changed

2 files changed

+102
-3
lines changed

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.text.SimpleDateFormat;
5656
import java.util.ArrayList;
5757
import java.util.Arrays;
58+
import java.util.Calendar;
5859
import java.util.Collection;
5960
import java.util.Date;
6061
import java.util.List;
@@ -91,7 +92,7 @@ public class ImpersonatedCredentials extends GoogleCredentials
9192
implements ServiceAccountSigner, IdTokenProvider, QuotaProjectIdProvider {
9293

9394
private static final long serialVersionUID = -2133257318957488431L;
94-
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
95+
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";
9596
private static final int TWELVE_HOURS_IN_SECONDS = 43200;
9697
private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600;
9798
private static final String CLOUD_PLATFORM_SCOPE =
@@ -110,6 +111,8 @@ public class ImpersonatedCredentials extends GoogleCredentials
110111

111112
private transient HttpTransportFactory transportFactory;
112113

114+
private transient Calendar calendar;
115+
113116
/**
114117
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It
115118
* should be either a user account credential or a service account credential.
@@ -429,6 +432,25 @@ public GoogleCredentials createScoped(Collection<String> scopes) {
429432
.build();
430433
}
431434

435+
/**
436+
* Clones the impersonated credentials with a new calendar.
437+
*
438+
* @param calendar the calendar that will be used by the new ImpersonatedCredentials instance when
439+
* parsing the received expiration time of the refreshed access token
440+
* @return the cloned impersonated credentials with the given custom calendar
441+
*/
442+
public ImpersonatedCredentials createWithCustomCalendar(Calendar calendar) {
443+
return toBuilder()
444+
.setScopes(this.scopes)
445+
.setLifetime(this.lifetime)
446+
.setDelegates(this.delegates)
447+
.setHttpTransportFactory(this.transportFactory)
448+
.setQuotaProjectId(this.quotaProjectId)
449+
.setIamEndpointOverride(this.iamEndpointOverride)
450+
.setCalendar(calendar)
451+
.build();
452+
}
453+
432454
@Override
433455
protected Map<String, List<String>> getAdditionalHeaders() {
434456
Map<String, List<String>> headers = super.getAdditionalHeaders();
@@ -451,6 +473,7 @@ private ImpersonatedCredentials(Builder builder) {
451473
this.quotaProjectId = builder.quotaProjectId;
452474
this.iamEndpointOverride = builder.iamEndpointOverride;
453475
this.transportFactoryClassName = this.transportFactory.getClass().getName();
476+
this.calendar = builder.getCalendar();
454477
if (this.delegates == null) {
455478
this.delegates = new ArrayList<String>();
456479
}
@@ -512,6 +535,7 @@ public AccessToken refreshAccessToken() throws IOException {
512535
OAuth2Utils.validateString(responseData, "expireTime", "Expected to find an expireTime");
513536

514537
DateFormat format = new SimpleDateFormat(RFC3339);
538+
format.setCalendar(calendar);
515539
try {
516540
Date date = format.parse(expireTime);
517541
return new AccessToken(accessToken, date);
@@ -606,6 +630,7 @@ public static class Builder extends GoogleCredentials.Builder {
606630
private HttpTransportFactory transportFactory;
607631
private String quotaProjectId;
608632
private String iamEndpointOverride;
633+
private Calendar calendar = Calendar.getInstance();
609634

610635
protected Builder() {}
611636

@@ -678,6 +703,15 @@ public Builder setIamEndpointOverride(String iamEndpointOverride) {
678703
return this;
679704
}
680705

706+
public Builder setCalendar(Calendar calendar) {
707+
this.calendar = calendar;
708+
return this;
709+
}
710+
711+
public Calendar getCalendar() {
712+
return this.calendar;
713+
}
714+
681715
public ImpersonatedCredentials build() {
682716
return new ImpersonatedCredentials(this);
683717
}

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

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,17 @@
6060
import java.io.InputStream;
6161
import java.nio.charset.Charset;
6262
import java.security.PrivateKey;
63+
import java.text.DateFormat;
6364
import java.text.SimpleDateFormat;
65+
import java.time.temporal.ChronoUnit;
6466
import java.util.ArrayList;
6567
import java.util.Arrays;
6668
import java.util.Calendar;
6769
import java.util.Date;
6870
import java.util.List;
6971
import java.util.Map;
7072
import java.util.Set;
73+
import java.util.TimeZone;
7174
import org.junit.jupiter.api.BeforeEach;
7275
import org.junit.jupiter.api.Test;
7376

@@ -118,7 +121,7 @@ class ImpersonatedCredentialsTest extends BaseSerializationTest {
118121
private static final int INVALID_LIFETIME = 43210;
119122
private static JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
120123

121-
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
124+
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";
122125
public static final String DEFAULT_IMPERSONATION_URL =
123126
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"
124127
+ IMPERSONATED_CLIENT_EMAIL
@@ -562,6 +565,56 @@ void refreshAccessToken_delegates_success() throws IOException, IllegalStateExce
562565
assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue());
563566
}
564567

568+
@Test
569+
void refreshAccessToken_GMT_dateParsedCorrectly() throws IOException, IllegalStateException {
570+
Calendar c = Calendar.getInstance();
571+
c.add(Calendar.SECOND, VALID_LIFETIME);
572+
573+
mockTransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
574+
mockTransportFactory.transport.setAccessToken(ACCESS_TOKEN);
575+
mockTransportFactory.transport.setExpireTime(getFormattedTime(c.getTime()));
576+
ImpersonatedCredentials targetCredentials =
577+
ImpersonatedCredentials.create(
578+
sourceCredentials,
579+
IMPERSONATED_CLIENT_EMAIL,
580+
null,
581+
IMMUTABLE_SCOPES_LIST,
582+
VALID_LIFETIME,
583+
mockTransportFactory)
584+
.createWithCustomCalendar(
585+
// Set system timezone to GMT
586+
Calendar.getInstance(TimeZone.getTimeZone("GMT")));
587+
588+
assertEquals(
589+
c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli(),
590+
targetCredentials.refreshAccessToken().getExpirationTimeMillis());
591+
}
592+
593+
@Test
594+
void refreshAccessToken_nonGMT_dateParsedCorrectly() throws IOException, IllegalStateException {
595+
Calendar c = Calendar.getInstance();
596+
c.add(Calendar.SECOND, VALID_LIFETIME);
597+
598+
mockTransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
599+
mockTransportFactory.transport.setAccessToken(ACCESS_TOKEN);
600+
mockTransportFactory.transport.setExpireTime(getFormattedTime(c.getTime()));
601+
ImpersonatedCredentials targetCredentials =
602+
ImpersonatedCredentials.create(
603+
sourceCredentials,
604+
IMPERSONATED_CLIENT_EMAIL,
605+
null,
606+
IMMUTABLE_SCOPES_LIST,
607+
VALID_LIFETIME,
608+
mockTransportFactory)
609+
.createWithCustomCalendar(
610+
// Set system timezone to one different than GMT
611+
Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles")));
612+
613+
assertEquals(
614+
c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli(),
615+
targetCredentials.refreshAccessToken().getExpirationTimeMillis());
616+
}
617+
565618
@Test
566619
void refreshAccessToken_invalidDate() throws IllegalStateException {
567620

@@ -926,7 +979,19 @@ void serialize() throws IOException, ClassNotFoundException {
926979
public static String getDefaultExpireTime() {
927980
Calendar c = Calendar.getInstance();
928981
c.add(Calendar.SECOND, VALID_LIFETIME);
929-
return new SimpleDateFormat(RFC3339).format(c.getTime());
982+
return getFormattedTime(c.getTime());
983+
}
984+
985+
/**
986+
* Given a {@link Date}, it will return a string of the date formatted like
987+
* <b>yyyy-MM-dd'T'HH:mm:ss'Z'</b>
988+
*/
989+
private static String getFormattedTime(final Date date) {
990+
// Set timezone to GMT since that's the TZ used in the response from the service impersonation
991+
// token exchange
992+
final DateFormat formatter = new SimpleDateFormat(RFC3339);
993+
formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
994+
return formatter.format(date);
930995
}
931996

932997
private String generateErrorJson(

0 commit comments

Comments
 (0)