Skip to content

Commit

Permalink
feat: add support for Workforce Pools (#729)
Browse files Browse the repository at this point in the history
* feat: add workforce pools support

* fix: move workforce aud check to constructor

* fix: adds `isWorkforcePoolConfiguration` method

* fix: review comments

* fix: test
  • Loading branch information
lsirac committed Sep 21, 2021
1 parent 29e3a12 commit 5f3fed7
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 80 deletions.
Expand Up @@ -280,7 +280,7 @@ public static ExternalAccountCredentials fromStream(
parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class);
try {
return fromJson(fileContents, transportFactory);
} catch (ClassCastException e) {
} catch (ClassCastException | IllegalArgumentException e) {
throw new CredentialFormatException("An invalid input stream was provided.", e);
}
}
Expand All @@ -300,15 +300,16 @@ static ExternalAccountCredentials fromJson(
String audience = (String) json.get("audience");
String subjectTokenType = (String) json.get("subject_token_type");
String tokenUrl = (String) json.get("token_url");
String serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");

Map<String, Object> credentialSourceMap = (Map<String, Object>) json.get("credential_source");

// Optional params.
String serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
String tokenInfoUrl = (String) json.get("token_info_url");
String clientId = (String) json.get("client_id");
String clientSecret = (String) json.get("client_secret");
String quotaProjectId = (String) json.get("quota_project_id");
String userProject = (String) json.get("workforce_pool_user_project");

if (isAwsCredential(credentialSourceMap)) {
return new AwsCredentials(
Expand All @@ -325,19 +326,20 @@ static ExternalAccountCredentials fromJson(
/* scopes= */ null,
/* environmentProvider= */ null);
}
return new IdentityPoolCredentials(
transportFactory,
audience,
subjectTokenType,
tokenUrl,
new IdentityPoolCredentialSource(credentialSourceMap),
tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
clientSecret,
/* scopes= */ null,
/* environmentProvider= */ null);

return IdentityPoolCredentials.newBuilder()
.setWorkforcePoolUserProject(userProject)
.setHttpTransportFactory(transportFactory)
.setAudience(audience)
.setSubjectTokenType(subjectTokenType)
.setTokenUrl(tokenUrl)
.setTokenInfoUrl(tokenInfoUrl)
.setCredentialSource(new IdentityPoolCredentialSource(credentialSourceMap))
.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
.setQuotaProjectId(quotaProjectId)
.setClientId(clientId)
.setClientSecret(clientSecret)
.build();
}

private static boolean isAwsCredential(Map<String, Object> credentialSource) {
Expand All @@ -362,6 +364,7 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
StsRequestHandler requestHandler =
StsRequestHandler.newBuilder(
tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory())
.setInternalOptions(stsTokenExchangeRequest.getInternalOptions())
.build();

StsTokenExchangeResponse response = requestHandler.exchangeToken();
Expand Down
121 changes: 63 additions & 58 deletions oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.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.JsonObjectParser;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource.CredentialFormatType;
import com.google.common.io.CharStreams;
import java.io.BufferedReader;
Expand All @@ -54,6 +53,7 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
Expand Down Expand Up @@ -155,39 +155,37 @@ private boolean hasHeaders() {

private final IdentityPoolCredentialSource identityPoolCredentialSource;

/**
* Internal constructor. See {@link
* ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
* String, CredentialSource, String, String, String, String, String, Collection,
* EnvironmentProvider)}
*/
IdentityPoolCredentials(
HttpTransportFactory transportFactory,
String audience,
String subjectTokenType,
String tokenUrl,
IdentityPoolCredentialSource credentialSource,
@Nullable String tokenInfoUrl,
@Nullable String serviceAccountImpersonationUrl,
@Nullable String quotaProjectId,
@Nullable String clientId,
@Nullable String clientSecret,
@Nullable Collection<String> scopes,
@Nullable EnvironmentProvider environmentProvider) {
// 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 String workforcePoolUserProject;

/** Internal constructor. See {@link Builder}. */
IdentityPoolCredentials(Builder builder) {
super(
transportFactory,
audience,
subjectTokenType,
tokenUrl,
credentialSource,
tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
clientSecret,
scopes,
environmentProvider);
this.identityPoolCredentialSource = credentialSource;
builder.transportFactory,
builder.audience,
builder.subjectTokenType,
builder.tokenUrl,
builder.credentialSource,
builder.tokenInfoUrl,
builder.serviceAccountImpersonationUrl,
builder.quotaProjectId,
builder.clientId,
builder.clientSecret,
builder.scopes,
builder.environmentProvider);
this.identityPoolCredentialSource = (IdentityPoolCredentialSource) builder.credentialSource;
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.");
}
}

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

@Override
Expand All @@ -202,6 +200,15 @@ public AccessToken refreshAccessToken() throws IOException {
stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes));
}

// 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);
stsTokenExchangeRequest.setInternalOptions(options.toString());
}

return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build());
}

Expand Down Expand Up @@ -269,22 +276,24 @@ private String getSubjectTokenFromMetadataServer() throws IOException {
}
}

/**
* 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/projects/.+/locations/.+/workforcePools/.+/providers/.+$");
return workforcePoolUserProject != null
&& !workforcePoolUserProject.isEmpty()
&& workforceAudiencePattern.matcher(getAudience()).matches();
}

/** Clones the IdentityPoolCredentials with the specified scopes. */
@Override
public IdentityPoolCredentials createScoped(Collection<String> newScopes) {
return new IdentityPoolCredentials(
transportFactory,
getAudience(),
getSubjectTokenType(),
getTokenUrl(),
identityPoolCredentialSource,
getTokenInfoUrl(),
getServiceAccountImpersonationUrl(),
getQuotaProjectId(),
getClientId(),
getClientSecret(),
newScopes,
getEnvironmentProvider());
(IdentityPoolCredentials.Builder) newBuilder(this).setScopes(newScopes));
}

public static Builder newBuilder() {
Expand All @@ -297,27 +306,23 @@ public static Builder newBuilder(IdentityPoolCredentials identityPoolCredentials

public static class Builder extends ExternalAccountCredentials.Builder {

@Nullable private String workforcePoolUserProject;

Builder() {}

Builder(IdentityPoolCredentials credentials) {
super(credentials);
setWorkforcePoolUserProject(credentials.getWorkforcePoolUserProject());
}

public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) {
this.workforcePoolUserProject = workforcePoolUserProject;
return this;
}

@Override
public IdentityPoolCredentials build() {
return new IdentityPoolCredentials(
transportFactory,
audience,
subjectTokenType,
tokenUrl,
(IdentityPoolCredentialSource) credentialSource,
tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
clientSecret,
scopes,
environmentProvider);
return new IdentityPoolCredentials(this);
}
}
}
Expand Up @@ -51,6 +51,7 @@ final class StsTokenExchangeRequest {
@Nullable private final String resource;
@Nullable private final String audience;
@Nullable private final String requestedTokenType;
@Nullable private final String internalOptions;

private StsTokenExchangeRequest(
String subjectToken,
Expand All @@ -59,14 +60,16 @@ private StsTokenExchangeRequest(
@Nullable List<String> scopes,
@Nullable String resource,
@Nullable String audience,
@Nullable String requestedTokenType) {
@Nullable String requestedTokenType,
@Nullable String internalOptions) {
this.subjectToken = checkNotNull(subjectToken);
this.subjectTokenType = checkNotNull(subjectTokenType);
this.actingParty = actingParty;
this.scopes = scopes;
this.resource = resource;
this.audience = audience;
this.requestedTokenType = requestedTokenType;
this.internalOptions = internalOptions;
}

public static Builder newBuilder(String subjectToken, String subjectTokenType) {
Expand Down Expand Up @@ -110,6 +113,11 @@ public ActingParty getActingParty() {
return actingParty;
}

@Nullable
public String getInternalOptions() {
return internalOptions;
}

public boolean hasResource() {
return resource != null && !resource.isEmpty();
}
Expand Down Expand Up @@ -139,6 +147,7 @@ public static class Builder {
@Nullable private String requestedTokenType;
@Nullable private List<String> scopes;
@Nullable private ActingParty actingParty;
@Nullable private String internalOptions;

private Builder(String subjectToken, String subjectTokenType) {
this.subjectToken = subjectToken;
Expand Down Expand Up @@ -170,6 +179,11 @@ public StsTokenExchangeRequest.Builder setActingParty(ActingParty actingParty) {
return this;
}

public StsTokenExchangeRequest.Builder setInternalOptions(String internalOptions) {
this.internalOptions = internalOptions;
return this;
}

public StsTokenExchangeRequest build() {
return new StsTokenExchangeRequest(
subjectToken,
Expand All @@ -178,7 +192,8 @@ public StsTokenExchangeRequest build() {
scopes,
resource,
audience,
requestedTokenType);
requestedTokenType,
internalOptions);
}
}
}

0 comments on commit 5f3fed7

Please sign in to comment.