Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
Expand All @@ -75,12 +77,19 @@
* }
* </pre>
*/
public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials {
public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials
implements TrustBoundaryProvider {

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

private static final long serialVersionUID = -2181779590486283287L;

private static final String WORKFORCE_POOL_URL_FORMAT =
"https://iamcredentials.googleapis.com/v1/locations/global/workforcePools/%s/allowedLocations";
Comment thread
vverman marked this conversation as resolved.
Outdated
private static final Pattern WORKFORCE_PATTERN =
Pattern.compile(
"^//iam.googleapis.com/locations/(?<location>[^/]+)/workforcePools/(?<pool>[^/]+)/providers/(?<provider>[^/]+)$");

private final String transportFactoryClassName;
private final String audience;
private final String tokenUrl;
Expand Down Expand Up @@ -210,10 +219,25 @@ public AccessToken refreshAccessToken() throws IOException {
this.refreshToken = refreshToken;
}

return AccessToken.newBuilder()
.setExpirationTime(expiresAtMilliseconds)
.setTokenValue(accessToken)
.build();
AccessToken newAccessToken =
AccessToken.newBuilder()
.setExpirationTime(expiresAtMilliseconds)
.setTokenValue(accessToken)
.build();

refreshTrustBoundary(newAccessToken, transportFactory);
return newAccessToken;
}

@Override
Comment thread
vverman marked this conversation as resolved.
public String getTrustBoundaryUrl() throws IOException {
Matcher matcher = WORKFORCE_PATTERN.matcher(getAudience());
if (!matcher.matches()) {
throw new IOException(
"The provided audience is not in the correct format for a workforce pool.");
Comment thread
vverman marked this conversation as resolved.
Outdated
}
String poolId = matcher.group("pool");
return String.format(WORKFORCE_POOL_URL_FORMAT, poolId);
}

@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

Expand All @@ -64,7 +65,8 @@
* <p>Handles initializing external credentials, calls to the Security Token Service, and service
* account impersonation.
*/
public abstract class ExternalAccountCredentials extends GoogleCredentials {
public abstract class ExternalAccountCredentials extends GoogleCredentials
implements TrustBoundaryProvider {

private static final long serialVersionUID = 8049126194174465023L;

Expand Down Expand Up @@ -98,6 +100,18 @@ public abstract class ExternalAccountCredentials extends GoogleCredentials {

private EnvironmentProvider environmentProvider;

private static final String WORKFORCE_POOL_URL_FORMAT =
"https://iamcredentials.googleapis.com/v1/locations/global/workforcePools/%s/allowedLocations";
private static final String WORKLOAD_POOL_URL_FORMAT =
"https://iamcredentials.googleapis.com/v1/projects/%s/locations/global/workloadIdentityPools/%s/allowedLocations";

Comment thread
vverman marked this conversation as resolved.
Outdated
private static final Pattern WORKFORCE_PATTERN =
Pattern.compile(
"^//iam.googleapis.com/locations/(?<location>[^/]+)/workforcePools/(?<pool>[^/]+)/providers/(?<provider>[^/]+)$");
private static final Pattern WORKLOAD_PATTERN =
Pattern.compile(
"^//iam.googleapis.com/projects/(?<project>[^/]+)/locations/(?<location>[^/]+)/workloadIdentityPools/(?<pool>[^/]+)/providers/(?<provider>[^/]+)$");
Comment thread
lqiu96 marked this conversation as resolved.
Outdated

/**
* Constructor with minimum identifying information and custom HTTP transport. Does not support
* workforce credentials.
Expand Down Expand Up @@ -527,7 +541,11 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
this.impersonatedCredentials = this.buildImpersonatedCredentials();
}
if (this.impersonatedCredentials != null) {
return this.impersonatedCredentials.refreshAccessToken();
AccessToken accessToken = this.impersonatedCredentials.refreshAccessToken();
// After the impersonated credential refreshes, its trust boundary is
// also refreshed. That is the trust boundary we will use.
setTrustBoundary(this.impersonatedCredentials.getTrustBoundary());
Comment thread
lqiu96 marked this conversation as resolved.
Outdated
Comment thread
vverman marked this conversation as resolved.
Outdated
return accessToken;
}

StsRequestHandler.Builder requestHandler =
Expand Down Expand Up @@ -556,7 +574,9 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
}

StsTokenExchangeResponse response = requestHandler.build().exchangeToken();
return response.getAccessToken();
AccessToken accessToken = response.getAccessToken();
refreshTrustBoundary(accessToken, transportFactory);
return accessToken;
}

/**
Expand Down Expand Up @@ -613,6 +633,29 @@ public String getServiceAccountEmail() {
return ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
}

// todo Add doc comment.
Comment thread
vverman marked this conversation as resolved.
Outdated
@Override
public String getTrustBoundaryUrl() throws IOException {
Comment thread
vverman marked this conversation as resolved.
Outdated
if (isWorkforcePoolConfiguration()) {
Matcher matcher = WORKFORCE_PATTERN.matcher(getAudience());
if (!matcher.matches()) {
throw new IOException(
"The provided audience is not in the correct format for a workforce pool.");
}
String poolId = matcher.group("pool");
return String.format(WORKFORCE_POOL_URL_FORMAT, poolId);
} else {
Matcher matcher = WORKLOAD_PATTERN.matcher(getAudience());
if (!matcher.matches()) {
throw new IOException(
"The provided audience is not in the correct format for a workload identity pool.");
Comment thread
vverman marked this conversation as resolved.
Outdated
}
String projectNumber = matcher.group("project");
String poolId = matcher.group("pool");
return String.format(WORKLOAD_POOL_URL_FORMAT, projectNumber, poolId);
}
Comment thread
vverman marked this conversation as resolved.
}
Comment thread
vverman marked this conversation as resolved.

@Nullable
public String getClientId() {
return clientId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,10 @@ TrustBoundary getTrustBoundary() {
return trustBoundary;
}

protected void setTrustBoundary(TrustBoundary trustBoundary) {
this.trustBoundary = trustBoundary;
}
Comment thread
vverman marked this conversation as resolved.
Outdated

/**
* Refreshes the trust boundary by making a call to the trust boundary URL.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,7 @@ public static class Builder extends GoogleCredentials.Builder {
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS;
private boolean useJwtAccessWithScope = false;
private boolean defaultRetriesEnabled = true;
private TrustBoundary trustBoundary;

protected Builder() {}

Expand All @@ -1180,6 +1181,7 @@ protected Builder(ServiceAccountCredentials credentials) {
this.lifetime = credentials.lifetime;
this.useJwtAccessWithScope = credentials.useJwtAccessWithScope;
this.defaultRetriesEnabled = credentials.defaultRetriesEnabled;
this.trustBoundary = credentials.getTrustBoundary();
Comment thread
vverman marked this conversation as resolved.
Outdated
}

@CanIgnoreReturnValue
Expand Down
4 changes: 1 addition & 3 deletions oauth2_http/java/com/google/auth/oauth2/TrustBoundary.java
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,7 @@ static TrustBoundary refresh(

// Add the cached trust boundary header, if available.
if (cachedTrustBoundary != null) {
String headerValue =
cachedTrustBoundary.isNoOp() ? "" : cachedTrustBoundary.getEncodedLocations();
request.getHeaders().set(TRUST_BOUNDARY_KEY, headerValue);
request.getHeaders().set(TRUST_BOUNDARY_KEY, cachedTrustBoundary.getEncodedLocations());
}

// Add retry logic
Expand Down
6 changes: 5 additions & 1 deletion oauth2_http/javatests/com/google/auth/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.google.api.client.json.gson.GsonFactory;
import com.google.auth.http.AuthHttpConstants;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.ByteArrayInputStream;
import java.io.IOException;
Expand All @@ -64,6 +65,9 @@ public class TestUtils {
URI.create("https://auth.cloud.google/authorize");
public static final URI WORKFORCE_IDENTITY_FEDERATION_TOKEN_SERVER_URI =
URI.create("https://sts.googleapis.com/v1/oauthtoken");
public static final String TRUST_BOUNDARY_ENCODED_LOCATION = "0x800000";
public static final List<String> TRUST_BOUNDARY_LOCATIONS =
ImmutableList.of("us-central1", "us-central2");

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

Expand Down Expand Up @@ -146,7 +150,7 @@ public static HttpResponseException buildHttpResponseException(
public static String getDefaultExpireTime() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.SECOND, 300);
calendar.add(Calendar.SECOND, 30000);
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(calendar.getTime());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1399,4 +1399,34 @@ public AwsSecurityCredentials getCredentials(ExternalAccountSupplierContext cont
return credentials;
}
}

@Test
public void testRefresh_trustBoundarySuccess() throws IOException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");

MockExternalAccountCredentialsTransportFactory transportFactory =
new MockExternalAccountCredentialsTransportFactory();

AwsSecurityCredentialsSupplier supplier =
new TestAwsSecurityCredentialsSupplier("test", programmaticAwsCreds, null, null);

AwsCredentials awsCredential =
AwsCredentials.newBuilder()
.setAwsSecurityCredentialsSupplier(supplier)
.setHttpTransportFactory(transportFactory)
.setAudience(
"//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/pool/providers/provider")
.setTokenUrl(STS_URL)
.setSubjectTokenType("subjectTokenType")
.build();

awsCredential.refreshAccessToken();
Comment thread
vverman marked this conversation as resolved.
Outdated

TrustBoundary trustBoundary = awsCredential.getTrustBoundary();
assertNotNull(trustBoundary);
assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
TrustBoundary.setEnvironmentProviderForTest(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

import static com.google.auth.oauth2.ComputeEngineCredentials.METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE;
import static com.google.auth.oauth2.ImpersonatedCredentialsTest.SA_CLIENT_EMAIL;
import static com.google.auth.oauth2.TrustBoundary.TRUST_BOUNDARY_KEY;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
Expand Down Expand Up @@ -1159,15 +1160,17 @@ public void refresh_trustBoundarySuccess() throws IOException {
String defaultAccountEmail = "default@email.com";
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
TrustBoundary trustBoundary =
new TrustBoundary("0x80000", Collections.singletonList("us-central1"));
new TrustBoundary(
TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, TestUtils.TRUST_BOUNDARY_LOCATIONS);
transportFactory.transport.setTrustBoundary(trustBoundary);
transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);

ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();

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

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
Expand Down Expand Up @@ -62,6 +63,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -132,6 +134,11 @@ public void setup() {
transportFactory = new MockExternalAccountAuthorizedUserCredentialsTransportFactory();
}

@After
public void tearDown() {
TrustBoundary.setEnvironmentProviderForTest(null);
}

@Test
public void builder_allFields() throws IOException {
ExternalAccountAuthorizedUserCredentials credentials =
Expand Down Expand Up @@ -1216,6 +1223,56 @@ public void toString_expectedFormat() {
assertEquals(expectedToString, credentials.toString());
}

@Test
public void testRefresh_trustBoundarySuccess() throws IOException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");

ExternalAccountAuthorizedUserCredentials credentials =
ExternalAccountAuthorizedUserCredentials.newBuilder()
.setHttpTransportFactory(transportFactory)
.setAudience(AUDIENCE)
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setRefreshToken(REFRESH_TOKEN)
.setTokenUrl(TOKEN_URL)
.build();

credentials.refresh();
TrustBoundary trustBoundary = credentials.getTrustBoundary();
assertNotNull(trustBoundary);
assertEquals(TestUtils.TRUST_BOUNDARY_ENCODED_LOCATION, trustBoundary.getEncodedLocations());
TrustBoundary.setEnvironmentProviderForTest(null);
Comment thread
vverman marked this conversation as resolved.
Outdated
}

@Test
public void testRefresh_trustBoundaryFails_incorrectAudience() throws IOException {
TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");

ExternalAccountAuthorizedUserCredentials credentials =
ExternalAccountAuthorizedUserCredentials.newBuilder()
.setHttpTransportFactory(transportFactory)
.setAudience("audience")
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setRefreshToken(REFRESH_TOKEN)
.setTokenUrl(TOKEN_URL)
.build();

try {
credentials.refresh();
fail("Expected IOException to be thrown.");
} catch (IOException e) {
assertEquals(
"The provided audience is not in the correct format for a workforce pool.",
e.getMessage());
}
Comment thread
vverman marked this conversation as resolved.
Outdated
TrustBoundary.setEnvironmentProviderForTest(null);
Comment thread
vverman marked this conversation as resolved.
Outdated
}

@Test
public void serialize() throws IOException, ClassNotFoundException {
ExternalAccountAuthorizedUserCredentials credentials =
Expand Down
Loading