Skip to content

Commit 97bfc4c

Browse files
aeitzmanlsirac
andauthored
feat: Add iam endpoint override to ImpersonatedCredentials (googleapis#910)
* feat: Added iam endpoint override to ImpersonatedCredentials * fix: Fixed GoogleCredentialsTests that were broken by regional impersonated credential url change * fix: Addressed code review comments * fix: fixed createScoped method in impersonatedCredentials to use override endpoint correctly and added test * fix: fixed linter errors Co-authored-by: Leo <[email protected]>
1 parent 112bfc9 commit 97bfc4c

File tree

5 files changed

+144
-8
lines changed

5 files changed

+144
-8
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ private ImpersonatedCredentials initializeImpersonatedCredentials() {
265265
.setTargetPrincipal(targetPrincipal)
266266
.setScopes(new ArrayList<>(scopes))
267267
.setLifetime(3600) // 1 hour in seconds
268+
.setIamEndpointOverride(serviceAccountImpersonationUrl)
268269
.build();
269270
}
270271

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

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public class ImpersonatedCredentials extends GoogleCredentials
105105
private List<String> scopes;
106106
private int lifetime;
107107
private String quotaProjectId;
108+
private String iamEndpointOverride;
108109
private final String transportFactoryClassName;
109110

110111
private transient HttpTransportFactory transportFactory;
@@ -192,6 +193,54 @@ public static ImpersonatedCredentials create(
192193
.build();
193194
}
194195

196+
/**
197+
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It
198+
* should be either a user account credential or a service account credential.
199+
* @param targetPrincipal the service account to impersonate
200+
* @param delegates the chained list of delegates required to grant the final access_token. If
201+
* set, the sequence of identities must have "Service Account Token Creator" capability
202+
* granted to the preceding identity. For example, if set to [serviceAccountB,
203+
* serviceAccountC], the sourceCredential must have the Token Creator role on serviceAccountB.
204+
* serviceAccountB must have the Token Creator on serviceAccountC. Finally, C must have Token
205+
* Creator on target_principal. If unset, sourceCredential must have that role on
206+
* targetPrincipal.
207+
* @param scopes scopes to request during the authorization grant
208+
* @param lifetime number of seconds the delegated credential should be valid. By default this
209+
* value should be at most 3600. However, you can follow <a
210+
* href='https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth'>these
211+
* instructions</a> to set up the service account and extend the maximum lifetime to 43200 (12
212+
* hours). If the given lifetime is 0, default value 3600 will be used instead when creating
213+
* the credentials.
214+
* @param transportFactory HTTP transport factory that creates the transport used to get access
215+
* tokens.
216+
* @param quotaProjectId the project used for quota and billing purposes. Should be null unless
217+
* the caller wants to use a project different from the one that owns the impersonated
218+
* credential for billing/quota purposes.
219+
* @param iamEndpointOverride The full IAM endpoint override with the target_principal embedded.
220+
* This is useful when supporting impersonation with regional endpoints.
221+
* @return new credentials
222+
*/
223+
public static ImpersonatedCredentials create(
224+
GoogleCredentials sourceCredentials,
225+
String targetPrincipal,
226+
List<String> delegates,
227+
List<String> scopes,
228+
int lifetime,
229+
HttpTransportFactory transportFactory,
230+
String quotaProjectId,
231+
String iamEndpointOverride) {
232+
return ImpersonatedCredentials.newBuilder()
233+
.setSourceCredentials(sourceCredentials)
234+
.setTargetPrincipal(targetPrincipal)
235+
.setDelegates(delegates)
236+
.setScopes(scopes)
237+
.setLifetime(lifetime)
238+
.setHttpTransportFactory(transportFactory)
239+
.setQuotaProjectId(quotaProjectId)
240+
.setIamEndpointOverride(iamEndpointOverride)
241+
.build();
242+
}
243+
195244
/**
196245
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It
197246
* should be either a user account credential or a service account credential.
@@ -257,6 +306,11 @@ public String getQuotaProjectId() {
257306
return this.quotaProjectId;
258307
}
259308

309+
@VisibleForTesting
310+
String getIamEndpointOverride() {
311+
return this.iamEndpointOverride;
312+
}
313+
260314
@VisibleForTesting
261315
List<String> getDelegates() {
262316
return delegates;
@@ -320,9 +374,9 @@ static ImpersonatedCredentials fromJson(
320374
String sourceCredentialsType;
321375
String quotaProjectId;
322376
String targetPrincipal;
377+
String serviceAccountImpersonationUrl;
323378
try {
324-
String serviceAccountImpersonationUrl =
325-
(String) json.get("service_account_impersonation_url");
379+
serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
326380
if (json.containsKey("delegates")) {
327381
delegates = (List<String>) json.get("delegates");
328382
}
@@ -354,6 +408,7 @@ static ImpersonatedCredentials fromJson(
354408
.setLifetime(DEFAULT_LIFETIME_IN_SECONDS)
355409
.setHttpTransportFactory(transportFactory)
356410
.setQuotaProjectId(quotaProjectId)
411+
.setIamEndpointOverride(serviceAccountImpersonationUrl)
357412
.build();
358413
}
359414

@@ -370,6 +425,7 @@ public GoogleCredentials createScoped(Collection<String> scopes) {
370425
.setDelegates(this.delegates)
371426
.setHttpTransportFactory(this.transportFactory)
372427
.setQuotaProjectId(this.quotaProjectId)
428+
.setIamEndpointOverride(this.iamEndpointOverride)
373429
.build();
374430
}
375431

@@ -393,6 +449,7 @@ private ImpersonatedCredentials(Builder builder) {
393449
builder.getHttpTransportFactory(),
394450
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
395451
this.quotaProjectId = builder.quotaProjectId;
452+
this.iamEndpointOverride = builder.iamEndpointOverride;
396453
this.transportFactoryClassName = this.transportFactory.getClass().getName();
397454
if (this.delegates == null) {
398455
this.delegates = new ArrayList<String>();
@@ -424,7 +481,10 @@ public AccessToken refreshAccessToken() throws IOException {
424481
HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(sourceCredentials);
425482
HttpRequestFactory requestFactory = httpTransport.createRequestFactory();
426483

427-
String endpointUrl = String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
484+
String endpointUrl =
485+
this.iamEndpointOverride != null
486+
? this.iamEndpointOverride
487+
: String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
428488
GenericUrl url = new GenericUrl(endpointUrl);
429489

430490
Map<String, Object> body =
@@ -489,7 +549,13 @@ public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.O
489549
@Override
490550
public int hashCode() {
491551
return Objects.hash(
492-
sourceCredentials, targetPrincipal, delegates, scopes, lifetime, quotaProjectId);
552+
sourceCredentials,
553+
targetPrincipal,
554+
delegates,
555+
scopes,
556+
lifetime,
557+
quotaProjectId,
558+
iamEndpointOverride);
493559
}
494560

495561
@Override
@@ -502,6 +568,7 @@ public String toString() {
502568
.add("lifetime", lifetime)
503569
.add("transportFactoryClassName", transportFactoryClassName)
504570
.add("quotaProjectId", quotaProjectId)
571+
.add("iamEndpointOverride", iamEndpointOverride)
505572
.toString();
506573
}
507574

@@ -517,7 +584,8 @@ public boolean equals(Object obj) {
517584
&& Objects.equals(this.scopes, other.scopes)
518585
&& Objects.equals(this.lifetime, other.lifetime)
519586
&& Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
520-
&& Objects.equals(this.quotaProjectId, other.quotaProjectId);
587+
&& Objects.equals(this.quotaProjectId, other.quotaProjectId)
588+
&& Objects.equals(this.iamEndpointOverride, other.iamEndpointOverride);
521589
}
522590

523591
public Builder toBuilder() {
@@ -537,6 +605,7 @@ public static class Builder extends GoogleCredentials.Builder {
537605
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS;
538606
private HttpTransportFactory transportFactory;
539607
private String quotaProjectId;
608+
private String iamEndpointOverride;
540609

541610
protected Builder() {}
542611

@@ -604,6 +673,11 @@ public Builder setQuotaProjectId(String quotaProjectId) {
604673
return this;
605674
}
606675

676+
public Builder setIamEndpointOverride(String iamEndpointOverride) {
677+
this.iamEndpointOverride = iamEndpointOverride;
678+
return this;
679+
}
680+
607681
public ImpersonatedCredentials build() {
608682
return new ImpersonatedCredentials(this);
609683
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ void fromStream_Impersonation_providesToken_WithQuotaProject() throws IOExceptio
273273
ImpersonatedCredentialsTest.IMPERSONATED_CLIENT_EMAIL);
274274
transportFactory.transport.setAccessToken(ImpersonatedCredentialsTest.ACCESS_TOKEN);
275275
transportFactory.transport.setExpireTime(ImpersonatedCredentialsTest.getDefaultExpireTime());
276+
transportFactory.transport.setAccessTokenEndpoint(
277+
ImpersonatedCredentialsTest.IMPERSONATION_URL);
276278

277279
InputStream impersonationCredentialsStream =
278280
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
@@ -307,6 +309,8 @@ void fromStream_Impersonation_providesToken_WithoutQuotaProject() throws IOExcep
307309
ImpersonatedCredentialsTest.IMPERSONATED_CLIENT_EMAIL);
308310
transportFactory.transport.setAccessToken(ImpersonatedCredentialsTest.ACCESS_TOKEN);
309311
transportFactory.transport.setExpireTime(ImpersonatedCredentialsTest.getDefaultExpireTime());
312+
transportFactory.transport.setAccessTokenEndpoint(
313+
ImpersonatedCredentialsTest.IMPERSONATION_URL);
310314

311315
InputStream impersonationCredentialsStream =
312316
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(

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

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,14 @@ class ImpersonatedCredentialsTest extends BaseSerializationTest {
119119
private static JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
120120

121121
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
122-
public static final String IMPERSONATION_URL =
122+
public static final String DEFAULT_IMPERSONATION_URL =
123123
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"
124124
+ IMPERSONATED_CLIENT_EMAIL
125125
+ ":generateAccessToken";
126+
public static final String IMPERSONATION_URL =
127+
"https://us-east1-iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"
128+
+ IMPERSONATED_CLIENT_EMAIL
129+
+ ":generateAccessToken";
126130
private static final String USER_ACCOUNT_CLIENT_ID =
127131
"76408650-6qr441hur.apps.googleusercontent.com";
128132
private static final String USER_ACCOUNT_CLIENT_SECRET = "d-F499q74hFpdHD0T5";
@@ -180,6 +184,7 @@ void fromJson_userAsSource_WithQuotaProjectId() throws IOException {
180184
ImpersonatedCredentials credentials =
181185
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
182186
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
187+
assertEquals(IMPERSONATION_URL, credentials.getIamEndpointOverride());
183188
assertEquals(QUOTA_PROJECT_ID, credentials.getQuotaProjectId());
184189
assertEquals(DELEGATES, credentials.getDelegates());
185190
assertEquals(new ArrayList<String>(), credentials.getScopes());
@@ -201,6 +206,7 @@ void fromJson_userAsSource_WithoutQuotaProjectId() throws IOException {
201206
ImpersonatedCredentials credentials =
202207
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
203208
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
209+
assertEquals(IMPERSONATION_URL, credentials.getIamEndpointOverride());
204210
assertNull(credentials.getQuotaProjectId());
205211
assertEquals(DELEGATES, credentials.getDelegates());
206212
assertEquals(new ArrayList<String>(), credentials.getScopes());
@@ -223,6 +229,7 @@ void fromJson_userAsSource_MissingDelegatesField() throws IOException {
223229
ImpersonatedCredentials credentials =
224230
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
225231
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
232+
assertEquals(IMPERSONATION_URL, credentials.getIamEndpointOverride());
226233
assertNull(credentials.getQuotaProjectId());
227234
assertEquals(new ArrayList<String>(), credentials.getDelegates());
228235
assertEquals(new ArrayList<String>(), credentials.getScopes());
@@ -238,6 +245,7 @@ void fromJson_ServiceAccountAsSource() throws IOException {
238245
ImpersonatedCredentials credentials =
239246
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
240247
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
248+
assertEquals(IMPERSONATION_URL, credentials.getIamEndpointOverride());
241249
assertEquals(QUOTA_PROJECT_ID, credentials.getQuotaProjectId());
242250
assertEquals(DELEGATES, credentials.getDelegates());
243251
assertEquals(new ArrayList<String>(), credentials.getScopes());
@@ -329,6 +337,25 @@ void createScopedWithImmutableScopes() {
329337
assertEquals(Arrays.asList("scope1", "scope2"), scoped_credentials.getScopes());
330338
}
331339

340+
@Test
341+
void createScopedWithIamEndpointOverride() {
342+
ImpersonatedCredentials targetCredentials =
343+
ImpersonatedCredentials.create(
344+
sourceCredentials,
345+
IMPERSONATED_CLIENT_EMAIL,
346+
DELEGATES,
347+
IMMUTABLE_SCOPES_LIST,
348+
VALID_LIFETIME,
349+
mockTransportFactory,
350+
QUOTA_PROJECT_ID,
351+
IMPERSONATION_URL);
352+
353+
ImpersonatedCredentials scoped_credentials =
354+
(ImpersonatedCredentials) targetCredentials.createScoped(IMMUTABLE_SCOPES_SET);
355+
assertEquals(
356+
targetCredentials.getIamEndpointOverride(), scoped_credentials.getIamEndpointOverride());
357+
}
358+
332359
@Test
333360
void refreshAccessToken_unauthorized() throws IOException {
334361

@@ -449,6 +476,29 @@ void refreshAccessToken_success() throws IOException, IllegalStateException {
449476
mockTransportFactory);
450477

451478
assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue());
479+
assertEquals(DEFAULT_IMPERSONATION_URL, mockTransportFactory.transport.getRequest().getUrl());
480+
}
481+
482+
@Test
483+
void refreshAccessToken_endpointOverride() throws IOException, IllegalStateException {
484+
mockTransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
485+
mockTransportFactory.transport.setAccessToken(ACCESS_TOKEN);
486+
mockTransportFactory.transport.setExpireTime(getDefaultExpireTime());
487+
mockTransportFactory.transport.setAccessTokenEndpoint(IMPERSONATION_URL);
488+
489+
ImpersonatedCredentials targetCredentials =
490+
ImpersonatedCredentials.create(
491+
sourceCredentials,
492+
IMPERSONATED_CLIENT_EMAIL,
493+
null,
494+
IMMUTABLE_SCOPES_LIST,
495+
VALID_LIFETIME,
496+
mockTransportFactory,
497+
QUOTA_PROJECT_ID,
498+
IMPERSONATION_URL);
499+
500+
assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue());
501+
assertEquals(IMPERSONATION_URL, mockTransportFactory.transport.getRequest().getUrl());
452502
}
453503

454504
@Test

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
/** Transport that simulates the IAMCredentials server for access tokens. */
4747
public class MockIAMCredentialsServiceTransport extends MockHttpTransport {
4848

49-
private static final String IAM_ACCESS_TOKEN_ENDPOINT =
49+
private static final String DEFAULT_IAM_ACCESS_TOKEN_ENDPOINT =
5050
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken";
5151
private static final String IAM_ID_TOKEN_ENDPOINT =
5252
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken";
@@ -58,6 +58,7 @@ public class MockIAMCredentialsServiceTransport extends MockHttpTransport {
5858
private byte[] signedBlob;
5959
private int responseCode = HttpStatusCodes.STATUS_CODE_OK;
6060
private String errorMessage;
61+
private String iamAccessTokenEndpoint;
6162

6263
private String accessToken;
6364
private String expireTime;
@@ -101,6 +102,10 @@ public void setIdToken(String idToken) {
101102
this.idToken = idToken;
102103
}
103104

105+
public void setAccessTokenEndpoint(String accessTokenEndpoint) {
106+
this.iamAccessTokenEndpoint = accessTokenEndpoint;
107+
}
108+
104109
public MockLowLevelHttpRequest getRequest() {
105110
return request;
106111
}
@@ -109,7 +114,9 @@ public MockLowLevelHttpRequest getRequest() {
109114
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
110115

111116
String iamAccesssTokenformattedUrl =
112-
String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
117+
iamAccessTokenEndpoint != null
118+
? iamAccessTokenEndpoint
119+
: String.format(DEFAULT_IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
113120
String iamSignBlobformattedUrl = String.format(IAM_SIGN_ENDPOINT, this.targetPrincipal);
114121
String iamIdTokenformattedUrl = String.format(IAM_ID_TOKEN_ENDPOINT, this.targetPrincipal);
115122
if (url.equals(iamAccesssTokenformattedUrl)) {

0 commit comments

Comments
 (0)