Skip to content

Commit c498ccf

Browse files
zhumin8blakeli0
andauthored
feat: service sccount to service account impersonation to support universe domain (#1528)
for context: b/340602527 Changes in this pr: - Override `getUniverseDomain()` to grab source credentials’s universe domain (UD) by default. Always use source credentials UD, not explicit provided UD. (In current design, impersonated credentials may not have universe domain in the outer layer. relay on UD from source credential. This may change in future) - Fix `isDefaultUniverseDomain()` in `GoogleCredentials` to account for `getUniverseDomain()` overrides in child classes. - In refreshAccessToken(), use endpoint url pattern to account for TPC case. - note that I choose to bypass this refreshIfExpired step because it wrongly steps into code path meant only for OAuth2 token request (GDU flow). Filed #1534 to address this separately. But for GDU flow here, this refresh step is redundant because the SSJ will get re-generated at [initialize request](https://github.com/googleapis/google-auth-library-java/blob/a987ecd06fd25a0048cdb3da6d1df4d029d85d79/oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java#L558). Also skip this step for SA GDU with SSJ flow. - Throw IllegalStateException if UD is explicitly set (with parent class setter) and not matching source credential's UD - Fix toBuilder() to invoke super, and fix related issue with createScoped. (see #1489, #1428); Also fix equals() to compare super first. Not in this pr: - idtoken and signBlob endpoint changes are out-of-scope for this pr, will raise separate pr for it. sa-to-sa impersonation is successfully E2E tested for TPC usage according to [go/prptst-testing-service-account-impersonation](http://goto.google.com/prptst-testing-service-account-impersonation). --------- Co-authored-by: Blake Li <blakeli@google.com>
1 parent f154edb commit c498ccf

File tree

6 files changed

+286
-52
lines changed

6 files changed

+286
-52
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,11 +263,11 @@ protected boolean isExplicitUniverseDomain() {
263263
/**
264264
* Checks if universe domain equals to {@link Credentials#GOOGLE_DEFAULT_UNIVERSE}.
265265
*
266-
* @return true if universeDomain equals to {@link Credentials#GOOGLE_DEFAULT_UNIVERSE}, false
266+
* @return true if universe domain equals to {@link Credentials#GOOGLE_DEFAULT_UNIVERSE}, false
267267
* otherwise
268268
*/
269-
boolean isDefaultUniverseDomain() {
270-
return this.universeDomain.equals(Credentials.GOOGLE_DEFAULT_UNIVERSE);
269+
boolean isDefaultUniverseDomain() throws IOException {
270+
return getUniverseDomain().equals(Credentials.GOOGLE_DEFAULT_UNIVERSE);
271271
}
272272

273273
/**

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

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,6 @@ public class ImpersonatedCredentials extends GoogleCredentials
103103
private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600;
104104
private static final String CLOUD_PLATFORM_SCOPE =
105105
"https://www.googleapis.com/auth/cloud-platform";
106-
private static final String IAM_ACCESS_TOKEN_ENDPOINT =
107-
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken";
108-
109106
private GoogleCredentials sourceCredentials;
110107
private String targetPrincipal;
111108
private List<String> delegates;
@@ -423,14 +420,7 @@ public boolean createScopedRequired() {
423420

424421
@Override
425422
public GoogleCredentials createScoped(Collection<String> scopes) {
426-
return toBuilder()
427-
.setScopes(new ArrayList<>(scopes))
428-
.setLifetime(this.lifetime)
429-
.setDelegates(this.delegates)
430-
.setHttpTransportFactory(this.transportFactory)
431-
.setQuotaProjectId(this.quotaProjectId)
432-
.setIamEndpointOverride(this.iamEndpointOverride)
433-
.build();
423+
return toBuilder().setScopes(new ArrayList<>(scopes)).setAccessToken(null).build();
434424
}
435425

436426
@Override
@@ -457,7 +447,7 @@ public ImpersonatedCredentials createWithCustomCalendar(Calendar calendar) {
457447
.build();
458448
}
459449

460-
private ImpersonatedCredentials(Builder builder) {
450+
private ImpersonatedCredentials(Builder builder) throws IOException {
461451
super(builder);
462452
this.sourceCredentials = builder.getSourceCredentials();
463453
this.targetPrincipal = builder.getTargetPrincipal();
@@ -472,14 +462,36 @@ private ImpersonatedCredentials(Builder builder) {
472462
this.transportFactoryClassName = this.transportFactory.getClass().getName();
473463
this.calendar = builder.getCalendar();
474464
if (this.delegates == null) {
475-
this.delegates = new ArrayList<String>();
465+
this.delegates = new ArrayList<>();
476466
}
477467
if (this.scopes == null) {
478468
throw new IllegalStateException("Scopes cannot be null");
479469
}
480470
if (this.lifetime > TWELVE_HOURS_IN_SECONDS) {
481471
throw new IllegalStateException("lifetime must be less than or equal to 43200");
482472
}
473+
474+
// Do not expect explicit universe domain, throw exception if the explicit universe domain
475+
// does not match the source credential.
476+
// Do nothing if it matches the source credential
477+
if (isExplicitUniverseDomain()
478+
&& !this.sourceCredentials.getUniverseDomain().equals(builder.getUniverseDomain())) {
479+
throw new IllegalStateException(
480+
String.format(
481+
"Universe domain %s in source credentials "
482+
+ "does not match %s universe domain set for impersonated credentials.",
483+
this.sourceCredentials.getUniverseDomain(), builder.getUniverseDomain()));
484+
}
485+
}
486+
487+
/**
488+
* Gets the universe domain for the credential.
489+
*
490+
* @return the universe domain from source credentials
491+
*/
492+
@Override
493+
public String getUniverseDomain() throws IOException {
494+
return this.sourceCredentials.getUniverseDomain();
483495
}
484496

485497
@Override
@@ -489,10 +501,18 @@ public AccessToken refreshAccessToken() throws IOException {
489501
this.sourceCredentials.createScoped(Arrays.asList(CLOUD_PLATFORM_SCOPE));
490502
}
491503

492-
try {
493-
this.sourceCredentials.refreshIfExpired();
494-
} catch (IOException e) {
495-
throw new IOException("Unable to refresh sourceCredentials", e);
504+
// skip for SA with SSJ flow because it uses self-signed JWT
505+
// and will get refreshed at initialize request step
506+
// run for other source credential types or SA with GDU assert flow
507+
if (!(this.sourceCredentials instanceof ServiceAccountCredentials)
508+
|| (isDefaultUniverseDomain()
509+
&& ((ServiceAccountCredentials) this.sourceCredentials)
510+
.shouldUseAssertionFlowForGdu())) {
511+
try {
512+
this.sourceCredentials.refreshIfExpired();
513+
} catch (IOException e) {
514+
throw new IOException("Unable to refresh sourceCredentials", e);
515+
}
496516
}
497517

498518
HttpTransport httpTransport = this.transportFactory.create();
@@ -504,7 +524,11 @@ public AccessToken refreshAccessToken() throws IOException {
504524
String endpointUrl =
505525
this.iamEndpointOverride != null
506526
? this.iamEndpointOverride
507-
: String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
527+
: String.format(
528+
OAuth2Utils.IAM_ACCESS_TOKEN_ENDPOINT_FORMAT,
529+
getUniverseDomain(),
530+
this.targetPrincipal);
531+
508532
GenericUrl url = new GenericUrl(endpointUrl);
509533

510534
Map<String, Object> body =
@@ -603,6 +627,9 @@ public boolean equals(Object obj) {
603627
if (!(obj instanceof ImpersonatedCredentials)) {
604628
return false;
605629
}
630+
if (!super.equals(obj)) {
631+
return false;
632+
}
606633
ImpersonatedCredentials other = (ImpersonatedCredentials) obj;
607634
return Objects.equals(this.sourceCredentials, other.sourceCredentials)
608635
&& Objects.equals(this.targetPrincipal, other.targetPrincipal)
@@ -616,7 +643,7 @@ public boolean equals(Object obj) {
616643

617644
@Override
618645
public Builder toBuilder() {
619-
return new Builder(this.sourceCredentials, this.targetPrincipal);
646+
return new Builder(this);
620647
}
621648

622649
public static Builder newBuilder() {
@@ -636,11 +663,29 @@ public static class Builder extends GoogleCredentials.Builder {
636663

637664
protected Builder() {}
638665

666+
/**
667+
* @param sourceCredentials The source credentials to use for impersonation.
668+
* @param targetPrincipal The service account to impersonate.
669+
* @deprecated Use {@link #Builder(ImpersonatedCredentials)} instead. This constructor will be
670+
* removed in a future release.
671+
*/
672+
@Deprecated
639673
protected Builder(GoogleCredentials sourceCredentials, String targetPrincipal) {
640674
this.sourceCredentials = sourceCredentials;
641675
this.targetPrincipal = targetPrincipal;
642676
}
643677

678+
protected Builder(ImpersonatedCredentials credentials) {
679+
super(credentials);
680+
this.sourceCredentials = credentials.sourceCredentials;
681+
this.targetPrincipal = credentials.targetPrincipal;
682+
this.delegates = credentials.delegates;
683+
this.scopes = credentials.scopes;
684+
this.lifetime = credentials.lifetime;
685+
this.transportFactory = credentials.transportFactory;
686+
this.iamEndpointOverride = credentials.iamEndpointOverride;
687+
}
688+
644689
@CanIgnoreReturnValue
645690
public Builder setSourceCredentials(GoogleCredentials sourceCredentials) {
646691
this.sourceCredentials = sourceCredentials;
@@ -726,7 +771,13 @@ public Calendar getCalendar() {
726771

727772
@Override
728773
public ImpersonatedCredentials build() {
729-
return new ImpersonatedCredentials(this);
774+
try {
775+
return new ImpersonatedCredentials(this);
776+
} catch (IOException e) {
777+
// throwing exception would be breaking change. catching instead.
778+
// this should never happen.
779+
throw new IllegalStateException(e);
780+
}
730781
}
731782
}
732783

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ class OAuth2Utils {
8181
static final String IAM_ID_TOKEN_ENDPOINT_FORMAT =
8282
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:generateIdToken";
8383

84+
static final String IAM_ACCESS_TOKEN_ENDPOINT_FORMAT =
85+
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:generateAccessToken";
86+
static final String SIGN_BLOB_ENDPOINT_FORMAT =
87+
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s:signBlob";
88+
8489
static final URI TOKEN_SERVER_URI = URI.create("https://oauth2.googleapis.com/token");
8590

8691
static final URI TOKEN_REVOKE_URI = URI.create("https://oauth2.googleapis.com/revoke");

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -992,13 +992,19 @@ public void getRequestMetadata(
992992
// For default universe Self-signed JWT could be explicitly disabled with
993993
// {@code ServiceAccountCredentials.useJwtAccessWithScope} flag.
994994
// If universe is non-default, it only supports self-signed JWT, and it is always allowed.
995-
if (this.useJwtAccessWithScope || !isDefaultUniverseDomain()) {
996-
// This will call getRequestMetadata(URI uri), which handles self-signed JWT logic.
997-
// Self-signed JWT doesn't use network, so here we do a blocking call to improve
998-
// efficiency. executor will be ignored since it is intended for async operation.
999-
blockingGetToCallback(uri, callback);
1000-
} else {
1001-
super.getRequestMetadata(uri, executor, callback);
995+
try {
996+
if (this.useJwtAccessWithScope || !isDefaultUniverseDomain()) {
997+
// This will call getRequestMetadata(URI uri), which handles self-signed JWT logic.
998+
// Self-signed JWT doesn't use network, so here we do a blocking call to improve
999+
// efficiency. executor will be ignored since it is intended for async operation.
1000+
blockingGetToCallback(uri, callback);
1001+
} else {
1002+
super.getRequestMetadata(uri, executor, callback);
1003+
}
1004+
} catch (IOException e) {
1005+
// Wrap here because throwing exception would be breaking change.
1006+
// This should not happen for this credential type.
1007+
throw new IllegalStateException(e);
10021008
}
10031009
}
10041010

@@ -1021,20 +1027,20 @@ public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException
10211027

10221028
@Override
10231029
public CredentialTypeForMetrics getMetricsCredentialType() {
1024-
return shouldUseAssertionFlow()
1030+
return shouldUseAssertionFlowForGdu()
10251031
? CredentialTypeForMetrics.SERVICE_ACCOUNT_CREDENTIALS_AT
10261032
: CredentialTypeForMetrics.SERVICE_ACCOUNT_CREDENTIALS_JWT;
10271033
}
10281034

1029-
private boolean shouldUseAssertionFlow() {
1035+
boolean shouldUseAssertionFlowForGdu() {
10301036
// If scopes are provided, but we cannot use self-signed JWT or domain-wide delegation is
10311037
// configured then use scopes to get access token.
10321038
return ((!createScopedRequired() && !useJwtAccessWithScope)
10331039
|| isConfiguredForDomainWideDelegation());
10341040
}
10351041

10361042
private Map<String, List<String>> getRequestMetadataForGdu(URI uri) throws IOException {
1037-
return shouldUseAssertionFlow()
1043+
return shouldUseAssertionFlowForGdu()
10381044
? super.getRequestMetadata(uri)
10391045
: getRequestMetadataWithSelfSignedJwt(uri);
10401046
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -612,12 +612,12 @@ public void fromStream_Impersonation_providesToken_WithQuotaProject() throws IOE
612612
.setExpireTime(ImpersonatedCredentialsTest.getDefaultExpireTime());
613613
transportFactory
614614
.getTransport()
615-
.setAccessTokenEndpoint(ImpersonatedCredentialsTest.IMPERSONATION_URL);
615+
.setAccessTokenEndpoint(ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL);
616616
transportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
617617

618618
InputStream impersonationCredentialsStream =
619619
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
620-
ImpersonatedCredentialsTest.IMPERSONATION_URL,
620+
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
621621
ImpersonatedCredentialsTest.DELEGATES,
622622
ImpersonatedCredentialsTest.QUOTA_PROJECT_ID);
623623

@@ -647,7 +647,7 @@ public void fromStream_Impersonation_defaultUniverse() throws IOException {
647647

648648
InputStream impersonationCredentialsStream =
649649
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
650-
ImpersonatedCredentialsTest.IMPERSONATION_URL,
650+
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
651651
ImpersonatedCredentialsTest.DELEGATES,
652652
ImpersonatedCredentialsTest.QUOTA_PROJECT_ID);
653653

@@ -677,12 +677,12 @@ public void fromStream_Impersonation_providesToken_WithoutQuotaProject() throws
677677
.setExpireTime(ImpersonatedCredentialsTest.getDefaultExpireTime());
678678
transportFactory
679679
.getTransport()
680-
.setAccessTokenEndpoint(ImpersonatedCredentialsTest.IMPERSONATION_URL);
680+
.setAccessTokenEndpoint(ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL);
681681
transportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "");
682682

683683
InputStream impersonationCredentialsStream =
684684
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
685-
ImpersonatedCredentialsTest.IMPERSONATION_URL,
685+
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
686686
ImpersonatedCredentialsTest.DELEGATES,
687687
null);
688688

0 commit comments

Comments
 (0)