Skip to content

Commit d58147c

Browse files
committed
External Account Initial Changes.
1 parent dab6d3b commit d58147c

File tree

8 files changed

+724
-22
lines changed

8 files changed

+724
-22
lines changed

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

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import java.util.Date;
5656
import java.util.Map;
5757
import java.util.Objects;
58+
import java.util.regex.Matcher;
59+
import java.util.regex.Pattern;
5860
import javax.annotation.Nullable;
5961

6062
/**
@@ -75,12 +77,19 @@
7577
* }
7678
* </pre>
7779
*/
78-
public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials {
80+
public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials
81+
implements TrustBoundaryProvider {
7982

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

8285
private static final long serialVersionUID = -2181779590486283287L;
8386

87+
private static final String WORKFORCE_POOL_URL_FORMAT =
88+
"https://iamcredentials.googleapis.com/v1/locations/global/workforcePools/%s/allowedLocations";
89+
private static final Pattern WORKFORCE_PATTERN =
90+
Pattern.compile(
91+
"^//iam.googleapis.com/locations/(?<location>[^/]+)/workforcePools/(?<pool>[^/]+)/providers/(?<provider>[^/]+)$");
92+
8493
private final String transportFactoryClassName;
8594
private final String audience;
8695
private final String tokenUrl;
@@ -210,10 +219,30 @@ public AccessToken refreshAccessToken() throws IOException {
210219
this.refreshToken = refreshToken;
211220
}
212221

213-
return AccessToken.newBuilder()
214-
.setExpirationTime(expiresAtMilliseconds)
215-
.setTokenValue(accessToken)
216-
.build();
222+
AccessToken newAccessToken =
223+
AccessToken.newBuilder()
224+
.setExpirationTime(expiresAtMilliseconds)
225+
.setTokenValue(accessToken)
226+
.build();
227+
228+
refreshTrustBoundaries(newAccessToken);
229+
return newAccessToken;
230+
}
231+
232+
@Override
233+
public String getTrustBoundaryUrl() throws IOException {
234+
Matcher matcher = WORKFORCE_PATTERN.matcher(getAudience());
235+
if (!matcher.matches()) {
236+
throw new IOException(
237+
"The provided audience is not in the correct format for a workforce pool.");
238+
}
239+
String poolId = matcher.group("pool");
240+
return String.format(WORKFORCE_POOL_URL_FORMAT, poolId);
241+
}
242+
243+
@Override
244+
public HttpTransportFactory getTransportFactory() {
245+
return transportFactory;
217246
}
218247

219248
@Nullable

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.Locale;
5656
import java.util.Map;
5757
import java.util.concurrent.Executor;
58+
import java.util.regex.Matcher;
5859
import java.util.regex.Pattern;
5960
import javax.annotation.Nullable;
6061

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

6971
private static final long serialVersionUID = 8049126194174465023L;
7072

@@ -98,6 +100,18 @@ public abstract class ExternalAccountCredentials extends GoogleCredentials {
98100

99101
private EnvironmentProvider environmentProvider;
100102

103+
private static final String WORKFORCE_POOL_URL_FORMAT =
104+
"https://iamcredentials.googleapis.com/v1/locations/global/workforcePools/%s/allowedLocations";
105+
private static final String WORKLOAD_POOL_URL_FORMAT =
106+
"https://iamcredentials.googleapis.com/v1/projects/%s/locations/global/workloadIdentityPools/%s/allowedLocations";
107+
108+
private static final Pattern WORKFORCE_PATTERN =
109+
Pattern.compile(
110+
"^//iam.googleapis.com/locations/(?<location>[^/]+)/workforcePools/(?<pool>[^/]+)/providers/(?<provider>[^/]+)$");
111+
private static final Pattern WORKLOAD_PATTERN =
112+
Pattern.compile(
113+
"^//iam.googleapis.com/projects/(?<project>[^/]+)/locations/(?<location>[^/]+)/workloadIdentityPools/(?<pool>[^/]+)/providers/(?<provider>[^/]+)$");
114+
101115
/**
102116
* Constructor with minimum identifying information and custom HTTP transport. Does not support
103117
* workforce credentials.
@@ -527,7 +541,11 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
527541
this.impersonatedCredentials = this.buildImpersonatedCredentials();
528542
}
529543
if (this.impersonatedCredentials != null) {
530-
return this.impersonatedCredentials.refreshAccessToken();
544+
AccessToken accessToken = this.impersonatedCredentials.refreshAccessToken();
545+
// After the impersonated credential refreshes, its trust boundary is
546+
// also refreshed. We need to get the refreshed trust boundary.
547+
setTrustBoundary(this.impersonatedCredentials.getTrustBoundary());
548+
return accessToken;
531549
}
532550

533551
StsRequestHandler.Builder requestHandler =
@@ -556,7 +574,9 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
556574
}
557575

558576
StsTokenExchangeResponse response = requestHandler.build().exchangeToken();
559-
return response.getAccessToken();
577+
AccessToken accessToken = response.getAccessToken();
578+
refreshTrustBoundaries(accessToken);
579+
return accessToken;
560580
}
561581

562582
/**
@@ -613,6 +633,34 @@ public String getServiceAccountEmail() {
613633
return ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
614634
}
615635

636+
// todo Add doc comment.
637+
@Override
638+
public String getTrustBoundaryUrl() throws IOException {
639+
if (isWorkforcePoolConfiguration()) {
640+
Matcher matcher = WORKFORCE_PATTERN.matcher(getAudience());
641+
if (!matcher.matches()) {
642+
throw new IOException(
643+
"The provided audience is not in the correct format for a workforce pool.");
644+
}
645+
String poolId = matcher.group("pool");
646+
return String.format(WORKFORCE_POOL_URL_FORMAT, poolId);
647+
} else {
648+
Matcher matcher = WORKLOAD_PATTERN.matcher(getAudience());
649+
if (!matcher.matches()) {
650+
throw new IOException(
651+
"The provided audience is not in the correct format for a workload identity pool.");
652+
}
653+
String projectNumber = matcher.group("project");
654+
String poolId = matcher.group("pool");
655+
return String.format(WORKLOAD_POOL_URL_FORMAT, projectNumber, poolId);
656+
}
657+
}
658+
659+
@Override
660+
public HttpTransportFactory getTransportFactory() {
661+
return transportFactory;
662+
}
663+
616664
@Nullable
617665
public String getClientId() {
618666
return clientId;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,10 @@ TrustBoundary getTrustBoundary() {
339339
return trustBoundary;
340340
}
341341

342+
protected void setTrustBoundary(TrustBoundary trustBoundary) {
343+
this.trustBoundary = trustBoundary;
344+
}
345+
342346
/**
343347
* Refreshes the trust boundary by making a call to the trust boundary URL.
344348
*

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

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,4 +1399,106 @@ 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+
// MockHttpTransport mockHttpTransport =
1410+
// new MockHttpTransport.Builder()
1411+
// // AWS region call
1412+
// .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent("us-east-1a"))
1413+
// // AWS IAM role name call
1414+
// .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent("roleName"))
1415+
// // AWS credentials call
1416+
// .setLowLevelHttpResponse(
1417+
// new MockLowLevelHttpResponse()
1418+
// .setContent(
1419+
//
1420+
// "{\"Code\":\"Success\",\"AccessKeyId\":\"accessKeyId\",\"SecretAccessKey\":\"secretAccessKey\",\"Token\":\"token\"}"))
1421+
// // STS token call
1422+
// .setLowLevelHttpResponse(
1423+
// new MockLowLevelHttpResponse()
1424+
// .setContentType(Json.MEDIA_TYPE)
1425+
// .setContent(
1426+
// String.format(
1427+
// "{\"access_token\": \"%s\", \"expires_in\": %s, \"token_type\":
1428+
// \"Bearer\"}",
1429+
// "sts_access_token", 3600)))
1430+
// // Trust boundary call
1431+
// .setLowLevelHttpResponse(
1432+
// new MockLowLevelHttpResponse()
1433+
// .setContentType(Json.MEDIA_TYPE)
1434+
// .setContent(
1435+
// "{\"locations\": [\"us-central1\"], \"encodedLocations\": \"0x1\"}"))
1436+
// .build();
1437+
//
1438+
// AwsCredentials credentials =
1439+
// AwsCredentials.newBuilder()
1440+
// .setHttpTransportFactory(() -> mockHttpTransport)
1441+
// .setAudience(
1442+
//
1443+
// "//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/pool/providers/provider")
1444+
// .setSubjectTokenType("subjectTokenType")
1445+
// .setTokenUrl(STS_URL)
1446+
// .setCredentialSource(AWS_CREDENTIAL_SOURCE)
1447+
// .build();
1448+
//
1449+
// credentials.refresh();
1450+
//
1451+
// TrustBoundary trustBoundary = credentials.getTrustBoundary();
1452+
// assertNotNull(trustBoundary);
1453+
// assertEquals("0x1", trustBoundary.getEncodedLocations());
1454+
// }
1455+
//
1456+
// @Test
1457+
// public void testRefresh_trustBoundaryFails() throws IOException {
1458+
// TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
1459+
// TrustBoundary.setEnvironmentProviderForTest(environmentProvider);
1460+
// environmentProvider.setEnv("GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT", "1");
1461+
//
1462+
// MockHttpTransport mockHttpTransport =
1463+
// new MockHttpTransport.Builder()
1464+
// .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent("us-east-1a"))
1465+
// .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent("roleName"))
1466+
// .setLowLevelHttpResponse(
1467+
// new MockLowLevelHttpResponse()
1468+
// .setContent(
1469+
//
1470+
// "{\"Code\":\"Success\",\"AccessKeyId\":\"accessKeyId\",\"SecretAccessKey\":\"secretAccessKey\",\"Token\":\"token\"}"))
1471+
// .setLowLevelHttpResponse(
1472+
// new MockLowLevelHttpResponse()
1473+
// .setContentType(Json.MEDIA_TYPE)
1474+
// .setContent(
1475+
// String.format(
1476+
// "{\"access_token\": \"%s\", \"expires_in\": %s, \"token_type\":
1477+
// \"Bearer\"}",
1478+
// "sts_access_token", 3600)))
1479+
// .setLowLevelHttpResponse(
1480+
// new MockLowLevelHttpResponse()
1481+
// .setStatusCode(404)
1482+
// .setContent("{\"error\": \"not found\"}"))
1483+
// .build();
1484+
//
1485+
// AwsCredentials credentials =
1486+
// AwsCredentials.newBuilder()
1487+
// .setHttpTransportFactory(() -> mockHttpTransport)
1488+
// .setAudience(
1489+
//
1490+
// "//iam.googleapis.com/projects/12345/locations/global/workloadIdentityPools/pool/providers/provider")
1491+
// .setSubjectTokenType("subjectTokenType")
1492+
// .setTokenUrl(STS_URL)
1493+
// .setCredentialSource(AWS_CREDENTIAL_SOURCE)
1494+
// .build();
1495+
//
1496+
// try {
1497+
// credentials.refresh();
1498+
// fail("Expected IOException to be thrown.");
1499+
// } catch (IOException e) {
1500+
// assertEquals(
1501+
// "Failed to refresh trust boundary and no cached value is available.", e.getMessage());
1502+
// }
1503+
// }
14021504
}

0 commit comments

Comments
 (0)