Skip to content

Commit 626335b

Browse files
authored
feat: Add TrustBoundaries support for ServiceAccounts (#1808)
Added support for trust boundaries for service accounts, impersonated and GCE flows. # 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 * Added changes to authenticate TB endpoint, add correct TB headers. * Corrected Response Struct for TB Look up. Fixed Trust Boundary Enabled Logic. * Added separate refreshTrustBoundaryAndGetAdditionalHeaders in OAuth2Credentials. * TB Refresh Now happens in each token refresh. * Removed unnecessary refreshTrustBoundaryAndGetAdditionalHeaders as tb refresh now happens within access token refresh of individual classes. * 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 * Const string for Service Account Lookup IAM URI. Network call removed from locking. Other doc changes. * Added docs for public methods * Addressed PR comments. All refresh logic within Google Credentials. Use supportsTrustBoundary to identify whether a child class supports tb prior to refresh. * Formatting changes * AssertNull and AssertThrows changes. Documentation changes. * Reinstated TrustBoundaryProvider.java and addressed PR comments. * Update Copyright TrustBoundaryProvider.java * getTrustBoundaryUrl now called within refreshTrustBoundaries(...) * lint change
1 parent 77cd082 commit 626335b

16 files changed

+1030
-22
lines changed

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import com.google.api.client.http.HttpStatusCodes;
4242
import com.google.api.client.json.JsonObjectParser;
4343
import com.google.api.client.util.GenericData;
44+
import com.google.api.core.InternalApi;
4445
import com.google.auth.CredentialTypeForMetrics;
4546
import com.google.auth.Credentials;
4647
import com.google.auth.Retryable;
@@ -82,7 +83,7 @@
8283
* <p>These credentials use the IAM API to sign data. See {@link #sign(byte[])} for more details.
8384
*/
8485
public class ComputeEngineCredentials extends GoogleCredentials
85-
implements ServiceAccountSigner, IdTokenProvider {
86+
implements ServiceAccountSigner, IdTokenProvider, TrustBoundaryProvider {
8687

8788
static final String METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE =
8889
"Empty content from metadata token server request.";
@@ -385,8 +386,11 @@ public AccessToken refreshAccessToken() throws IOException {
385386
int expiresInSeconds =
386387
OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
387388
long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000;
389+
AccessToken newAccessToken = new AccessToken(accessToken, new Date(expiresAtMilliseconds));
388390

389-
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
391+
refreshTrustBoundary(newAccessToken, transportFactory);
392+
393+
return newAccessToken;
390394
}
391395

392396
/**
@@ -703,6 +707,15 @@ public String getAccount() {
703707
return principal;
704708
}
705709

710+
@InternalApi
711+
@Override
712+
public String getTrustBoundaryUrl() throws IOException {
713+
return String.format(
714+
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
715+
getUniverseDomain(),
716+
getAccount());
717+
}
718+
706719
/**
707720
* Signs the provided bytes using the private key associated with the service account.
708721
*

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

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.google.api.client.json.JsonFactory;
3636
import com.google.api.client.json.JsonObjectParser;
3737
import com.google.api.client.util.Preconditions;
38+
import com.google.api.core.InternalApi;
3839
import com.google.api.core.ObsoleteApi;
3940
import com.google.auth.Credentials;
4041
import com.google.auth.http.HttpTransportFactory;
@@ -107,6 +108,8 @@ String getFileType() {
107108
private final String universeDomain;
108109
private final boolean isExplicitUniverseDomain;
109110

111+
private TrustBoundary trustBoundary;
112+
110113
protected final String quotaProjectId;
111114

112115
private static final DefaultCredentialsProvider defaultCredentialsProvider =
@@ -331,6 +334,60 @@ public GoogleCredentials createWithQuotaProject(String quotaProject) {
331334
return this.toBuilder().setQuotaProjectId(quotaProject).build();
332335
}
333336

337+
@VisibleForTesting
338+
TrustBoundary getTrustBoundary() {
339+
return trustBoundary;
340+
}
341+
342+
/**
343+
* Refreshes the trust boundary by making a call to the trust boundary URL.
344+
*
345+
* @param newAccessToken The new access token to be used for the refresh.
346+
* @param transportFactory The HTTP transport factory to be used for the refresh.
347+
* @throws IOException If the refresh fails and no cached value is available.
348+
*/
349+
@InternalApi
350+
void refreshTrustBoundary(AccessToken newAccessToken, HttpTransportFactory transportFactory)
351+
throws IOException {
352+
353+
if (!(this instanceof TrustBoundaryProvider)
354+
|| !TrustBoundary.isTrustBoundaryEnabled()
355+
|| !isDefaultUniverseDomain()) {
356+
return;
357+
}
358+
359+
String trustBoundaryUrl = ((TrustBoundaryProvider) this).getTrustBoundaryUrl();
360+
TrustBoundary cachedTrustBoundary;
361+
362+
synchronized (lock) {
363+
// Do not refresh if the cached value is already NO_OP.
364+
if (trustBoundary != null && trustBoundary.isNoOp()) {
365+
return;
366+
}
367+
cachedTrustBoundary = trustBoundary;
368+
}
369+
370+
TrustBoundary newTrustBoundary;
371+
try {
372+
newTrustBoundary =
373+
TrustBoundary.refresh(
374+
transportFactory, trustBoundaryUrl, newAccessToken, cachedTrustBoundary);
375+
} catch (IOException e) {
376+
// If refresh fails, check for a cached value.
377+
if (cachedTrustBoundary == null) {
378+
// No cached value, so fail hard.
379+
throw new IOException(
380+
"Failed to refresh trust boundary and no cached value is available.", e);
381+
}
382+
return;
383+
}
384+
385+
// A lock is required to safely update the shared field.
386+
synchronized (lock) {
387+
trustBoundary = newTrustBoundary;
388+
}
389+
}
390+
334391
/**
335392
* Gets the universe domain for the credential.
336393
*
@@ -384,12 +441,15 @@ static Map<String, List<String>> addQuotaProjectIdToRequestMetadata(
384441

385442
@Override
386443
protected Map<String, List<String>> getAdditionalHeaders() {
387-
Map<String, List<String>> headers = super.getAdditionalHeaders();
388-
String quotaProjectId = this.getQuotaProjectId();
389-
if (quotaProjectId != null) {
390-
return addQuotaProjectIdToRequestMetadata(quotaProjectId, headers);
444+
Map<String, List<String>> headers = new HashMap<>(super.getAdditionalHeaders());
445+
446+
if (this.trustBoundary != null) {
447+
String headerValue = trustBoundary.isNoOp() ? "" : trustBoundary.getEncodedLocations();
448+
headers.put(TrustBoundary.TRUST_BOUNDARY_KEY, Collections.singletonList(headerValue));
391449
}
392-
return headers;
450+
451+
String quotaProjectId = this.getQuotaProjectId();
452+
return addQuotaProjectIdToRequestMetadata(quotaProjectId, headers);
393453
}
394454

395455
/** Default constructor. */

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import com.google.api.client.http.json.JsonHttpContent;
4444
import com.google.api.client.json.JsonObjectParser;
4545
import com.google.api.client.util.GenericData;
46+
import com.google.api.core.InternalApi;
4647
import com.google.auth.CredentialTypeForMetrics;
4748
import com.google.auth.ServiceAccountSigner;
4849
import com.google.auth.http.HttpCredentialsAdapter;
@@ -95,7 +96,7 @@
9596
* </pre>
9697
*/
9798
public class ImpersonatedCredentials extends GoogleCredentials
98-
implements ServiceAccountSigner, IdTokenProvider {
99+
implements ServiceAccountSigner, IdTokenProvider, TrustBoundaryProvider {
99100

100101
private static final long serialVersionUID = -2133257318957488431L;
101102
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";
@@ -325,6 +326,15 @@ public GoogleCredentials getSourceCredentials() {
325326
return sourceCredentials;
326327
}
327328

329+
@InternalApi
330+
@Override
331+
public String getTrustBoundaryUrl() throws IOException {
332+
return String.format(
333+
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
334+
getUniverseDomain(),
335+
getAccount());
336+
}
337+
328338
int getLifetime() {
329339
return this.lifetime;
330340
}
@@ -593,7 +603,11 @@ public AccessToken refreshAccessToken() throws IOException {
593603
format.setCalendar(calendar);
594604
try {
595605
Date date = format.parse(expireTime);
596-
return new AccessToken(accessToken, date);
606+
AccessToken newAccessToken = new AccessToken(accessToken, date);
607+
608+
refreshTrustBoundary(newAccessToken, transportFactory);
609+
610+
return newAccessToken;
597611
} catch (ParseException pe) {
598612
throw new IOException("Error parsing expireTime: " + pe.getMessage());
599613
}

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
import java.util.Map;
6060
import java.util.Objects;
6161
import java.util.ServiceLoader;
62-
import java.util.concurrent.Callable;
6362
import java.util.concurrent.ExecutionException;
6463
import java.util.concurrent.Executor;
6564
import javax.annotation.Nullable;
@@ -264,11 +263,8 @@ private AsyncRefreshResult getOrCreateRefreshTask() {
264263

265264
final ListenableFutureTask<OAuthValue> task =
266265
ListenableFutureTask.create(
267-
new Callable<OAuthValue>() {
268-
@Override
269-
public OAuthValue call() throws Exception {
270-
return OAuthValue.create(refreshAccessToken(), getAdditionalHeaders());
271-
}
266+
() -> {
267+
return OAuthValue.create(refreshAccessToken(), getAdditionalHeaders());
272268
});
273269

274270
refreshTask = new RefreshTask(task, new RefreshTaskListener(task));
@@ -373,7 +369,7 @@ public AccessToken refreshAccessToken() throws IOException {
373369
/**
374370
* Provide additional headers to return as request metadata.
375371
*
376-
* @return additional headers
372+
* @return additional headers.
377373
*/
378374
protected Map<String, List<String>> getAdditionalHeaders() {
379375
return EMPTY_EXTRA_HEADERS;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ public class OAuth2Utils {
9393
static final URI TOKEN_SERVER_URI = URI.create("https://oauth2.googleapis.com/token");
9494

9595
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";
9699
static final URI USER_AUTH_URI = URI.create("https://accounts.google.com/o/oauth2/auth");
97100

98101
public static final String CLOUD_PLATFORM_SCOPE =

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import com.google.api.client.util.GenericData;
5252
import com.google.api.client.util.Joiner;
5353
import com.google.api.client.util.Preconditions;
54+
import com.google.api.core.InternalApi;
5455
import com.google.auth.CredentialTypeForMetrics;
5556
import com.google.auth.Credentials;
5657
import com.google.auth.RequestMetadataCallback;
@@ -89,7 +90,7 @@
8990
* <p>By default uses a JSON Web Token (JWT) to fetch access tokens.
9091
*/
9192
public class ServiceAccountCredentials extends GoogleCredentials
92-
implements ServiceAccountSigner, IdTokenProvider, JwtProvider {
93+
implements ServiceAccountSigner, IdTokenProvider, JwtProvider, TrustBoundaryProvider {
9394

9495
private static final long serialVersionUID = 7807543542681217978L;
9596
private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
@@ -580,7 +581,11 @@ public AccessToken refreshAccessToken() throws IOException {
580581
int expiresInSeconds =
581582
OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
582583
long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000L;
583-
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
584+
AccessToken newAccessToken = new AccessToken(accessToken, new Date(expiresAtMilliseconds));
585+
586+
refreshTrustBoundary(newAccessToken, transportFactory);
587+
588+
return newAccessToken;
584589
}
585590

586591
/**
@@ -823,6 +828,15 @@ public boolean getUseJwtAccessWithScope() {
823828
return useJwtAccessWithScope;
824829
}
825830

831+
@InternalApi
832+
@Override
833+
public String getTrustBoundaryUrl() throws IOException {
834+
return String.format(
835+
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
836+
getUniverseDomain(),
837+
getAccount());
838+
}
839+
826840
@VisibleForTesting
827841
JwtCredentials getSelfSignedJwtCredentialsWithScope() {
828842
return selfSignedJwtCredentialsWithScope;

0 commit comments

Comments
 (0)