Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit 4b2bdf8

Browse files
authored
feat: Add TrustBoundaries support for ExternalAccounts. (#1836)
* Initial Changes for trust boundary structure without TB enabled check and header value. # Conflicts: # oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java # oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java # oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java # Conflicts: # oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java # oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java # oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java # oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java # oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java # oauth2_http/java/com/google/auth/oauth2/TrustBoundary.java * Added changes to authenticate TB endpoint, add correct TB headers. * Added unit tests for Trust Boundary for Service accounts. Updated. the trust boundary enabler env variable * Formatting changes * Trust Boundary Recovery * Changed key for encodedLocations and changed tests. * minor fixes * External Account Initial Changes. * Added tests for all excepting ExternalAccountCredentials. * Added tests for ExternalAccountCredentials trust boundary. Added comments regarding a separate mock for trust boundary. * Some nit fixes. * Fixed test failures. * TrustBoundaryUrl's universe domain is now configured from the client's universe domain. * Formatted ExternalAccountCredentials. * assertThrows changes. Fixed TestUtils getDefaultExpireTime to represent time in UTC. * Error thrown when audience doesn't match pattern is IllegalStateException and format changes. * Nit fixes. * Imports are listed. setTrustBoundaries removed. * Added doc reference for Invalid WF and WL audience.
1 parent 626335b commit 4b2bdf8

19 files changed

Lines changed: 518 additions & 45 deletions

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

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131

3232
package com.google.auth.oauth2;
3333

34+
import static com.google.auth.oauth2.OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL;
3435
import static com.google.auth.oauth2.OAuth2Utils.JSON_FACTORY;
36+
import static com.google.auth.oauth2.OAuth2Utils.WORKFORCE_AUDIENCE_PATTERN;
3537
import static com.google.common.base.Preconditions.checkNotNull;
3638

3739
import com.google.api.client.http.GenericUrl;
@@ -44,6 +46,7 @@
4446
import com.google.api.client.json.JsonObjectParser;
4547
import com.google.api.client.util.GenericData;
4648
import com.google.api.client.util.Preconditions;
49+
import com.google.api.core.InternalApi;
4750
import com.google.auth.http.HttpTransportFactory;
4851
import com.google.common.base.MoreObjects;
4952
import com.google.common.io.BaseEncoding;
@@ -55,6 +58,7 @@
5558
import java.util.Date;
5659
import java.util.Map;
5760
import java.util.Objects;
61+
import java.util.regex.Matcher;
5862
import javax.annotation.Nullable;
5963

6064
/**
@@ -75,12 +79,12 @@
7579
* }
7680
* </pre>
7781
*/
78-
public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials {
82+
public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials
83+
implements TrustBoundaryProvider {
7984

8085
private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
8186

8287
private static final long serialVersionUID = -2181779590486283287L;
83-
8488
private final String transportFactoryClassName;
8589
private final String audience;
8690
private final String tokenUrl;
@@ -210,10 +214,28 @@ public AccessToken refreshAccessToken() throws IOException {
210214
this.refreshToken = refreshToken;
211215
}
212216

213-
return AccessToken.newBuilder()
214-
.setExpirationTime(expiresAtMilliseconds)
215-
.setTokenValue(accessToken)
216-
.build();
217+
AccessToken newAccessToken =
218+
AccessToken.newBuilder()
219+
.setExpirationTime(expiresAtMilliseconds)
220+
.setTokenValue(accessToken)
221+
.build();
222+
223+
refreshTrustBoundary(newAccessToken, transportFactory);
224+
return newAccessToken;
225+
}
226+
227+
@InternalApi
228+
@Override
229+
public String getTrustBoundaryUrl() throws IOException {
230+
Matcher matcher = WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience());
231+
if (!matcher.matches()) {
232+
throw new IllegalStateException(
233+
"The provided audience is not in the correct format for a workforce pool. "
234+
+ "Refer: https://docs.cloud.google.com/iam/docs/principal-identifiers");
235+
}
236+
String poolId = matcher.group("pool");
237+
return String.format(
238+
IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL, getUniverseDomain(), poolId);
217239
}
218240

219241
@Nullable

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@
3131

3232
package com.google.auth.oauth2;
3333

34+
import static com.google.auth.oauth2.OAuth2Utils.WORKFORCE_AUDIENCE_PATTERN;
35+
import static com.google.auth.oauth2.OAuth2Utils.WORKLOAD_AUDIENCE_PATTERN;
3436
import static com.google.common.base.Preconditions.checkNotNull;
3537

3638
import com.google.api.client.http.HttpHeaders;
3739
import com.google.api.client.json.GenericJson;
3840
import com.google.api.client.json.JsonObjectParser;
3941
import com.google.api.client.util.Data;
42+
import com.google.api.core.InternalApi;
4043
import com.google.auth.RequestMetadataCallback;
4144
import com.google.auth.http.HttpTransportFactory;
4245
import com.google.common.base.MoreObjects;
@@ -55,6 +58,7 @@
5558
import java.util.Locale;
5659
import java.util.Map;
5760
import java.util.concurrent.Executor;
61+
import java.util.regex.Matcher;
5862
import java.util.regex.Pattern;
5963
import javax.annotation.Nullable;
6064

@@ -64,7 +68,8 @@
6468
* <p>Handles initializing external credentials, calls to the Security Token Service, and service
6569
* account impersonation.
6670
*/
67-
public abstract class ExternalAccountCredentials extends GoogleCredentials {
71+
public abstract class ExternalAccountCredentials extends GoogleCredentials
72+
implements TrustBoundaryProvider {
6873

6974
private static final long serialVersionUID = 8049126194174465023L;
7075

@@ -527,7 +532,11 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
527532
this.impersonatedCredentials = this.buildImpersonatedCredentials();
528533
}
529534
if (this.impersonatedCredentials != null) {
530-
return this.impersonatedCredentials.refreshAccessToken();
535+
AccessToken accessToken = this.impersonatedCredentials.refreshAccessToken();
536+
// After the impersonated credential refreshes, its trust boundary is
537+
// also refreshed. That is the trust boundary we will use.
538+
this.trustBoundary = this.impersonatedCredentials.getTrustBoundary();
539+
return accessToken;
531540
}
532541

533542
StsRequestHandler.Builder requestHandler =
@@ -556,7 +565,9 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
556565
}
557566

558567
StsTokenExchangeResponse response = requestHandler.build().exchangeToken();
559-
return response.getAccessToken();
568+
AccessToken accessToken = response.getAccessToken();
569+
refreshTrustBoundary(accessToken, transportFactory);
570+
return accessToken;
560571
}
561572

562573
/**
@@ -613,6 +624,34 @@ public String getServiceAccountEmail() {
613624
return ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
614625
}
615626

627+
@InternalApi
628+
@Override
629+
public String getTrustBoundaryUrl() {
630+
Matcher workforceMatcher = WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience());
631+
if (workforceMatcher.matches()) {
632+
String poolId = workforceMatcher.group("pool");
633+
return String.format(
634+
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL,
635+
getUniverseDomain(),
636+
poolId);
637+
}
638+
639+
Matcher workloadMatcher = WORKLOAD_AUDIENCE_PATTERN.matcher(getAudience());
640+
if (workloadMatcher.matches()) {
641+
String projectNumber = workloadMatcher.group("project");
642+
String poolId = workloadMatcher.group("pool");
643+
return String.format(
644+
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKLOAD_POOL,
645+
getUniverseDomain(),
646+
projectNumber,
647+
poolId);
648+
}
649+
650+
throw new IllegalStateException(
651+
"The provided audience is not in a valid format for either a workload identity pool or a workforce pool."
652+
+ " Refer: https://docs.cloud.google.com/iam/docs/principal-identifiers");
653+
}
654+
616655
@Nullable
617656
public String getClientId() {
618657
return clientId;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ String getFileType() {
108108
private final String universeDomain;
109109
private final boolean isExplicitUniverseDomain;
110110

111-
private TrustBoundary trustBoundary;
111+
TrustBoundary trustBoundary;
112112

113113
protected final String quotaProjectId;
114114

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import java.util.List;
6969
import java.util.Map;
7070
import java.util.Set;
71+
import java.util.regex.Pattern;
7172

7273
/**
7374
* Internal utilities for the com.google.auth.oauth2 namespace.
@@ -93,9 +94,6 @@ public class OAuth2Utils {
9394
static final URI TOKEN_SERVER_URI = URI.create("https://oauth2.googleapis.com/token");
9495

9596
static final URI TOKEN_REVOKE_URI = URI.create("https://oauth2.googleapis.com/revoke");
96-
97-
static final String IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT =
98-
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s/allowedLocations";
9997
static final URI USER_AUTH_URI = URI.create("https://accounts.google.com/o/oauth2/auth");
10098

10199
public static final String CLOUD_PLATFORM_SCOPE =
@@ -120,6 +118,22 @@ public class OAuth2Utils {
120118
static final double RETRY_MULTIPLIER = 2;
121119
static final int DEFAULT_NUMBER_OF_RETRIES = 3;
122120

121+
static final Pattern WORKFORCE_AUDIENCE_PATTERN =
122+
Pattern.compile(
123+
"^//iam.googleapis.com/locations/(?<location>[^/]+)/workforcePools/(?<pool>[^/]+)/providers/(?<provider>[^/]+)$");
124+
static final Pattern WORKLOAD_AUDIENCE_PATTERN =
125+
Pattern.compile(
126+
"^//iam.googleapis.com/projects/(?<project>[^/]+)/locations/(?<location>[^/]+)/workloadIdentityPools/(?<pool>[^/]+)/providers/(?<provider>[^/]+)$");
127+
128+
static final String IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT =
129+
"https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s/allowedLocations";
130+
131+
static final String IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL =
132+
"https://iamcredentials.%s/v1/locations/global/workforcePools/%s/allowedLocations";
133+
134+
static final String IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKLOAD_POOL =
135+
"https://iamcredentials.%s/v1/projects/%s/locations/global/workloadIdentityPools/%s/allowedLocations";
136+
123137
// Includes expected server errors from Google token endpoint
124138
// Other 5xx codes are either not used or retries are unlikely to succeed
125139
public static final Set<Integer> TOKEN_ENDPOINT_RETRYABLE_STATUS_CODES =

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,7 @@ static TrustBoundary refresh(
184184

185185
// Add the cached trust boundary header, if available.
186186
if (cachedTrustBoundary != null) {
187-
String headerValue =
188-
cachedTrustBoundary.isNoOp() ? "" : cachedTrustBoundary.getEncodedLocations();
189-
request.getHeaders().set(TRUST_BOUNDARY_KEY, headerValue);
187+
request.getHeaders().set(TRUST_BOUNDARY_KEY, cachedTrustBoundary.getEncodedLocations());
190188
}
191189

192190
// Add retry logic

oauth2_http/javatests/com/google/auth/TestUtils.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.google.api.client.json.gson.GsonFactory;
4343
import com.google.auth.http.AuthHttpConstants;
4444
import com.google.common.base.Splitter;
45+
import com.google.common.collect.ImmutableList;
4546
import com.google.common.collect.Lists;
4647
import java.io.ByteArrayInputStream;
4748
import java.io.IOException;
@@ -55,6 +56,7 @@
5556
import java.util.HashMap;
5657
import java.util.List;
5758
import java.util.Map;
59+
import java.util.TimeZone;
5860
import javax.annotation.Nullable;
5961

6062
/** Utilities for test code under com.google.auth. */
@@ -64,6 +66,9 @@ public class TestUtils {
6466
URI.create("https://auth.cloud.google/authorize");
6567
public static final URI WORKFORCE_IDENTITY_FEDERATION_TOKEN_SERVER_URI =
6668
URI.create("https://sts.googleapis.com/v1/oauthtoken");
69+
public static final String TRUST_BOUNDARY_ENCODED_LOCATION = "0x800000";
70+
public static final List<String> TRUST_BOUNDARY_LOCATIONS =
71+
ImmutableList.of("us-central1", "us-central2");
6772

6873
private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
6974

@@ -147,7 +152,9 @@ public static String getDefaultExpireTime() {
147152
Calendar calendar = Calendar.getInstance();
148153
calendar.setTime(new Date());
149154
calendar.add(Calendar.SECOND, 300);
150-
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(calendar.getTime());
155+
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
156+
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
157+
return dateFormat.format(calendar.getTime());
151158
}
152159

153160
private TestUtils() {}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,4 +1399,34 @@ public AwsSecurityCredentials getCredentials(ExternalAccountSupplierContext cont
13991399
return credentials;
14001400
}
14011401
}
1402+
1403+
@Test
1404+
public void testRefresh_trustBoundarySuccess() throws IOException {
1405+
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
1406+
TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
1407+
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
1408+
1409+
MockExternalAccountCredentialsTransportFactory transportFactory =
1410+
new MockExternalAccountCredentialsTransportFactory();
1411+
1412+
AwsSecurityCredentialsSupplier supplier =
1413+
new TestAwsSecurityCredentialsSupplier("test", programmaticAwsCreds, null, null);
1414+
1415+
AwsCredentials awsCredential =
1416+
AwsCredentials.newBuilder()
1417+
.setAwsSecurityCredentialsSupplier(supplier)
1418+
.setHttpTransportFactory(transportFactory)
1419+
.setAudience(
1420+
"//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/pool/providers/provider")
1421+
.setTokenUrl(STS_URL)
1422+
.setSubjectTokenType("subjectTokenType")
1423+
.build();
1424+
1425+
awsCredential.refresh();
1426+
1427+
TrustBoundary trustBoundary = awsCredential.getTrustBoundary();
1428+
assertNotNull(trustBoundary);
1429+
assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
1430+
TrustBoundary.setEnvironmentProviderForTest(null);
1431+
}
14021432
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import static com.google.auth.oauth2.ComputeEngineCredentials.METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE;
3535
import static com.google.auth.oauth2.ImpersonatedCredentialsTest.SA_CLIENT_EMAIL;
36+
import static com.google.auth.oauth2.TrustBoundary.TRUST_BOUNDARY_KEY;
3637
import static org.junit.Assert.assertArrayEquals;
3738
import static org.junit.Assert.assertEquals;
3839
import static org.junit.Assert.assertFalse;
@@ -1159,15 +1160,17 @@ public void refresh_trustBoundarySuccess() throws IOException {
11591160
String defaultAccountEmail = "default@email.com";
11601161
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
11611162
TrustBoundary trustBoundary =
1162-
new TrustBoundary("0x80000", Collections.singletonList("us-central1"));
1163+
new TrustBoundary(
1164+
TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, TestUtils.TRUST_BOUNDARY_LOCATIONS);
11631165
transportFactory.transport.setTrustBoundary(trustBoundary);
11641166
transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
11651167

11661168
ComputeEngineCredentials credentials =
11671169
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
11681170

11691171
Map<String, List<String>> headers = credentials.getRequestMetadata();
1170-
assertEquals(headers.get("x-allowed-locations"), Arrays.asList("0x80000"));
1172+
assertEquals(
1173+
headers.get(TRUST_BOUNDARY_KEY), Arrays.asList(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION));
11711174
}
11721175

11731176
@Test

0 commit comments

Comments
 (0)