Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
import com.google.api.core.InternalApi;
import com.google.api.core.ObsoleteApi;
import com.google.auth.CredentialTypeForMetrics;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.HttpCredentialsAdapter;
Expand All @@ -59,9 +60,9 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
Expand Down Expand Up @@ -101,7 +102,6 @@
implements ServiceAccountSigner, IdTokenProvider {

private static final long serialVersionUID = -2133257318957488431L;
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";
private static final int TWELVE_HOURS_IN_SECONDS = 43200;
private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600;
private GoogleCredentials sourceCredentials;
Expand Down Expand Up @@ -461,7 +461,7 @@
sourceCredentialsType = (String) sourceCredentialsJson.get("type");
quotaProjectId = (String) json.get("quota_project_id");
targetPrincipal = extractTargetPrincipal(serviceAccountImpersonationUrl);
if (json.containsKey("scopes")) {

Check failure on line 464 in oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "scopes" 3 times.

See more on https://sonarcloud.io/project/issues?id=googleapis_google-auth-library-java&issues=AZ0hzzEHvoD1eC86mCFI&open=AZ0hzzEHvoD1eC86mCFI&pullRequest=1908
scopes = ImmutableList.copyOf((List<String>) json.get("scopes"));
}
} catch (ClassCastException | NullPointerException | IllegalArgumentException e) {
Expand Down Expand Up @@ -510,12 +510,16 @@
}

/**
* Clones the impersonated credentials with a new calendar.
* This method is marked obsolete. There is no alternative to setting a custom calendar for the
* Credential.
*
* <p>Clones the impersonated credentials with a new calendar.
*
* @param calendar the calendar that will be used by the new ImpersonatedCredentials instance when
* parsing the received expiration time of the refreshed access token
* @return the cloned impersonated credentials with the given custom calendar
*/
@ObsoleteApi("This method is obsolete and will be removed in a future release.")
public ImpersonatedCredentials createWithCustomCalendar(Calendar calendar) {
return toBuilder()
.setScopes(this.scopes)
Expand Down Expand Up @@ -660,14 +664,23 @@
String expireTime =
OAuth2Utils.validateString(responseData, "expireTime", "Expected to find an expireTime");

DateFormat format = new SimpleDateFormat(RFC3339);
format.setCalendar(calendar);
Instant expirationInstant;
try {
Date date = format.parse(expireTime);
return new AccessToken(accessToken, date);
} catch (ParseException pe) {
throw new IOException("Error parsing expireTime: " + pe.getMessage());
if (calendar != null) {
// For backward compatibility, if a custom calendar is set, use its timezone
// and convert it to an Instant
expirationInstant =
Instant.from(
DateTimeFormatter.ISO_INSTANT
.withZone(calendar.getTimeZone().toZoneId())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC the only effect of the calendar parameter is to change the formatter's timezone. Does that affect the effective expiration instant in the end, or is it just format?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should not affect the effective expire time. The expireTime represents a moment in time, however, it may be a moment of time represented in a different time zone. This change is to ensure that we do not assume that it is always in Zulu time (e.g. account for offsets like +02:00)

.parse(expireTime));
} else {
expirationInstant = Instant.parse(expireTime);
}
} catch (DateTimeException e) {
throw new IOException("Error parsing expireTime: " + expireTime, e);
}
return new AccessToken(accessToken, Date.from(expirationInstant));
}

/**
Expand Down Expand Up @@ -883,7 +896,17 @@
return this;
}

/**
* This method is marked obsolete. There is no alternative to setting a custom calendar for the
* Credential.
*
* <p>Sets the calendar to be used for parsing the expiration time.
*
* @param calendar the calendar to use
* @return the builder
*/
@CanIgnoreReturnValue
@ObsoleteApi("This method is obsolete and will be removed in a future release.")
public Builder setCalendar(Calendar calendar) {
this.calendar = calendar;
return this;
Expand All @@ -903,6 +926,15 @@
return this;
}

/**
* This method is marked obsolete. There is no alternative to getting a custom calendar for the
* Credential.
*
* <p>Returns the calendar to be used for parsing the expiration time.
*
* @return the calendar
*/
@ObsoleteApi("This method is obsolete and will be removed in a future release.")
public Calendar getCalendar() {
return this.calendar;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import static org.mockito.Mockito.when;

import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonGenerator;
Expand All @@ -54,23 +55,22 @@
import com.google.auth.Credentials;
import com.google.auth.ServiceAccountSigner.SigningException;
import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.PrivateKey;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -122,8 +122,6 @@
private static final int INVALID_LIFETIME = 43210;
private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();

private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";

private static final String TEST_UNIVERSE_DOMAIN = "test.xyz";
private static final String OLD_IMPERSONATION_URL =
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"
Expand Down Expand Up @@ -480,7 +478,7 @@
}

@Test()
void credential_with_invalid_scope() throws IOException, IllegalStateException {

Check warning on line 481 in oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the declaration of thrown exception 'java.io.IOException', as it cannot be thrown from method's body.

See more on https://sonarcloud.io/project/issues?id=googleapis_google-auth-library-java&issues=AZ0hzzBevoD1eC86mCFD&open=AZ0hzzBevoD1eC86mCFD&pullRequest=1908
assertThrows(
NullPointerException.class,
() ->
Expand Down Expand Up @@ -653,61 +651,9 @@
assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue());
}

@Test
void refreshAccessToken_GMT_dateParsedCorrectly() throws IOException, IllegalStateException {
Calendar c = Calendar.getInstance();
c.add(Calendar.SECOND, VALID_LIFETIME);

mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN);
mockTransportFactory.getTransport().setExpireTime(getFormattedTime(c.getTime()));
mockTransportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
Comment on lines -656 to -664
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed old tests that reference the obsolete api (setting custom calendar)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq: Are there still use cases where calendar may be used without serializing and deserializing the credentials?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not as far as I know. A code search through the repo shows that it's really only used for computing the expire time with the correct timezone offset.

ImpersonatedCredentials targetCredentials =
ImpersonatedCredentials.create(
sourceCredentials,
IMPERSONATED_CLIENT_EMAIL,
null,
IMMUTABLE_SCOPES_LIST,
VALID_LIFETIME,
mockTransportFactory)
.createWithCustomCalendar(
// Set system timezone to GMT
Calendar.getInstance(TimeZone.getTimeZone("GMT")));

assertTrue(
c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli()
== targetCredentials.refreshAccessToken().getExpirationTimeMillis());
}

@Test
void refreshAccessToken_nonGMT_dateParsedCorrectly() throws IOException, IllegalStateException {
Calendar c = Calendar.getInstance();
c.add(Calendar.SECOND, VALID_LIFETIME);

mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN);
mockTransportFactory.getTransport().setExpireTime(getFormattedTime(c.getTime()));
mockTransportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
ImpersonatedCredentials targetCredentials =
ImpersonatedCredentials.create(
sourceCredentials,
IMPERSONATED_CLIENT_EMAIL,
null,
IMMUTABLE_SCOPES_LIST,
VALID_LIFETIME,
mockTransportFactory)
.createWithCustomCalendar(
// Set system timezone to one different than GMT
Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles")));

assertTrue(
c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli()
== targetCredentials.refreshAccessToken().getExpirationTimeMillis());
}

@Test
void refreshAccessToken_invalidDate() throws IllegalStateException {
String expectedMessage = "Unparseable date";
String expectedMessage = "Error parsing expireTime: ";
mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
mockTransportFactory.getTransport().setAccessToken("foo");
mockTransportFactory.getTransport().setExpireTime("1973-09-29T15:01:23");
Expand Down Expand Up @@ -1169,7 +1115,7 @@
}

@Test
void universeDomain_whenExplicit_notAllowedIfNotMatchToSourceUD() throws IOException {

Check warning on line 1118 in oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the declaration of thrown exception 'java.io.IOException', as it cannot be thrown from method's body.

See more on https://sonarcloud.io/project/issues?id=googleapis_google-auth-library-java&issues=AZ0hzzBevoD1eC86mCFE&open=AZ0hzzBevoD1eC86mCFE&pullRequest=1908
GoogleCredentials sourceCredentialsNonGDU =
sourceCredentials.toBuilder().setUniverseDomain("source.domain.xyz").build();
IllegalStateException illegalStateException =
Expand Down Expand Up @@ -1249,7 +1195,7 @@
}

@Test
void hashCode_equals() throws IOException {

Check warning on line 1198 in oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the declaration of thrown exception 'java.io.IOException', as it cannot be thrown from method's body.

See more on https://sonarcloud.io/project/issues?id=googleapis_google-auth-library-java&issues=AZ0hzzBevoD1eC86mCFF&open=AZ0hzzBevoD1eC86mCFF&pullRequest=1908
mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN);
mockTransportFactory.getTransport().setExpireTime(getDefaultExpireTime());
Expand Down Expand Up @@ -1297,22 +1243,69 @@
assertSame(deserializedCredentials.clock, Clock.SYSTEM);
}

public static String getDefaultExpireTime() {
Calendar c = Calendar.getInstance();
c.add(Calendar.SECOND, VALID_LIFETIME);
return getFormattedTime(c.getTime());
}

/**
* Given a {@link Date}, it will return a string of the date formatted like
* <b>yyyy-MM-dd'T'HH:mm:ss'Z'</b>
* A stateful {@link HttpTransportFactory} that provides a shared {@link
* MockIAMCredentialsServiceTransport} instance.
*
* <p>This is necessary for serialization tests because {@link ImpersonatedCredentials} stores the
* factory's class name and re-instantiates it via reflection during deserialization. A standard
* factory would create a fresh, unconfigured transport upon re-instantiation, causing refreshed
* token requests to fail. Using a static transport ensures the mock configuration persists across
* serialization boundaries.
*/
private static String getFormattedTime(final Date date) {
// Set timezone to GMT since that's the TZ used in the response from the service impersonation
// token exchange
final DateFormat formatter = new SimpleDateFormat(RFC3339);
formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
return formatter.format(date);
public static class StatefulMockIAMTransportFactory implements HttpTransportFactory {
private static final MockIAMCredentialsServiceTransport TRANSPORT =
new MockIAMCredentialsServiceTransport(GoogleCredentials.GOOGLE_DEFAULT_UNIVERSE);

@Override
public HttpTransport create() {
return TRANSPORT;
}

public static MockIAMCredentialsServiceTransport getTransport() {
return TRANSPORT;
}
}

@Test
void refreshAccessToken_afterSerialization_success() throws IOException, ClassNotFoundException {
// This test ensures that credentials can still refresh after being serialized.
// ImpersonatedCredentials only serializes the transport factory's class name.
// Upon deserialization, it creates a new instance of that factory via reflection.
// StatefulMockIAMTransportFactory uses a static transport instance so that the
// configuration we set here (token, expiration) is available to the new factory instance.
MockIAMCredentialsServiceTransport transport = StatefulMockIAMTransportFactory.getTransport();
transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
transport.setAccessToken(ACCESS_TOKEN);

transport.setExpireTime(getDefaultExpireTime());
transport.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "", true);

// Use a source credential that doesn't need refresh
AccessToken sourceToken =
new AccessToken("source-token", new Date(System.currentTimeMillis() + 3600000));
GoogleCredentials sourceCredentials = GoogleCredentials.create(sourceToken);

Check warning on line 1287 in oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "sourceCredentials" which hides the field declared at line 152.

See more on https://sonarcloud.io/project/issues?id=googleapis_google-auth-library-java&issues=AZ0hzzBevoD1eC86mCFG&open=AZ0hzzBevoD1eC86mCFG&pullRequest=1908

ImpersonatedCredentials targetCredentials =
ImpersonatedCredentials.create(
sourceCredentials,
IMPERSONATED_CLIENT_EMAIL,
null,
IMMUTABLE_SCOPES_LIST,
VALID_LIFETIME,
new StatefulMockIAMTransportFactory());

ImpersonatedCredentials deserializedCredentials = serializeAndDeserialize(targetCredentials);

// This should not throw NPE. The transient 'calendar' field being null after
// deserialization is now handled by using java.time.Instant for parsing.
AccessToken token = deserializedCredentials.refreshAccessToken();
assertNotNull(token);
assertEquals(ACCESS_TOKEN, token.getTokenValue());
}

public static String getDefaultExpireTime() {
return Instant.now().plusSeconds(VALID_LIFETIME).truncatedTo(ChronoUnit.SECONDS).toString();
}

private String generateErrorJson(
Expand Down
Loading