Skip to content

Commit

Permalink
feat: allow set lifetime for service account creds (#516)
Browse files Browse the repository at this point in the history
* feat: allow set lifetime for service account creds

* update

* update name

* update

* update

* change lifetime 0 to default

* update

* update

Co-authored-by: Jeff Ching <chingor@google.com>
  • Loading branch information
arithmetic1728 and chingor13 committed Jan 12, 2021
1 parent af21727 commit 427f2d5
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 10 deletions.
Expand Up @@ -93,6 +93,8 @@ public class ServiceAccountCredentials extends GoogleCredentials
private static final long serialVersionUID = 7807543542681217978L;
private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
private static final int TWELVE_HOURS_IN_SECONDS = 43200;
private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600;

private final String clientId;
private final String clientEmail;
Expand All @@ -104,6 +106,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
private final URI tokenServerUri;
private final Collection<String> scopes;
private final String quotaProjectId;
private final int lifetime;

private transient HttpTransportFactory transportFactory;

Expand All @@ -123,6 +126,10 @@ public class ServiceAccountCredentials extends GoogleCredentials
* authority to the service account.
* @param projectId the project used for billing
* @param quotaProjectId The project used for quota and billing purposes. May be null.
* @param lifetime number of seconds the access token should be valid for. The value should be at
* most 43200 (12 hours). If the token is used for calling a Google API, then the value should
* be at most 3600 (1 hour). If the given value is 0, then the default value 3600 will be used
* when creating the credentials.
*/
ServiceAccountCredentials(
String clientId,
Expand All @@ -134,7 +141,8 @@ public class ServiceAccountCredentials extends GoogleCredentials
URI tokenServerUri,
String serviceAccountUser,
String projectId,
String quotaProjectId) {
String quotaProjectId,
int lifetime) {
this.clientId = clientId;
this.clientEmail = Preconditions.checkNotNull(clientEmail);
this.privateKey = Preconditions.checkNotNull(privateKey);
Expand All @@ -149,6 +157,10 @@ public class ServiceAccountCredentials extends GoogleCredentials
this.serviceAccountUser = serviceAccountUser;
this.projectId = projectId;
this.quotaProjectId = quotaProjectId;
if (lifetime > TWELVE_HOURS_IN_SECONDS) {
throw new IllegalStateException("lifetime must be less than or equal to 43200");
}
this.lifetime = lifetime;
}

/**
Expand Down Expand Up @@ -325,7 +337,8 @@ static ServiceAccountCredentials fromPkcs8(
tokenServerUri,
serviceAccountUser,
projectId,
quotaProject);
quotaProject,
DEFAULT_LIFETIME_IN_SECONDS);
}

/** Helper to convert from a PKCS#8 String to an RSA private key */
Expand Down Expand Up @@ -513,7 +526,21 @@ public GoogleCredentials createScoped(Collection<String> newScopes) {
tokenServerUri,
serviceAccountUser,
projectId,
quotaProjectId);
quotaProjectId,
lifetime);
}

/**
* Clones the service account with a new lifetime value.
*
* @param lifetime life time value in seconds. The value should be at most 43200 (12 hours). If
* the token is used for calling a Google API, then the value should be at most 3600 (1 hour).
* If the given value is 0, then the default value 3600 will be used when creating the
* credentials.
* @return the cloned service account credentials with the given custom life time
*/
public ServiceAccountCredentials createWithCustomLifetime(int lifetime) {
return this.toBuilder().setLifetime(lifetime).build();
}

@Override
Expand All @@ -528,7 +555,8 @@ public GoogleCredentials createDelegated(String user) {
tokenServerUri,
user,
projectId,
quotaProjectId);
quotaProjectId,
lifetime);
}

public final String getClientId() {
Expand Down Expand Up @@ -563,6 +591,11 @@ public final URI getTokenServerUri() {
return tokenServerUri;
}

@VisibleForTesting
int getLifetime() {
return lifetime;
}

@Override
public String getAccount() {
return getClientEmail();
Expand Down Expand Up @@ -618,7 +651,8 @@ public int hashCode() {
transportFactoryClassName,
tokenServerUri,
scopes,
quotaProjectId);
quotaProjectId,
lifetime);
}

@Override
Expand All @@ -632,6 +666,7 @@ public String toString() {
.add("scopes", scopes)
.add("serviceAccountUser", serviceAccountUser)
.add("quotaProjectId", quotaProjectId)
.add("lifetime", lifetime)
.toString();
}

Expand All @@ -648,7 +683,8 @@ public boolean equals(Object obj) {
&& Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
&& Objects.equals(this.tokenServerUri, other.tokenServerUri)
&& Objects.equals(this.scopes, other.scopes)
&& Objects.equals(this.quotaProjectId, other.quotaProjectId);
&& Objects.equals(this.quotaProjectId, other.quotaProjectId)
&& Objects.equals(this.lifetime, other.lifetime);
}

String createAssertion(JsonFactory jsonFactory, long currentTime, String audience)
Expand All @@ -661,7 +697,7 @@ String createAssertion(JsonFactory jsonFactory, long currentTime, String audienc
JsonWebToken.Payload payload = new JsonWebToken.Payload();
payload.setIssuer(clientEmail);
payload.setIssuedAtTimeSeconds(currentTime / 1000);
payload.setExpirationTimeSeconds(currentTime / 1000 + 3600);
payload.setExpirationTimeSeconds(currentTime / 1000 + this.lifetime);
payload.setSubject(serviceAccountUser);
payload.put("scope", Joiner.on(' ').join(scopes));

Expand Down Expand Up @@ -693,7 +729,7 @@ String createAssertionForIdToken(
JsonWebToken.Payload payload = new JsonWebToken.Payload();
payload.setIssuer(clientEmail);
payload.setIssuedAtTimeSeconds(currentTime / 1000);
payload.setExpirationTimeSeconds(currentTime / 1000 + 3600);
payload.setExpirationTimeSeconds(currentTime / 1000 + this.lifetime);
payload.setSubject(serviceAccountUser);

if (audience == null) {
Expand Down Expand Up @@ -746,6 +782,7 @@ public static class Builder extends GoogleCredentials.Builder {
private Collection<String> scopes;
private HttpTransportFactory transportFactory;
private String quotaProjectId;
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS;

protected Builder() {}

Expand All @@ -760,6 +797,7 @@ protected Builder(ServiceAccountCredentials credentials) {
this.serviceAccountUser = credentials.serviceAccountUser;
this.projectId = credentials.projectId;
this.quotaProjectId = credentials.quotaProjectId;
this.lifetime = credentials.lifetime;
}

public Builder setClientId(String clientId) {
Expand Down Expand Up @@ -812,6 +850,11 @@ public Builder setQuotaProjectId(String quotaProjectId) {
return this;
}

public Builder setLifetime(int lifetime) {
this.lifetime = lifetime == 0 ? DEFAULT_LIFETIME_IN_SECONDS : lifetime;
return this;
}

public String getClientId() {
return clientId;
}
Expand Down Expand Up @@ -852,6 +895,10 @@ public String getQuotaProjectId() {
return quotaProjectId;
}

public int getLifetime() {
return lifetime;
}

public ServiceAccountCredentials build() {
return new ServiceAccountCredentials(
clientId,
Expand All @@ -863,7 +910,8 @@ public ServiceAccountCredentials build() {
tokenServerUri,
serviceAccountUser,
projectId,
quotaProjectId);
quotaProjectId,
lifetime);
}
}
}
Expand Up @@ -111,6 +111,54 @@ public class ServiceAccountCredentialsTest extends BaseSerializationTest {
+ "aXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwNzA4NTY4In0"
+ ".redacted";
private static final String QUOTA_PROJECT = "sample-quota-project-id";
private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600;
private static final int INVALID_LIFETIME = 43210;

private ServiceAccountCredentials.Builder createDefaultBuilder() throws IOException {
PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(PRIVATE_KEY_PKCS8);
return ServiceAccountCredentials.newBuilder()
.setClientId(CLIENT_ID)
.setClientEmail(CLIENT_EMAIL)
.setPrivateKey(privateKey)
.setPrivateKeyId(PRIVATE_KEY_ID)
.setScopes(SCOPES)
.setServiceAccountUser(USER)
.setProjectId(PROJECT_ID);
}

@Test
public void setLifetime() throws IOException {
ServiceAccountCredentials.Builder builder = createDefaultBuilder();
assertEquals(DEFAULT_LIFETIME_IN_SECONDS, builder.getLifetime());
assertEquals(DEFAULT_LIFETIME_IN_SECONDS, builder.build().getLifetime());

builder.setLifetime(4000);
assertEquals(4000, builder.getLifetime());
assertEquals(4000, builder.build().getLifetime());

builder.setLifetime(0);
assertEquals(DEFAULT_LIFETIME_IN_SECONDS, builder.build().getLifetime());
}

@Test
public void setLifetime_invalid_lifetime() throws IOException, IllegalStateException {
try {
createDefaultBuilder().setLifetime(INVALID_LIFETIME).build();
fail(
String.format(
"Should throw exception with message containing '%s'",
"lifetime must be less than or equal to 43200"));
} catch (IllegalStateException expected) {
assertTrue(expected.getMessage().contains("lifetime must be less than or equal to 43200"));
}
}

@Test
public void createWithCustomLifetime() throws IOException {
ServiceAccountCredentials credentials = createDefaultBuilder().build();
credentials = credentials.createWithCustomLifetime(4000);
assertEquals(4000, credentials.getLifetime());
}

@Test
public void createdScoped_clones() throws IOException {
Expand Down Expand Up @@ -202,6 +250,19 @@ public void createAssertion_correct() throws IOException {
assertEquals(Joiner.on(' ').join(scopes), payload.get("scope"));
}

@Test
public void createAssertion_custom_lifetime() throws IOException {
ServiceAccountCredentials credentials = createDefaultBuilder().setLifetime(4000).build();

JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
long currentTimeMillis = Clock.SYSTEM.currentTimeMillis();
String assertion = credentials.createAssertion(jsonFactory, currentTimeMillis, null);

JsonWebSignature signature = JsonWebSignature.parse(jsonFactory, assertion);
JsonWebToken.Payload payload = signature.getPayload();
assertEquals(currentTimeMillis / 1000 + 4000, (long) payload.getExpirationTimeSeconds());
}

@Test
public void createAssertionForIdToken_correct() throws IOException {

Expand Down Expand Up @@ -231,6 +292,22 @@ public void createAssertionForIdToken_correct() throws IOException {
assertEquals(USER, payload.getSubject());
}

@Test
public void createAssertionForIdToken_custom_lifetime() throws IOException {

ServiceAccountCredentials credentials = createDefaultBuilder().setLifetime(4000).build();

JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
long currentTimeMillis = Clock.SYSTEM.currentTimeMillis();
String assertion =
credentials.createAssertionForIdToken(
jsonFactory, currentTimeMillis, null, "https://foo.com/bar");

JsonWebSignature signature = JsonWebSignature.parse(jsonFactory, assertion);
JsonWebToken.Payload payload = signature.getPayload();
assertEquals(currentTimeMillis / 1000 + 4000, (long) payload.getExpirationTimeSeconds());
}

@Test
public void createAssertionForIdToken_incorrect() throws IOException {

Expand Down Expand Up @@ -904,7 +981,7 @@ public void toString_containsFields() throws IOException {
String.format(
"ServiceAccountCredentials{clientId=%s, clientEmail=%s, privateKeyId=%s, "
+ "transportFactoryClassName=%s, tokenServerUri=%s, scopes=%s, serviceAccountUser=%s, "
+ "quotaProjectId=%s}",
+ "quotaProjectId=%s, lifetime=3600}",
CLIENT_ID,
CLIENT_EMAIL,
PRIVATE_KEY_ID,
Expand Down

0 comments on commit 427f2d5

Please sign in to comment.