Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow set lifetime for service account creds #516

Merged
merged 12 commits into from Jan 12, 2021
Expand Up @@ -92,6 +92,10 @@ 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 ONE_HOUR_IN_SECONDS = 3600;
private static final String LIFETIME_EXCEEDED_ERROR =
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
"lifetime must be less than or equal to 43200";

private final String clientId;
private final String clientEmail;
Expand All @@ -103,6 +107,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 @@ -122,6 +127,9 @@ 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).
*/
ServiceAccountCredentials(
String clientId,
Expand All @@ -133,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 @@ -148,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_EXCEEDED_ERROR);
}
this.lifetime = lifetime;
}

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

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

/** Clones the service account with a new lifetime value * */
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
public ServiceAccountCredentials createWithNewLifetime(int lifetime) {
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
return this.toBuilder().setLifetime(lifetime).build();
}

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

public final String getClientId() {
Expand Down Expand Up @@ -562,6 +583,10 @@ public final URI getTokenServerUri() {
return tokenServerUri;
}

public final int getLifetime() {
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
return lifetime;
}

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

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

Expand All @@ -647,7 +674,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 @@ -660,7 +688,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 @@ -692,7 +720,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 @@ -745,6 +773,7 @@ public static class Builder extends GoogleCredentials.Builder {
private Collection<String> scopes;
private HttpTransportFactory transportFactory;
private String quotaProjectId;
private int lifetime = ONE_HOUR_IN_SECONDS;

protected Builder() {}

Expand All @@ -759,6 +788,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 @@ -811,6 +841,11 @@ public Builder setQuotaProjectId(String quotaProjectId) {
return this;
}

public Builder setLifetime(int lifetime) {
this.lifetime = lifetime;
return this;
}

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

public int getLifetime() {
return lifetime;
}

public ServiceAccountCredentials build() {
return new ServiceAccountCredentials(
clientId,
Expand All @@ -862,7 +901,8 @@ public ServiceAccountCredentials build() {
tokenServerUri,
serviceAccountUser,
projectId,
quotaProjectId);
quotaProjectId,
lifetime);
}
}
}
Expand Up @@ -111,6 +111,51 @@ public class ServiceAccountCredentialsTest extends BaseSerializationTest {
+ "aXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwNzA4NTY4In0"
+ ".redacted";
private static final String QUOTA_PROJECT = "sample-quota-project-id";
private static final int ONE_HOUR_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(ONE_HOUR_IN_SECONDS, builder.getLifetime());
assertEquals(ONE_HOUR_IN_SECONDS, builder.build().getLifetime());

builder.setLifetime(4000);
assertEquals(4000, builder.getLifetime());
assertEquals(4000, 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 createWithNewLifetime() throws IOException {
ServiceAccountCredentials credentials = createDefaultBuilder().build();
credentials = credentials.createWithNewLifetime(4000);
assertEquals(4000, credentials.getLifetime());
}

@Test
public void createdScoped_clones() throws IOException {
Expand Down Expand Up @@ -202,6 +247,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());
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
public void createAssertionForIdToken_correct() throws IOException {

Expand Down Expand Up @@ -231,6 +289,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());
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
public void createAssertionForIdToken_incorrect() throws IOException {

Expand Down Expand Up @@ -904,7 +978,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