Skip to content

Commit 427f2d5

Browse files
feat: allow set lifetime for service account creds (#516)
* 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>
1 parent af21727 commit 427f2d5

File tree

2 files changed

+135
-10
lines changed

2 files changed

+135
-10
lines changed

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

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ public class ServiceAccountCredentials extends GoogleCredentials
9393
private static final long serialVersionUID = 7807543542681217978L;
9494
private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
9595
private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
96+
private static final int TWELVE_HOURS_IN_SECONDS = 43200;
97+
private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600;
9698

9799
private final String clientId;
98100
private final String clientEmail;
@@ -104,6 +106,7 @@ public class ServiceAccountCredentials extends GoogleCredentials
104106
private final URI tokenServerUri;
105107
private final Collection<String> scopes;
106108
private final String quotaProjectId;
109+
private final int lifetime;
107110

108111
private transient HttpTransportFactory transportFactory;
109112

@@ -123,6 +126,10 @@ public class ServiceAccountCredentials extends GoogleCredentials
123126
* authority to the service account.
124127
* @param projectId the project used for billing
125128
* @param quotaProjectId The project used for quota and billing purposes. May be null.
129+
* @param lifetime number of seconds the access token should be valid for. The value should be at
130+
* most 43200 (12 hours). If the token is used for calling a Google API, then the value should
131+
* be at most 3600 (1 hour). If the given value is 0, then the default value 3600 will be used
132+
* when creating the credentials.
126133
*/
127134
ServiceAccountCredentials(
128135
String clientId,
@@ -134,7 +141,8 @@ public class ServiceAccountCredentials extends GoogleCredentials
134141
URI tokenServerUri,
135142
String serviceAccountUser,
136143
String projectId,
137-
String quotaProjectId) {
144+
String quotaProjectId,
145+
int lifetime) {
138146
this.clientId = clientId;
139147
this.clientEmail = Preconditions.checkNotNull(clientEmail);
140148
this.privateKey = Preconditions.checkNotNull(privateKey);
@@ -149,6 +157,10 @@ public class ServiceAccountCredentials extends GoogleCredentials
149157
this.serviceAccountUser = serviceAccountUser;
150158
this.projectId = projectId;
151159
this.quotaProjectId = quotaProjectId;
160+
if (lifetime > TWELVE_HOURS_IN_SECONDS) {
161+
throw new IllegalStateException("lifetime must be less than or equal to 43200");
162+
}
163+
this.lifetime = lifetime;
152164
}
153165

154166
/**
@@ -325,7 +337,8 @@ static ServiceAccountCredentials fromPkcs8(
325337
tokenServerUri,
326338
serviceAccountUser,
327339
projectId,
328-
quotaProject);
340+
quotaProject,
341+
DEFAULT_LIFETIME_IN_SECONDS);
329342
}
330343

331344
/** Helper to convert from a PKCS#8 String to an RSA private key */
@@ -513,7 +526,21 @@ public GoogleCredentials createScoped(Collection<String> newScopes) {
513526
tokenServerUri,
514527
serviceAccountUser,
515528
projectId,
516-
quotaProjectId);
529+
quotaProjectId,
530+
lifetime);
531+
}
532+
533+
/**
534+
* Clones the service account with a new lifetime value.
535+
*
536+
* @param lifetime life time value in seconds. The value should be at most 43200 (12 hours). If
537+
* the token is used for calling a Google API, then the value should be at most 3600 (1 hour).
538+
* If the given value is 0, then the default value 3600 will be used when creating the
539+
* credentials.
540+
* @return the cloned service account credentials with the given custom life time
541+
*/
542+
public ServiceAccountCredentials createWithCustomLifetime(int lifetime) {
543+
return this.toBuilder().setLifetime(lifetime).build();
517544
}
518545

519546
@Override
@@ -528,7 +555,8 @@ public GoogleCredentials createDelegated(String user) {
528555
tokenServerUri,
529556
user,
530557
projectId,
531-
quotaProjectId);
558+
quotaProjectId,
559+
lifetime);
532560
}
533561

534562
public final String getClientId() {
@@ -563,6 +591,11 @@ public final URI getTokenServerUri() {
563591
return tokenServerUri;
564592
}
565593

594+
@VisibleForTesting
595+
int getLifetime() {
596+
return lifetime;
597+
}
598+
566599
@Override
567600
public String getAccount() {
568601
return getClientEmail();
@@ -618,7 +651,8 @@ public int hashCode() {
618651
transportFactoryClassName,
619652
tokenServerUri,
620653
scopes,
621-
quotaProjectId);
654+
quotaProjectId,
655+
lifetime);
622656
}
623657

624658
@Override
@@ -632,6 +666,7 @@ public String toString() {
632666
.add("scopes", scopes)
633667
.add("serviceAccountUser", serviceAccountUser)
634668
.add("quotaProjectId", quotaProjectId)
669+
.add("lifetime", lifetime)
635670
.toString();
636671
}
637672

@@ -648,7 +683,8 @@ public boolean equals(Object obj) {
648683
&& Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
649684
&& Objects.equals(this.tokenServerUri, other.tokenServerUri)
650685
&& Objects.equals(this.scopes, other.scopes)
651-
&& Objects.equals(this.quotaProjectId, other.quotaProjectId);
686+
&& Objects.equals(this.quotaProjectId, other.quotaProjectId)
687+
&& Objects.equals(this.lifetime, other.lifetime);
652688
}
653689

654690
String createAssertion(JsonFactory jsonFactory, long currentTime, String audience)
@@ -661,7 +697,7 @@ String createAssertion(JsonFactory jsonFactory, long currentTime, String audienc
661697
JsonWebToken.Payload payload = new JsonWebToken.Payload();
662698
payload.setIssuer(clientEmail);
663699
payload.setIssuedAtTimeSeconds(currentTime / 1000);
664-
payload.setExpirationTimeSeconds(currentTime / 1000 + 3600);
700+
payload.setExpirationTimeSeconds(currentTime / 1000 + this.lifetime);
665701
payload.setSubject(serviceAccountUser);
666702
payload.put("scope", Joiner.on(' ').join(scopes));
667703

@@ -693,7 +729,7 @@ String createAssertionForIdToken(
693729
JsonWebToken.Payload payload = new JsonWebToken.Payload();
694730
payload.setIssuer(clientEmail);
695731
payload.setIssuedAtTimeSeconds(currentTime / 1000);
696-
payload.setExpirationTimeSeconds(currentTime / 1000 + 3600);
732+
payload.setExpirationTimeSeconds(currentTime / 1000 + this.lifetime);
697733
payload.setSubject(serviceAccountUser);
698734

699735
if (audience == null) {
@@ -746,6 +782,7 @@ public static class Builder extends GoogleCredentials.Builder {
746782
private Collection<String> scopes;
747783
private HttpTransportFactory transportFactory;
748784
private String quotaProjectId;
785+
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS;
749786

750787
protected Builder() {}
751788

@@ -760,6 +797,7 @@ protected Builder(ServiceAccountCredentials credentials) {
760797
this.serviceAccountUser = credentials.serviceAccountUser;
761798
this.projectId = credentials.projectId;
762799
this.quotaProjectId = credentials.quotaProjectId;
800+
this.lifetime = credentials.lifetime;
763801
}
764802

765803
public Builder setClientId(String clientId) {
@@ -812,6 +850,11 @@ public Builder setQuotaProjectId(String quotaProjectId) {
812850
return this;
813851
}
814852

853+
public Builder setLifetime(int lifetime) {
854+
this.lifetime = lifetime == 0 ? DEFAULT_LIFETIME_IN_SECONDS : lifetime;
855+
return this;
856+
}
857+
815858
public String getClientId() {
816859
return clientId;
817860
}
@@ -852,6 +895,10 @@ public String getQuotaProjectId() {
852895
return quotaProjectId;
853896
}
854897

898+
public int getLifetime() {
899+
return lifetime;
900+
}
901+
855902
public ServiceAccountCredentials build() {
856903
return new ServiceAccountCredentials(
857904
clientId,
@@ -863,7 +910,8 @@ public ServiceAccountCredentials build() {
863910
tokenServerUri,
864911
serviceAccountUser,
865912
projectId,
866-
quotaProjectId);
913+
quotaProjectId,
914+
lifetime);
867915
}
868916
}
869917
}

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

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,54 @@ public class ServiceAccountCredentialsTest extends BaseSerializationTest {
111111
+ "aXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwNzA4NTY4In0"
112112
+ ".redacted";
113113
private static final String QUOTA_PROJECT = "sample-quota-project-id";
114+
private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600;
115+
private static final int INVALID_LIFETIME = 43210;
116+
117+
private ServiceAccountCredentials.Builder createDefaultBuilder() throws IOException {
118+
PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(PRIVATE_KEY_PKCS8);
119+
return ServiceAccountCredentials.newBuilder()
120+
.setClientId(CLIENT_ID)
121+
.setClientEmail(CLIENT_EMAIL)
122+
.setPrivateKey(privateKey)
123+
.setPrivateKeyId(PRIVATE_KEY_ID)
124+
.setScopes(SCOPES)
125+
.setServiceAccountUser(USER)
126+
.setProjectId(PROJECT_ID);
127+
}
128+
129+
@Test
130+
public void setLifetime() throws IOException {
131+
ServiceAccountCredentials.Builder builder = createDefaultBuilder();
132+
assertEquals(DEFAULT_LIFETIME_IN_SECONDS, builder.getLifetime());
133+
assertEquals(DEFAULT_LIFETIME_IN_SECONDS, builder.build().getLifetime());
134+
135+
builder.setLifetime(4000);
136+
assertEquals(4000, builder.getLifetime());
137+
assertEquals(4000, builder.build().getLifetime());
138+
139+
builder.setLifetime(0);
140+
assertEquals(DEFAULT_LIFETIME_IN_SECONDS, builder.build().getLifetime());
141+
}
142+
143+
@Test
144+
public void setLifetime_invalid_lifetime() throws IOException, IllegalStateException {
145+
try {
146+
createDefaultBuilder().setLifetime(INVALID_LIFETIME).build();
147+
fail(
148+
String.format(
149+
"Should throw exception with message containing '%s'",
150+
"lifetime must be less than or equal to 43200"));
151+
} catch (IllegalStateException expected) {
152+
assertTrue(expected.getMessage().contains("lifetime must be less than or equal to 43200"));
153+
}
154+
}
155+
156+
@Test
157+
public void createWithCustomLifetime() throws IOException {
158+
ServiceAccountCredentials credentials = createDefaultBuilder().build();
159+
credentials = credentials.createWithCustomLifetime(4000);
160+
assertEquals(4000, credentials.getLifetime());
161+
}
114162

115163
@Test
116164
public void createdScoped_clones() throws IOException {
@@ -202,6 +250,19 @@ public void createAssertion_correct() throws IOException {
202250
assertEquals(Joiner.on(' ').join(scopes), payload.get("scope"));
203251
}
204252

253+
@Test
254+
public void createAssertion_custom_lifetime() throws IOException {
255+
ServiceAccountCredentials credentials = createDefaultBuilder().setLifetime(4000).build();
256+
257+
JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
258+
long currentTimeMillis = Clock.SYSTEM.currentTimeMillis();
259+
String assertion = credentials.createAssertion(jsonFactory, currentTimeMillis, null);
260+
261+
JsonWebSignature signature = JsonWebSignature.parse(jsonFactory, assertion);
262+
JsonWebToken.Payload payload = signature.getPayload();
263+
assertEquals(currentTimeMillis / 1000 + 4000, (long) payload.getExpirationTimeSeconds());
264+
}
265+
205266
@Test
206267
public void createAssertionForIdToken_correct() throws IOException {
207268

@@ -231,6 +292,22 @@ public void createAssertionForIdToken_correct() throws IOException {
231292
assertEquals(USER, payload.getSubject());
232293
}
233294

295+
@Test
296+
public void createAssertionForIdToken_custom_lifetime() throws IOException {
297+
298+
ServiceAccountCredentials credentials = createDefaultBuilder().setLifetime(4000).build();
299+
300+
JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
301+
long currentTimeMillis = Clock.SYSTEM.currentTimeMillis();
302+
String assertion =
303+
credentials.createAssertionForIdToken(
304+
jsonFactory, currentTimeMillis, null, "https://foo.com/bar");
305+
306+
JsonWebSignature signature = JsonWebSignature.parse(jsonFactory, assertion);
307+
JsonWebToken.Payload payload = signature.getPayload();
308+
assertEquals(currentTimeMillis / 1000 + 4000, (long) payload.getExpirationTimeSeconds());
309+
}
310+
234311
@Test
235312
public void createAssertionForIdToken_incorrect() throws IOException {
236313

@@ -904,7 +981,7 @@ public void toString_containsFields() throws IOException {
904981
String.format(
905982
"ServiceAccountCredentials{clientId=%s, clientEmail=%s, privateKeyId=%s, "
906983
+ "transportFactoryClassName=%s, tokenServerUri=%s, scopes=%s, serviceAccountUser=%s, "
907-
+ "quotaProjectId=%s}",
984+
+ "quotaProjectId=%s, lifetime=3600}",
908985
CLIENT_ID,
909986
CLIENT_EMAIL,
910987
PRIVATE_KEY_ID,

0 commit comments

Comments
 (0)