Skip to content

Commit 5f3fed7

Browse files
authored
feat: add support for Workforce Pools (googleapis#729)
* feat: add workforce pools support * fix: move workforce aud check to constructor * fix: adds `isWorkforcePoolConfiguration` method * fix: review comments * fix: test
1 parent 29e3a12 commit 5f3fed7

File tree

5 files changed

+299
-80
lines changed

5 files changed

+299
-80
lines changed

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

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ public static ExternalAccountCredentials fromStream(
280280
parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class);
281281
try {
282282
return fromJson(fileContents, transportFactory);
283-
} catch (ClassCastException e) {
283+
} catch (ClassCastException | IllegalArgumentException e) {
284284
throw new CredentialFormatException("An invalid input stream was provided.", e);
285285
}
286286
}
@@ -300,15 +300,16 @@ static ExternalAccountCredentials fromJson(
300300
String audience = (String) json.get("audience");
301301
String subjectTokenType = (String) json.get("subject_token_type");
302302
String tokenUrl = (String) json.get("token_url");
303-
String serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
304303

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

307306
// Optional params.
307+
String serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
308308
String tokenInfoUrl = (String) json.get("token_info_url");
309309
String clientId = (String) json.get("client_id");
310310
String clientSecret = (String) json.get("client_secret");
311311
String quotaProjectId = (String) json.get("quota_project_id");
312+
String userProject = (String) json.get("workforce_pool_user_project");
312313

313314
if (isAwsCredential(credentialSourceMap)) {
314315
return new AwsCredentials(
@@ -325,19 +326,20 @@ static ExternalAccountCredentials fromJson(
325326
/* scopes= */ null,
326327
/* environmentProvider= */ null);
327328
}
328-
return new IdentityPoolCredentials(
329-
transportFactory,
330-
audience,
331-
subjectTokenType,
332-
tokenUrl,
333-
new IdentityPoolCredentialSource(credentialSourceMap),
334-
tokenInfoUrl,
335-
serviceAccountImpersonationUrl,
336-
quotaProjectId,
337-
clientId,
338-
clientSecret,
339-
/* scopes= */ null,
340-
/* environmentProvider= */ null);
329+
330+
return IdentityPoolCredentials.newBuilder()
331+
.setWorkforcePoolUserProject(userProject)
332+
.setHttpTransportFactory(transportFactory)
333+
.setAudience(audience)
334+
.setSubjectTokenType(subjectTokenType)
335+
.setTokenUrl(tokenUrl)
336+
.setTokenInfoUrl(tokenInfoUrl)
337+
.setCredentialSource(new IdentityPoolCredentialSource(credentialSourceMap))
338+
.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
339+
.setQuotaProjectId(quotaProjectId)
340+
.setClientId(clientId)
341+
.setClientSecret(clientSecret)
342+
.build();
341343
}
342344

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

367370
StsTokenExchangeResponse response = requestHandler.exchangeToken();

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

Lines changed: 63 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import com.google.api.client.http.HttpResponse;
3838
import com.google.api.client.json.GenericJson;
3939
import com.google.api.client.json.JsonObjectParser;
40-
import com.google.auth.http.HttpTransportFactory;
4140
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource.CredentialFormatType;
4241
import com.google.common.io.CharStreams;
4342
import java.io.BufferedReader;
@@ -54,6 +53,7 @@
5453
import java.util.Collection;
5554
import java.util.HashMap;
5655
import java.util.Map;
56+
import java.util.regex.Pattern;
5757
import javax.annotation.Nullable;
5858

5959
/**
@@ -155,39 +155,37 @@ private boolean hasHeaders() {
155155

156156
private final IdentityPoolCredentialSource identityPoolCredentialSource;
157157

158-
/**
159-
* Internal constructor. See {@link
160-
* ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
161-
* String, CredentialSource, String, String, String, String, String, Collection,
162-
* EnvironmentProvider)}
163-
*/
164-
IdentityPoolCredentials(
165-
HttpTransportFactory transportFactory,
166-
String audience,
167-
String subjectTokenType,
168-
String tokenUrl,
169-
IdentityPoolCredentialSource credentialSource,
170-
@Nullable String tokenInfoUrl,
171-
@Nullable String serviceAccountImpersonationUrl,
172-
@Nullable String quotaProjectId,
173-
@Nullable String clientId,
174-
@Nullable String clientSecret,
175-
@Nullable Collection<String> scopes,
176-
@Nullable EnvironmentProvider environmentProvider) {
158+
// This is used for Workforce Pools. It is passed to STS during token exchange in the
159+
// `options` param and will be embedded in the token by STS.
160+
@Nullable private String workforcePoolUserProject;
161+
162+
/** Internal constructor. See {@link Builder}. */
163+
IdentityPoolCredentials(Builder builder) {
177164
super(
178-
transportFactory,
179-
audience,
180-
subjectTokenType,
181-
tokenUrl,
182-
credentialSource,
183-
tokenInfoUrl,
184-
serviceAccountImpersonationUrl,
185-
quotaProjectId,
186-
clientId,
187-
clientSecret,
188-
scopes,
189-
environmentProvider);
190-
this.identityPoolCredentialSource = credentialSource;
165+
builder.transportFactory,
166+
builder.audience,
167+
builder.subjectTokenType,
168+
builder.tokenUrl,
169+
builder.credentialSource,
170+
builder.tokenInfoUrl,
171+
builder.serviceAccountImpersonationUrl,
172+
builder.quotaProjectId,
173+
builder.clientId,
174+
builder.clientSecret,
175+
builder.scopes,
176+
builder.environmentProvider);
177+
this.identityPoolCredentialSource = (IdentityPoolCredentialSource) builder.credentialSource;
178+
this.workforcePoolUserProject = builder.workforcePoolUserProject;
179+
180+
if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) {
181+
throw new IllegalArgumentException(
182+
"The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.");
183+
}
184+
}
185+
186+
@Nullable
187+
public String getWorkforcePoolUserProject() {
188+
return workforcePoolUserProject;
191189
}
192190

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

203+
// If this credential was initialized with a Workforce configuration then the
204+
// workforcePoolUserProject must passed to STS via the the internal options param.
205+
if (isWorkforcePoolConfiguration()) {
206+
GenericJson options = new GenericJson();
207+
options.setFactory(OAuth2Utils.JSON_FACTORY);
208+
options.put("userProject", workforcePoolUserProject);
209+
stsTokenExchangeRequest.setInternalOptions(options.toString());
210+
}
211+
205212
return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build());
206213
}
207214

@@ -269,22 +276,24 @@ private String getSubjectTokenFromMetadataServer() throws IOException {
269276
}
270277
}
271278

279+
/**
280+
* Returns whether or not the current configuration is for Workforce Pools (which enable 3p user
281+
* identities, rather than workloads).
282+
*/
283+
public boolean isWorkforcePoolConfiguration() {
284+
Pattern workforceAudiencePattern =
285+
Pattern.compile(
286+
"^//iam.googleapis.com/projects/.+/locations/.+/workforcePools/.+/providers/.+$");
287+
return workforcePoolUserProject != null
288+
&& !workforcePoolUserProject.isEmpty()
289+
&& workforceAudiencePattern.matcher(getAudience()).matches();
290+
}
291+
272292
/** Clones the IdentityPoolCredentials with the specified scopes. */
273293
@Override
274294
public IdentityPoolCredentials createScoped(Collection<String> newScopes) {
275295
return new IdentityPoolCredentials(
276-
transportFactory,
277-
getAudience(),
278-
getSubjectTokenType(),
279-
getTokenUrl(),
280-
identityPoolCredentialSource,
281-
getTokenInfoUrl(),
282-
getServiceAccountImpersonationUrl(),
283-
getQuotaProjectId(),
284-
getClientId(),
285-
getClientSecret(),
286-
newScopes,
287-
getEnvironmentProvider());
296+
(IdentityPoolCredentials.Builder) newBuilder(this).setScopes(newScopes));
288297
}
289298

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

298307
public static class Builder extends ExternalAccountCredentials.Builder {
299308

309+
@Nullable private String workforcePoolUserProject;
310+
300311
Builder() {}
301312

302313
Builder(IdentityPoolCredentials credentials) {
303314
super(credentials);
315+
setWorkforcePoolUserProject(credentials.getWorkforcePoolUserProject());
316+
}
317+
318+
public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) {
319+
this.workforcePoolUserProject = workforcePoolUserProject;
320+
return this;
304321
}
305322

306323
@Override
307324
public IdentityPoolCredentials build() {
308-
return new IdentityPoolCredentials(
309-
transportFactory,
310-
audience,
311-
subjectTokenType,
312-
tokenUrl,
313-
(IdentityPoolCredentialSource) credentialSource,
314-
tokenInfoUrl,
315-
serviceAccountImpersonationUrl,
316-
quotaProjectId,
317-
clientId,
318-
clientSecret,
319-
scopes,
320-
environmentProvider);
325+
return new IdentityPoolCredentials(this);
321326
}
322327
}
323328
}

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ final class StsTokenExchangeRequest {
5151
@Nullable private final String resource;
5252
@Nullable private final String audience;
5353
@Nullable private final String requestedTokenType;
54+
@Nullable private final String internalOptions;
5455

5556
private StsTokenExchangeRequest(
5657
String subjectToken,
@@ -59,14 +60,16 @@ private StsTokenExchangeRequest(
5960
@Nullable List<String> scopes,
6061
@Nullable String resource,
6162
@Nullable String audience,
62-
@Nullable String requestedTokenType) {
63+
@Nullable String requestedTokenType,
64+
@Nullable String internalOptions) {
6365
this.subjectToken = checkNotNull(subjectToken);
6466
this.subjectTokenType = checkNotNull(subjectTokenType);
6567
this.actingParty = actingParty;
6668
this.scopes = scopes;
6769
this.resource = resource;
6870
this.audience = audience;
6971
this.requestedTokenType = requestedTokenType;
72+
this.internalOptions = internalOptions;
7073
}
7174

7275
public static Builder newBuilder(String subjectToken, String subjectTokenType) {
@@ -110,6 +113,11 @@ public ActingParty getActingParty() {
110113
return actingParty;
111114
}
112115

116+
@Nullable
117+
public String getInternalOptions() {
118+
return internalOptions;
119+
}
120+
113121
public boolean hasResource() {
114122
return resource != null && !resource.isEmpty();
115123
}
@@ -139,6 +147,7 @@ public static class Builder {
139147
@Nullable private String requestedTokenType;
140148
@Nullable private List<String> scopes;
141149
@Nullable private ActingParty actingParty;
150+
@Nullable private String internalOptions;
142151

143152
private Builder(String subjectToken, String subjectTokenType) {
144153
this.subjectToken = subjectToken;
@@ -170,6 +179,11 @@ public StsTokenExchangeRequest.Builder setActingParty(ActingParty actingParty) {
170179
return this;
171180
}
172181

182+
public StsTokenExchangeRequest.Builder setInternalOptions(String internalOptions) {
183+
this.internalOptions = internalOptions;
184+
return this;
185+
}
186+
173187
public StsTokenExchangeRequest build() {
174188
return new StsTokenExchangeRequest(
175189
subjectToken,
@@ -178,7 +192,8 @@ public StsTokenExchangeRequest build() {
178192
scopes,
179193
resource,
180194
audience,
181-
requestedTokenType);
195+
requestedTokenType,
196+
internalOptions);
182197
}
183198
}
184199
}

0 commit comments

Comments
 (0)