Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: service account impersonation with workforce credentials #770

Merged
merged 10 commits into from Oct 21, 2021
67 changes: 6 additions & 61 deletions oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
Expand Up @@ -37,7 +37,6 @@
import com.google.api.client.http.HttpResponse;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonParser;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
Expand All @@ -49,7 +48,6 @@
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
* AWS credentials representing a third-party identity for calling Google APIs.
Expand Down Expand Up @@ -114,39 +112,10 @@ static class AwsCredentialSource extends CredentialSource {

private final AwsCredentialSource awsCredentialSource;

/**
* Internal constructor. See {@link
* ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
* String, CredentialSource, String, String, String, String, String, Collection,
* EnvironmentProvider)}
*/
AwsCredentials(
HttpTransportFactory transportFactory,
String audience,
String subjectTokenType,
String tokenUrl,
AwsCredentialSource credentialSource,
@Nullable String tokenInfoUrl,
@Nullable String serviceAccountImpersonationUrl,
@Nullable String quotaProjectId,
@Nullable String clientId,
@Nullable String clientSecret,
@Nullable Collection<String> scopes,
@Nullable EnvironmentProvider environmentProvider) {
super(
transportFactory,
audience,
subjectTokenType,
tokenUrl,
credentialSource,
tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
clientSecret,
scopes,
environmentProvider);
this.awsCredentialSource = credentialSource;
/** Internal constructor. See {@link AwsCredentials.Builder}. */
AwsCredentials(Builder builder) {
super(builder);
this.awsCredentialSource = (AwsCredentialSource) builder.credentialSource;
}

@Override
Expand Down Expand Up @@ -192,19 +161,7 @@ public String retrieveSubjectToken() throws IOException {
/** Clones the AwsCredentials with the specified scopes. */
@Override
public GoogleCredentials createScoped(Collection<String> newScopes) {
return new AwsCredentials(
transportFactory,
getAudience(),
getSubjectTokenType(),
getTokenUrl(),
awsCredentialSource,
getTokenInfoUrl(),
getServiceAccountImpersonationUrl(),
getQuotaProjectId(),
getClientId(),
getClientSecret(),
newScopes,
getEnvironmentProvider());
return new AwsCredentials((AwsCredentials.Builder) newBuilder(this).setScopes(newScopes));
}

private String retrieveResource(String url, String resourceName) throws IOException {
Expand Down Expand Up @@ -342,19 +299,7 @@ public static class Builder extends ExternalAccountCredentials.Builder {

@Override
public AwsCredentials build() {
return new AwsCredentials(
transportFactory,
audience,
subjectTokenType,
tokenUrl,
(AwsCredentialSource) credentialSource,
tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
clientSecret,
scopes,
environmentProvider);
return new AwsCredentials(this);
}
}
}
Expand Up @@ -89,14 +89,19 @@ abstract static class CredentialSource {
@Nullable private final String clientId;
@Nullable private final String clientSecret;

// This is used for Workforce Pools. It is passed to STS during token exchange in the
// `options` param and will be embedded in the token by STS.
@Nullable private final String workforcePoolUserProject;

protected transient HttpTransportFactory transportFactory;

@Nullable protected final ImpersonatedCredentials impersonatedCredentials;

private EnvironmentProvider environmentProvider;

/**
* Constructor with minimum identifying information and custom HTTP transport.
* Constructor with minimum identifying information and custom HTTP transport. Does not support
* workforce credentials.
*
* @param transportFactory HTTP transport factory, creates the transport used to get access tokens
* @param audience the STS audience which is usually the fully specified resource name of the
Expand Down Expand Up @@ -181,6 +186,49 @@ protected ExternalAccountCredentials(
(scopes == null || scopes.isEmpty()) ? Arrays.asList(CLOUD_PLATFORM_SCOPE) : scopes;
this.environmentProvider =
environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider;
this.workforcePoolUserProject = null;

validateTokenUrl(tokenUrl);
if (serviceAccountImpersonationUrl != null) {
validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
}

this.impersonatedCredentials = initializeImpersonatedCredentials();
}

/**
* Internal constructor with minimum identifying information and custom HTTP transport. See {@link
* ExternalAccountCredentials.Builder}.
*/
protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) {
this.transportFactory =
MoreObjects.firstNonNull(
builder.transportFactory,
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
this.transportFactoryClassName = checkNotNull(this.transportFactory.getClass().getName());
this.audience = checkNotNull(builder.audience);
this.subjectTokenType = checkNotNull(builder.subjectTokenType);
this.tokenUrl = checkNotNull(builder.tokenUrl);
this.credentialSource = checkNotNull(builder.credentialSource);
this.tokenInfoUrl = builder.tokenInfoUrl;
this.serviceAccountImpersonationUrl = builder.serviceAccountImpersonationUrl;
this.quotaProjectId = builder.quotaProjectId;
this.clientId = builder.clientId;
this.clientSecret = builder.clientSecret;
this.scopes =
(builder.scopes == null || builder.scopes.isEmpty())
? Arrays.asList(CLOUD_PLATFORM_SCOPE)
: builder.scopes;
this.environmentProvider =
builder.environmentProvider == null
? SystemEnvironmentProvider.getInstance()
: builder.environmentProvider;

this.workforcePoolUserProject = builder.workforcePoolUserProject;
if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) {
throw new IllegalArgumentException(
"The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.");
}

validateTokenUrl(tokenUrl);
if (serviceAccountImpersonationUrl != null) {
Expand Down Expand Up @@ -312,23 +360,21 @@ static ExternalAccountCredentials fromJson(
String userProject = (String) json.get("workforce_pool_user_project");

if (isAwsCredential(credentialSourceMap)) {
return new AwsCredentials(
transportFactory,
audience,
subjectTokenType,
tokenUrl,
new AwsCredentialSource(credentialSourceMap),
tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
clientSecret,
/* scopes= */ null,
/* environmentProvider= */ null);
return AwsCredentials.newBuilder()
.setHttpTransportFactory(transportFactory)
.setAudience(audience)
.setSubjectTokenType(subjectTokenType)
.setTokenUrl(tokenUrl)
.setTokenInfoUrl(tokenInfoUrl)
.setCredentialSource(new AwsCredentialSource(credentialSourceMap))
.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
.setQuotaProjectId(quotaProjectId)
.setClientId(clientId)
.setClientSecret(clientSecret)
.build();
}

return IdentityPoolCredentials.newBuilder()
.setWorkforcePoolUserProject(userProject)
.setHttpTransportFactory(transportFactory)
.setAudience(audience)
.setSubjectTokenType(subjectTokenType)
Expand All @@ -339,6 +385,7 @@ static ExternalAccountCredentials fromJson(
.setQuotaProjectId(quotaProjectId)
.setClientId(clientId)
.setClientSecret(clientSecret)
.setWorkforcePoolUserProject(userProject)
.build();
}

Expand All @@ -361,13 +408,25 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
return impersonatedCredentials.refreshAccessToken();
}

StsRequestHandler requestHandler =
StsRequestHandler.Builder requestHandler =
StsRequestHandler.newBuilder(
tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory())
.setInternalOptions(stsTokenExchangeRequest.getInternalOptions())
.build();
tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory());

// If this credential was initialized with a Workforce configuration then the
// workforcePoolUserProject must passed to STS via the the internal options param.
if (isWorkforcePoolConfiguration()) {
GenericJson options = new GenericJson();
options.setFactory(OAuth2Utils.JSON_FACTORY);
options.put("userProject", workforcePoolUserProject);
requestHandler.setInternalOptions(options.toString());
}

if (stsTokenExchangeRequest.getInternalOptions() != null) {
// Overwrite internal options. Let subclass handle setting options.
requestHandler.setInternalOptions(stsTokenExchangeRequest.getInternalOptions());
}

StsTokenExchangeResponse response = requestHandler.exchangeToken();
StsTokenExchangeResponse response = requestHandler.build().exchangeToken();
return response.getAccessToken();
}

Expand Down Expand Up @@ -427,10 +486,26 @@ public Collection<String> getScopes() {
return scopes;
}

@Nullable
public String getWorkforcePoolUserProject() {
return workforcePoolUserProject;
}

EnvironmentProvider getEnvironmentProvider() {
return environmentProvider;
}

/**
* Returns whether or not the current configuration is for Workforce Pools (which enable 3p user
* identities, rather than workloads).
*/
public boolean isWorkforcePoolConfiguration() {
Pattern workforceAudiencePattern =
Pattern.compile("^//iam.googleapis.com/locations/.+/workforcePools/.+/providers/.+$");
return workforcePoolUserProject != null
&& workforceAudiencePattern.matcher(getAudience()).matches();
}

static void validateTokenUrl(String tokenUrl) {
List<Pattern> patterns = new ArrayList<>();
patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\.sts\\.googleapis\\.com$"));
Expand Down Expand Up @@ -501,6 +576,7 @@ public abstract static class Builder extends GoogleCredentials.Builder {
@Nullable protected String clientId;
@Nullable protected String clientSecret;
@Nullable protected Collection<String> scopes;
@Nullable protected String workforcePoolUserProject;

protected Builder() {}

Expand All @@ -517,60 +593,95 @@ protected Builder(ExternalAccountCredentials credentials) {
this.clientSecret = credentials.clientSecret;
this.scopes = credentials.scopes;
this.environmentProvider = credentials.environmentProvider;
this.workforcePoolUserProject = credentials.workforcePoolUserProject;
}

/** Sets the HTTP transport factory, creates the transport used to get access tokens. */
public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
this.transportFactory = transportFactory;
return this;
}

/**
* Sets the STS audience which is usually the fully specified resource name of the
* workload/workforce pool provider.
*/
public Builder setAudience(String audience) {
this.audience = audience;
return this;
}

/**
* Sets the STS subject token type based on the OAuth 2.0 token exchange spec. Indicates the
* type of the security token in the credential file.
*/
public Builder setSubjectTokenType(String subjectTokenType) {
this.subjectTokenType = subjectTokenType;
return this;
}

/** Sets the STS token exchange endpoint. */
public Builder setTokenUrl(String tokenUrl) {
this.tokenUrl = tokenUrl;
return this;
}

public Builder setTokenInfoUrl(String tokenInfoUrl) {
this.tokenInfoUrl = tokenInfoUrl;
/** Sets the external credential source. */
public Builder setCredentialSource(CredentialSource credentialSource) {
this.credentialSource = credentialSource;
return this;
}

/**
* Sets the optional URL used for service account impersonation. This is only required when APIs
* to be accessed have not integrated with UberMint. If this is not available, the STS returned
* GCP access token is directly used.
*/
public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) {
this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
return this;
}

public Builder setCredentialSource(CredentialSource credentialSource) {
this.credentialSource = credentialSource;
return this;
}

public Builder setScopes(Collection<String> scopes) {
this.scopes = scopes;
/**
* Sets the optional endpoint used to retrieve account related information. Required for gCloud
* session account identification.
*/
public Builder setTokenInfoUrl(String tokenInfoUrl) {
this.tokenInfoUrl = tokenInfoUrl;
return this;
}

/** Sets the optional project used for quota and billing purposes. */
public Builder setQuotaProjectId(String quotaProjectId) {
this.quotaProjectId = quotaProjectId;
return this;
}

/** Sets the optional client ID of the service account from the console. */
public Builder setClientId(String clientId) {
this.clientId = clientId;
return this;
}

/** Sets the optional client secret of the service account from the console. */
public Builder setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
return this;
}

public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
this.transportFactory = transportFactory;
/** Sets the optional scopes to request during the authorization grant. */
public Builder setScopes(Collection<String> scopes) {
this.scopes = scopes;
return this;
}

/**
* Sets the optional workforce pool user project number when the credential corresponds to a
* workforce pool and not a workload identity pool. The underlying principal must still have
* serviceusage.services.use IAM permission to use the project for billing/quota.
*/
public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) {
this.workforcePoolUserProject = workforcePoolUserProject;
return this;
}

Expand Down