Skip to content

Commit 7f2c535

Browse files
authored
feat: workforce identity federation for pluggable auth (googleapis#959)
* feat: add workforce support to ADC with pluggable auth * feat: document workforce identity federation
1 parent 48ff83d commit 7f2c535

File tree

3 files changed

+262
-22
lines changed

3 files changed

+262
-22
lines changed

README.md

Lines changed: 205 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ credentials as well as utility methods to create them and to get Application Def
2929
* [Accessing resources from Azure](#access-resources-from-microsoft-azure)
3030
* [Accessing resources from an OIDC identity provider](#accessing-resources-from-an-oidc-identity-provider)
3131
* [Accessing resources using Executable-sourced credentials](#using-executable-sourced-credentials-with-oidc-and-saml)
32+
* [Workforce Identity Federation](#workforce-identity-federation)
33+
* [Accessing resources using an OIDC or SAML 2.0 identity provider](#accessing-resources-using-an-oidc-or-saml-20-identity-provider)
34+
* [Accessing resources using Executable-sourced credentials](#using-executable-sourced-workforce-credentials-with-oidc-and-saml)
3235
* [Downscoping with Credential Access Boundaries](#downscoping-with-credential-access-boundaries)
3336
* [Configuring a Proxy](#configuring-a-proxy)
3437
* [Using Credentials with google-http-client](#using-credentials-with-google-http-client)
@@ -446,14 +449,15 @@ All response types must include both the `version` and `success` fields.
446449

447450
The library will populate the following environment variables when the executable is run:
448451
* `GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE`: The audience field from the credential configuration. Always present.
452+
* `GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE`: This expected subject token type. Always present.
449453
* `GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL`: The service account email. Only present when service account impersonation is used.
450454
* `GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE`: The output file location from the credential configuration. Only present when specified in the credential configuration.
451455

452456
These environment variables can be used by the executable to avoid hard-coding these values.
453457

454458
##### Security considerations
455459
The following security practices are highly recommended:
456-
* Access to the script should be restricted as it will be displaying credentials to stdout. This ensures that rogue processes do not gain access to the script.
460+
* Access to the script should be restricted as it will be displaying credentials to stdout. This ensures that rogue processes do not gain access to the script.
457461
* The configuration file should not be modifiable. Write access should be restricted to avoid processes modifying the executable command portion.
458462

459463
Given the complexity of using executable-sourced credentials, it is recommended to use
@@ -463,13 +467,207 @@ credentials unless they do not meet your specific requirements.
463467
You can now [use the Auth library](#using-external-identities) to call Google Cloud
464468
resources from an OIDC or SAML provider.
465469

466-
#### Using External Identities
470+
### Workforce Identity Federation
467471

468-
External identities (AWS, Azure, and OIDC-based providers) can be used with
469-
`Application Default Credentials`. In order to use external identities with Application Default
470-
Credentials, you need to generate the JSON credentials configuration file for your external identity
471-
as described above. Once generated, store the path to this file in the
472-
`GOOGLE_APPLICATION_CREDENTIALS` environment variable.
472+
[Workforce identity federation](https://cloud.google.com/iam/docs/workforce-identity-federation) lets you use an
473+
external identity provider (IdP) to authenticate and authorize a workforce—a group of users, such as employees,
474+
partners, and contractors—using IAM, so that the users can access Google Cloud services. Workforce identity federation
475+
extends Google Cloud's identity capabilities to support syncless, attribute-based single sign on.
476+
477+
With workforce identity federation, your workforce can access Google Cloud resources using an external
478+
identity provider (IdP) that supports OpenID Connect (OIDC) or SAML 2.0 such as Azure Active Directory (Azure AD),
479+
Active Directory Federation Services (AD FS), Okta, and others.
480+
481+
#### Accessing resources using an OIDC or SAML 2.0 identity provider
482+
483+
In order to access Google Cloud resources from an identity provider that supports [OpenID Connect (OIDC)](https://openid.net/connect/),
484+
the following requirements are needed:
485+
- A workforce identity pool needs to be created.
486+
- An OIDC or SAML 2.0 identity provider needs to be added in the workforce pool.
487+
488+
Follow the detailed [instructions](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation) on how
489+
to configure workforce identity federation.
490+
491+
After configuring an OIDC or SAML 2.0 provider, a credential configuration
492+
file needs to be generated. The generated credential configuration file contains non-sensitive metadata to instruct the
493+
library on how to retrieve external subject tokens and exchange them for GCP access tokens.
494+
The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/).
495+
496+
The Auth library can retrieve external subject tokens from a local file location
497+
(file-sourced credentials), from a local server (URL-sourced credentials) or by calling an executable
498+
(executable-sourced credentials).
499+
500+
**File-sourced credentials**
501+
For file-sourced credentials, a background process needs to be continuously refreshing the file
502+
location with a new subject token prior to expiration. For tokens with one hour lifetimes, the token
503+
needs to be updated in the file every hour. The token can be stored directly as plain text or in
504+
JSON format.
505+
506+
To generate a file-sourced OIDC configuration, run the following command:
507+
508+
```bash
509+
# Generate an OIDC configuration file for file-sourced credentials.
510+
gcloud iam workforce-pools create-cred-config \
511+
locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
512+
--subject-token-type=urn:ietf:params:oauth:token-type:id_token \
513+
--credential-source-file=$PATH_TO_OIDC_ID_TOKEN \
514+
--workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \
515+
# Optional arguments for file types. Default is "text":
516+
# --credential-source-type "json" \
517+
# Optional argument for the field that contains the OIDC credential.
518+
# This is required for json.
519+
# --credential-source-field-name "id_token" \
520+
--output-file=/path/to/generated/config.json
521+
```
522+
Where the following variables need to be substituted:
523+
- `$WORKFORCE_POOL_ID`: The workforce pool ID.
524+
- `$PROVIDER_ID`: The provider ID.
525+
- `$PATH_TO_OIDC_ID_TOKEN`: The file path used to retrieve the OIDC token.
526+
- `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project).
527+
528+
To generate a file-sourced SAML configuration, run the following command:
529+
530+
```bash
531+
# Generate a SAML configuration file for file-sourced credentials.
532+
gcloud iam workforce-pools create-cred-config \
533+
locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
534+
--credential-source-file=$PATH_TO_SAML_ASSERTION \
535+
--subject-token-type=urn:ietf:params:oauth:token-type:saml2 \
536+
--workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \
537+
--output-file=/path/to/generated/config.json
538+
```
539+
540+
Where the following variables need to be substituted:
541+
- `$WORKFORCE_POOL_ID`: The workforce pool ID.
542+
- `$PROVIDER_ID`: The provider ID.
543+
- `$PATH_TO_SAML_ASSERTION`: The file path used to retrieve the base64-encoded SAML assertion.
544+
- `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project).
545+
546+
These commands generate the configuration file in the specified output file.
547+
548+
**URL-sourced credentials**
549+
For URL-sourced credentials, a local server needs to host a GET endpoint to return the OIDC token.
550+
The response can be in plain text or JSON. Additional required request headers can also be
551+
specified.
552+
553+
To generate a URL-sourced OIDC workforce identity configuration, run the following command:
554+
555+
```bash
556+
# Generate an OIDC configuration file for URL-sourced credentials.
557+
gcloud iam workforce-pools create-cred-config \
558+
locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
559+
--subject-token-type=urn:ietf:params:oauth:token-type:id_token \
560+
--credential-source-url=$URL_TO_RETURN_OIDC_ID_TOKEN \
561+
--credential-source-headers $HEADER_KEY=$HEADER_VALUE \
562+
--workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \
563+
--output-file=/path/to/generated/config.json
564+
```
565+
566+
Where the following variables need to be substituted:
567+
- `$WORKFORCE_POOL_ID`: The workforce pool ID.
568+
- `$PROVIDER_ID`: The provider ID.
569+
- `$URL_TO_RETURN_OIDC_ID_TOKEN`: The URL of the local server endpoint.
570+
- `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to
571+
`$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`.
572+
- `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project).
573+
574+
To generate a URL-sourced SAML configuration, run the following command:
575+
576+
```bash
577+
# Generate a SAML configuration file for file-sourced credentials.
578+
gcloud iam workforce-pools create-cred-config \
579+
locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
580+
--subject-token-type=urn:ietf:params:oauth:token-type:saml2 \
581+
--credential-source-url=$URL_TO_GET_SAML_ASSERTION \
582+
--credential-source-headers $HEADER_KEY=$HEADER_VALUE \
583+
--workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \
584+
--output-file=/path/to/generated/config.json
585+
```
586+
587+
These commands generate the configuration file in the specified output file.
588+
589+
Where the following variables need to be substituted:
590+
- `$WORKFORCE_POOL_ID`: The workforce pool ID.
591+
- `$PROVIDER_ID`: The provider ID.
592+
- `$URL_TO_GET_SAML_ASSERTION`: The URL of the local server endpoint.
593+
- `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to
594+
`$URL_TO_GET_SAML_ASSERTION`, e.g. `Metadata-Flavor=Google`.
595+
- `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project).
596+
597+
#### Using Executable-sourced workforce credentials with OIDC and SAML
598+
599+
**Executable-sourced credentials**
600+
For executable-sourced credentials, a local executable is used to retrieve the 3rd party token.
601+
The executable must handle providing a valid, unexpired OIDC ID token or SAML assertion in JSON format
602+
to stdout.
603+
604+
To use executable-sourced credentials, the `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES`
605+
environment variable must be set to `1`.
606+
607+
To generate an executable-sourced workforce identity configuration, run the following command:
608+
609+
```bash
610+
# Generate a configuration file for executable-sourced credentials.
611+
gcloud iam workforce-pools create-cred-config \
612+
locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
613+
--subject-token-type=$SUBJECT_TOKEN_TYPE \
614+
# The absolute path for the program, including arguments.
615+
# e.g. --executable-command="/path/to/command --foo=bar"
616+
--executable-command=$EXECUTABLE_COMMAND \
617+
# Optional argument for the executable timeout. Defaults to 30s.
618+
# --executable-timeout-millis=$EXECUTABLE_TIMEOUT \
619+
# Optional argument for the absolute path to the executable output file.
620+
# See below on how this argument impacts the library behaviour.
621+
# --executable-output-file=$EXECUTABLE_OUTPUT_FILE \
622+
--workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \
623+
--output-file /path/to/generated/config.json
624+
```
625+
Where the following variables need to be substituted:
626+
- `$WORKFORCE_POOL_ID`: The workforce pool ID.
627+
- `$PROVIDER_ID`: The provider ID.
628+
- `$SUBJECT_TOKEN_TYPE`: The subject token type.
629+
- `$EXECUTABLE_COMMAND`: The full command to run, including arguments. Must be an absolute path to the program.
630+
- `$WORKFORCE_POOL_USER_PROJECT`: The project number associated with the [workforce pools user project](https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project).
631+
632+
The `--executable-timeout-millis` flag is optional. This is the duration for which
633+
the auth library will wait for the executable to finish, in milliseconds.
634+
Defaults to 30 seconds when not provided. The maximum allowed value is 2 minutes.
635+
The minimum is 5 seconds.
636+
637+
The `--executable-output-file` flag is optional. If provided, the file path must
638+
point to the 3rd party credential response generated by the executable. This is useful
639+
for caching the credentials. By specifying this path, the Auth libraries will first
640+
check for its existence before running the executable. By caching the executable JSON
641+
response to this file, it improves performance as it avoids the need to run the executable
642+
until the cached credentials in the output file are expired. The executable must
643+
handle writing to this file - the auth libraries will only attempt to read from
644+
this location. The format of contents in the file should match the JSON format
645+
expected by the executable shown below.
646+
647+
To retrieve the 3rd party token, the library will call the executable
648+
using the command specified. The executable's output must adhere to the response format
649+
specified below. It must output the response to stdout.
650+
651+
Refer to the [using executable-sourced credentials with Workload Identity Federation](#using-executable-sourced-credentials-with-oidc-and-saml)
652+
above for the executable response specification.
653+
654+
##### Security considerations
655+
The following security practices are highly recommended:
656+
* Access to the script should be restricted as it will be displaying credentials to stdout. This ensures that rogue processes do not gain access to the script.
657+
* The configuration file should not be modifiable. Write access should be restricted to avoid processes modifying the executable command portion.
658+
659+
Given the complexity of using executable-sourced credentials, it is recommended to use
660+
the existing supported mechanisms (file-sourced/URL-sourced) for providing 3rd party
661+
credentials unless they do not meet your specific requirements.
662+
663+
You can now [use the Auth library](#using-external-identities) to call Google Cloud
664+
resources from an OIDC or SAML provider.
665+
666+
### Using External Identities
667+
668+
External identities can be used with `Application Default Credentials`. In order to use external identities with
669+
Application Default Credentials, you need to generate the JSON credentials configuration file for your external identity
670+
as described above. Once generated, store the path to this file in the`GOOGLE_APPLICATION_CREDENTIALS` environment variable.
473671

474672
```bash
475673
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/config.json

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ static ExternalAccountCredentials fromJson(
401401
.setQuotaProjectId(quotaProjectId)
402402
.setClientId(clientId)
403403
.setClientSecret(clientSecret)
404+
.setWorkforcePoolUserProject(userProject)
404405
.build();
405406
}
406407
return IdentityPoolCredentials.newBuilder()

oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,7 @@ void fromJson_identityPoolCredentialsWorkforce() {
196196
assertEquals("subjectTokenType", credential.getSubjectTokenType());
197197
assertEquals(STS_URL, credential.getTokenUrl());
198198
assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
199-
assertEquals(
200-
"userProject", ((IdentityPoolCredentials) credential).getWorkforcePoolUserProject());
199+
assertEquals("userProject", credential.getWorkforcePoolUserProject());
201200
assertNotNull(credential.getCredentialSource());
202201
}
203202

@@ -235,6 +234,30 @@ void fromJson_pluggableAuthCredentials() {
235234
assertNull(source.getOutputFilePath());
236235
}
237236

237+
@Test
238+
void fromJson_pluggableAuthCredentialsWorkforce() {
239+
ExternalAccountCredentials credential =
240+
ExternalAccountCredentials.fromJson(
241+
buildJsonPluggableAuthWorkforceCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
242+
243+
assertTrue(credential instanceof PluggableAuthCredentials);
244+
assertEquals(
245+
"//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider",
246+
credential.getAudience());
247+
assertEquals("subjectTokenType", credential.getSubjectTokenType());
248+
assertEquals(STS_URL, credential.getTokenUrl());
249+
assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
250+
assertEquals("userProject", credential.getWorkforcePoolUserProject());
251+
252+
assertNotNull(credential.getCredentialSource());
253+
254+
PluggableAuthCredentialSource source =
255+
(PluggableAuthCredentialSource) credential.getCredentialSource();
256+
assertEquals("command", source.getCommand());
257+
assertEquals(30000, source.getTimeoutMs()); // Default timeout is 30s.
258+
assertNull(source.getOutputFilePath());
259+
}
260+
238261
@Test
239262
void fromJson_pluggableAuthCredentials_allExecutableOptionsSet() {
240263
GenericJson json = buildJsonPluggableAuthCredential();
@@ -502,25 +525,35 @@ void exchangeExternalCredentialForAccessToken_withInternalOptions() throws IOExc
502525
@Test
503526
void exchangeExternalCredentialForAccessToken_workforceCred_expectUserProjectPassedToSts()
504527
throws IOException {
505-
ExternalAccountCredentials credential =
528+
ExternalAccountCredentials identityPoolCredential =
506529
ExternalAccountCredentials.fromJson(
507530
buildJsonIdentityPoolWorkforceCredential(), transportFactory);
508531

509-
StsTokenExchangeRequest stsTokenExchangeRequest =
510-
StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
532+
ExternalAccountCredentials pluggableAuthCredential =
533+
ExternalAccountCredentials.fromJson(
534+
buildJsonPluggableAuthWorkforceCredential(), transportFactory);
511535

512-
AccessToken accessToken =
513-
credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
536+
List<ExternalAccountCredentials> credentials =
537+
Arrays.asList(identityPoolCredential, pluggableAuthCredential);
514538

515-
assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
539+
for (int i = 0; i < credentials.size(); i++) {
540+
StsTokenExchangeRequest stsTokenExchangeRequest =
541+
StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
516542

517-
// Validate internal options set.
518-
Map<String, String> query =
519-
TestUtils.parseQuery(transportFactory.transport.getLastRequest().getContentAsString());
520-
GenericJson internalOptions = new GenericJson();
521-
internalOptions.setFactory(OAuth2Utils.JSON_FACTORY);
522-
internalOptions.put("userProject", "userProject");
523-
assertEquals(internalOptions.toString(), query.get("options"));
543+
AccessToken accessToken =
544+
credentials.get(i).exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
545+
546+
assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
547+
548+
// Validate internal options set.
549+
Map<String, String> query =
550+
TestUtils.parseQuery(transportFactory.transport.getLastRequest().getContentAsString());
551+
GenericJson internalOptions = new GenericJson();
552+
internalOptions.setFactory(OAuth2Utils.JSON_FACTORY);
553+
internalOptions.put("userProject", "userProject");
554+
assertEquals(internalOptions.toString(), query.get("options"));
555+
assertEquals(i + 1, transportFactory.transport.getRequests().size());
556+
}
524557
}
525558

526559
@Test
@@ -813,6 +846,14 @@ private GenericJson buildJsonPluggableAuthCredential() {
813846
return json;
814847
}
815848

849+
private GenericJson buildJsonPluggableAuthWorkforceCredential() {
850+
GenericJson json = buildJsonPluggableAuthCredential();
851+
json.put(
852+
"audience", "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider");
853+
json.put("workforce_pool_user_project", "userProject");
854+
return json;
855+
}
856+
816857
static class TestExternalAccountCredentials extends ExternalAccountCredentials {
817858
static class TestCredentialSource extends IdentityPoolCredentials.IdentityPoolCredentialSource {
818859
protected TestCredentialSource(Map<String, Object> credentialSourceMap) {

0 commit comments

Comments
 (0)