Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -48,8 +48,8 @@
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
Expand All @@ -68,9 +68,6 @@ public abstract class ExternalAccountCredentials extends GoogleCredentials {

private static final long serialVersionUID = 8049126194174465023L;

private static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";

static final String EXECUTABLE_SOURCE_KEY = "executable";

static final String DEFAULT_TOKEN_URL = "https://sts.{UNIVERSE_DOMAIN}/v1/token";
Expand Down Expand Up @@ -200,7 +197,9 @@ protected ExternalAccountCredentials(
this.clientId = clientId;
this.clientSecret = clientSecret;
this.scopes =
(scopes == null || scopes.isEmpty()) ? Arrays.asList(CLOUD_PLATFORM_SCOPE) : scopes;
(scopes == null || scopes.isEmpty())
? Collections.singletonList(OAuth2Utils.CLOUD_PLATFORM_SCOPE)
: scopes;
this.environmentProvider =
environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider;
this.workforcePoolUserProject = null;
Expand Down Expand Up @@ -245,7 +244,7 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)

this.scopes =
(builder.scopes == null || builder.scopes.isEmpty())
? Arrays.asList(CLOUD_PLATFORM_SCOPE)
? Collections.singletonList(OAuth2Utils.CLOUD_PLATFORM_SCOPE)
: builder.scopes;
this.environmentProvider =
builder.environmentProvider == null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
import com.google.auth.oauth2.MetricsUtils.RequestType;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
Expand All @@ -58,9 +60,9 @@
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -99,14 +101,12 @@ public class ImpersonatedCredentials extends GoogleCredentials
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";
private static final int TWELVE_HOURS_IN_SECONDS = 43200;
private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600;
private static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";
private GoogleCredentials sourceCredentials;
private String targetPrincipal;
private final String targetPrincipal;
private List<String> delegates;
private List<String> scopes;
private int lifetime;
private String iamEndpointOverride;
private final List<String> scopes;
private final int lifetime;
private final String iamEndpointOverride;
private final String transportFactoryClassName;
private static final LoggerProvider LOGGER_PROVIDER =
LoggerProvider.forClazz(ImpersonatedCredentials.class);
Expand Down Expand Up @@ -390,6 +390,10 @@ static ImpersonatedCredentials fromJson(
String quotaProjectId;
String targetPrincipal;
String serviceAccountImpersonationUrl;
// This applies to the scopes applied for the impersonated token and not the
// underlying source credential. Default to empty list to keep the existing
// behavior (when json file did not populate a scopes field).
List<String> scopes = ImmutableList.of();
try {
serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
if (json.containsKey("delegates")) {
Expand All @@ -399,6 +403,9 @@ static ImpersonatedCredentials fromJson(
sourceCredentialsType = (String) sourceCredentialsJson.get("type");
quotaProjectId = (String) json.get("quota_project_id");
targetPrincipal = extractTargetPrincipal(serviceAccountImpersonationUrl);
if (json.containsKey("scopes")) {
scopes = ImmutableList.copyOf((List<String>) json.get("scopes"));
}
Comment on lines +406 to +408
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scopes field was recently added and may not exist in existing ImpersonatedCred Json files. Check if it exists

} catch (ClassCastException | NullPointerException | IllegalArgumentException e) {
throw new CredentialFormatException("An invalid input stream was provided.", e);
}
Expand All @@ -421,7 +428,7 @@ static ImpersonatedCredentials fromJson(
.setSourceCredentials(sourceCredentials)
.setTargetPrincipal(targetPrincipal)
.setDelegates(delegates)
.setScopes(new ArrayList<>())
.setScopes(scopes)
.setLifetime(DEFAULT_LIFETIME_IN_SECONDS)
.setHttpTransportFactory(transportFactory)
.setQuotaProjectId(quotaProjectId)
Expand All @@ -436,7 +443,7 @@ public boolean createScopedRequired() {

@Override
public GoogleCredentials createScoped(Collection<String> scopes) {
return toBuilder().setScopes(new ArrayList<>(scopes)).setAccessToken(null).build();
return toBuilder().setScopes(ImmutableList.copyOf(scopes)).setAccessToken(null).build();
}

@Override
Expand Down Expand Up @@ -468,7 +475,7 @@ private ImpersonatedCredentials(Builder builder) throws IOException {
this.sourceCredentials = builder.getSourceCredentials();
this.targetPrincipal = builder.getTargetPrincipal();
this.delegates = builder.getDelegates();
this.scopes = builder.getScopes();
this.scopes = ImmutableList.copyOf(builder.getScopes());
this.lifetime = builder.getLifetime();
this.transportFactory =
firstNonNull(
Expand All @@ -480,9 +487,6 @@ private ImpersonatedCredentials(Builder builder) throws IOException {
if (this.delegates == null) {
this.delegates = new ArrayList<>();
}
if (this.scopes == null) {
throw new IllegalStateException("Scopes cannot be null");
}
if (this.lifetime > TWELVE_HOURS_IN_SECONDS) {
throw new IllegalStateException("lifetime must be less than or equal to 43200");
}
Expand Down Expand Up @@ -516,8 +520,10 @@ public String getUniverseDomain() throws IOException {
@Override
public AccessToken refreshAccessToken() throws IOException {
if (this.sourceCredentials.getAccessToken() == null) {
// Apply the `CLOUD_PLATFORM_SCOPE` to access the iamcredentials endpoint
this.sourceCredentials =
this.sourceCredentials.createScoped(Arrays.asList(CLOUD_PLATFORM_SCOPE));
this.sourceCredentials.createScoped(
Collections.singletonList(OAuth2Utils.CLOUD_PLATFORM_SCOPE));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it work or is it better to switch to https://www.googleapis.com/auth/iam?

Copy link
Member Author

@lqiu96 lqiu96 Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either should work: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken#authorization-scopes

Not sure if there is a preference. I can update if there is.

}

// skip for SA with SSJ flow because it uses self-signed JWT
Expand Down Expand Up @@ -551,7 +557,7 @@ public AccessToken refreshAccessToken() throws IOException {
GenericUrl url = new GenericUrl(endpointUrl);

Map<String, Object> body =
ImmutableMap.<String, Object>of(
ImmutableMap.of(
"delegates", this.delegates, "scope", this.scopes, "lifetime", this.lifetime + "s");

HttpContent requestContent = new JsonHttpContent(parser.getJsonFactory(), body);
Expand Down Expand Up @@ -741,12 +747,22 @@ public List<String> getDelegates() {
return this.delegates;
}

/**
* Set the scopes to be applied on the impersonated token and not on the source credential. This
* user configuration has precedence over the scopes listed in the source credential json file.
*
* @param scopes List of scopes to apply to the impersonated token
*/
@CanIgnoreReturnValue
public Builder setScopes(List<String> scopes) {
Preconditions.checkNotNull(scopes, "Scopes cannot be null");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructor has a null check that threw a IllegalStateException runtime exception. Enforce this on the setter so users don't pass in an invalid data.

This never worked, so we don't expect any breakages or changes in behavior.

this.scopes = scopes;
return this;
}

/**
* @return List of scopes to be applied to the impersonated token.
*/
public List<String> getScopes() {
return this.scopes;
}
Expand Down
3 changes: 3 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public class OAuth2Utils {
static final URI TOKEN_REVOKE_URI = URI.create("https://oauth2.googleapis.com/revoke");
static final URI USER_AUTH_URI = URI.create("https://accounts.google.com/o/oauth2/auth");

public static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";

static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();

public static final HttpTransportFactory HTTP_TRANSPORT_FACTORY =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ public void builder_noTransport_defaults() throws IOException {
.build();

GoogleCredentials scopedSourceCredentials =
sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform");
sourceCredentials.createScoped(OAuth2Utils.CLOUD_PLATFORM_SCOPE);
assertEquals(scopedSourceCredentials, credentials.getSourceCredentials());
assertEquals(CREDENTIAL_ACCESS_BOUNDARY, credentials.getCredentialAccessBoundary());
assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, credentials.getTransportFactory());
Expand All @@ -254,7 +254,7 @@ public void builder_noUniverseDomain_defaults() throws IOException {
.build();

GoogleCredentials scopedSourceCredentials =
sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform");
sourceCredentials.createScoped(OAuth2Utils.CLOUD_PLATFORM_SCOPE);
assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, credentials.getTransportFactory());
assertEquals(scopedSourceCredentials, credentials.getSourceCredentials());
assertEquals(CREDENTIAL_ACCESS_BOUNDARY, credentials.getCredentialAccessBoundary());
Expand Down Expand Up @@ -320,7 +320,7 @@ private static GoogleCredentials getServiceAccountSourceCredentials(boolean canR
transportFactory.transport.setError(new IOException());
}

return sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform");
return sourceCredentials.createScoped(OAuth2Utils.CLOUD_PLATFORM_SCOPE);
}

private static GoogleCredentials getUserSourceCredentials() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,8 @@ public void fromStream_Impersonation_providesToken_WithQuotaProject() throws IOE
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
ImpersonatedCredentialsTest.DELEGATES,
ImpersonatedCredentialsTest.QUOTA_PROJECT_ID);
ImpersonatedCredentialsTest.QUOTA_PROJECT_ID,
ImpersonatedCredentialsTest.IMMUTABLE_SCOPES_LIST);

ImpersonatedCredentials credentials =
(ImpersonatedCredentials)
Expand Down Expand Up @@ -649,7 +650,8 @@ public void fromStream_Impersonation_defaultUniverse() throws IOException {
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
ImpersonatedCredentialsTest.DELEGATES,
ImpersonatedCredentialsTest.QUOTA_PROJECT_ID);
ImpersonatedCredentialsTest.QUOTA_PROJECT_ID,
ImpersonatedCredentialsTest.IMMUTABLE_SCOPES_LIST);

ImpersonatedCredentials credentials =
(ImpersonatedCredentials)
Expand Down Expand Up @@ -684,7 +686,8 @@ public void fromStream_Impersonation_providesToken_WithoutQuotaProject() throws
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
ImpersonatedCredentialsTest.DELEGATES,
null);
null,
ImpersonatedCredentialsTest.IMMUTABLE_SCOPES_LIST);

ImpersonatedCredentials credentials =
(ImpersonatedCredentials)
Expand Down Expand Up @@ -916,7 +919,8 @@ public void getCredentialInfo_impersonatedServiceAccount() throws IOException {
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
ImpersonatedCredentialsTest.DELEGATES,
null);
null,
ImpersonatedCredentialsTest.IMMUTABLE_SCOPES_LIST);

ImpersonatedCredentials credentials =
(ImpersonatedCredentials) GoogleCredentials.fromStream(impersonationCredentialsStream);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public AccessToken refreshAccessToken() throws IOException {
ServiceAccountCredentials credentials =
(ServiceAccountCredentials)
GoogleCredentials.getApplicationDefault()
.createScoped("https://www.googleapis.com/auth/cloud-platform");
.createScoped(OAuth2Utils.CLOUD_PLATFORM_SCOPE);

DownscopedCredentials downscopedCredentials =
DownscopedCredentials.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,7 @@ private void callGcs(GoogleCredentials credentials) throws IOException {
*/
private String generateGoogleIdToken(String audience) throws IOException {
GoogleCredentials googleCredentials =
GoogleCredentials.getApplicationDefault()
.createScoped("https://www.googleapis.com/auth/cloud-platform");
GoogleCredentials.getApplicationDefault().createScoped(OAuth2Utils.CLOUD_PLATFORM_SCOPE);

HttpCredentialsAdapter credentialsAdapter = new HttpCredentialsAdapter(googleCredentials);
HttpRequestFactory requestFactory =
Expand Down
Loading