Skip to content

Commit

Permalink
feat: workforce identity federation for pluggable auth (#959)
Browse files Browse the repository at this point in the history
* feat: add workforce support to ADC with pluggable auth

* feat: document workforce identity federation
  • Loading branch information
lsirac committed Aug 5, 2022
1 parent 48ff83d commit 7f2c535
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 22 deletions.
212 changes: 205 additions & 7 deletions README.md
Expand Up @@ -29,6 +29,9 @@ credentials as well as utility methods to create them and to get Application Def
* [Accessing resources from Azure](#access-resources-from-microsoft-azure)
* [Accessing resources from an OIDC identity provider](#accessing-resources-from-an-oidc-identity-provider)
* [Accessing resources using Executable-sourced credentials](#using-executable-sourced-credentials-with-oidc-and-saml)
* [Workforce Identity Federation](#workforce-identity-federation)
* [Accessing resources using an OIDC or SAML 2.0 identity provider](#accessing-resources-using-an-oidc-or-saml-20-identity-provider)
* [Accessing resources using Executable-sourced credentials](#using-executable-sourced-workforce-credentials-with-oidc-and-saml)
* [Downscoping with Credential Access Boundaries](#downscoping-with-credential-access-boundaries)
* [Configuring a Proxy](#configuring-a-proxy)
* [Using Credentials with google-http-client](#using-credentials-with-google-http-client)
Expand Down Expand Up @@ -446,14 +449,15 @@ All response types must include both the `version` and `success` fields.

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

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

##### Security considerations
The following security practices are highly recommended:
* 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.
* 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.
* The configuration file should not be modifiable. Write access should be restricted to avoid processes modifying the executable command portion.

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

#### Using External Identities
### Workforce Identity Federation

External identities (AWS, Azure, and OIDC-based providers) can be used with
`Application Default Credentials`. In order to use external identities with Application Default
Credentials, you need to generate the JSON credentials configuration file for your external identity
as described above. Once generated, store the path to this file in the
`GOOGLE_APPLICATION_CREDENTIALS` environment variable.
[Workforce identity federation](https://cloud.google.com/iam/docs/workforce-identity-federation) lets you use an
external identity provider (IdP) to authenticate and authorize a workforce—a group of users, such as employees,
partners, and contractors—using IAM, so that the users can access Google Cloud services. Workforce identity federation
extends Google Cloud's identity capabilities to support syncless, attribute-based single sign on.

With workforce identity federation, your workforce can access Google Cloud resources using an external
identity provider (IdP) that supports OpenID Connect (OIDC) or SAML 2.0 such as Azure Active Directory (Azure AD),
Active Directory Federation Services (AD FS), Okta, and others.

#### Accessing resources using an OIDC or SAML 2.0 identity provider

In order to access Google Cloud resources from an identity provider that supports [OpenID Connect (OIDC)](https://openid.net/connect/),
the following requirements are needed:
- A workforce identity pool needs to be created.
- An OIDC or SAML 2.0 identity provider needs to be added in the workforce pool.

Follow the detailed [instructions](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation) on how
to configure workforce identity federation.

After configuring an OIDC or SAML 2.0 provider, a credential configuration
file needs to be generated. The generated credential configuration file contains non-sensitive metadata to instruct the
library on how to retrieve external subject tokens and exchange them for GCP access tokens.
The configuration file can be generated by using the [gcloud CLI](https://cloud.google.com/sdk/).

The Auth library can retrieve external subject tokens from a local file location
(file-sourced credentials), from a local server (URL-sourced credentials) or by calling an executable
(executable-sourced credentials).

**File-sourced credentials**
For file-sourced credentials, a background process needs to be continuously refreshing the file
location with a new subject token prior to expiration. For tokens with one hour lifetimes, the token
needs to be updated in the file every hour. The token can be stored directly as plain text or in
JSON format.

To generate a file-sourced OIDC configuration, run the following command:

```bash
# Generate an OIDC configuration file for file-sourced credentials.
gcloud iam workforce-pools create-cred-config \
locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
--subject-token-type=urn:ietf:params:oauth:token-type:id_token \
--credential-source-file=$PATH_TO_OIDC_ID_TOKEN \
--workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \
# Optional arguments for file types. Default is "text":
# --credential-source-type "json" \
# Optional argument for the field that contains the OIDC credential.
# This is required for json.
# --credential-source-field-name "id_token" \
--output-file=/path/to/generated/config.json
```
Where the following variables need to be substituted:
- `$WORKFORCE_POOL_ID`: The workforce pool ID.
- `$PROVIDER_ID`: The provider ID.
- `$PATH_TO_OIDC_ID_TOKEN`: The file path used to retrieve the OIDC token.
- `$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).

To generate a file-sourced SAML configuration, run the following command:

```bash
# Generate a SAML configuration file for file-sourced credentials.
gcloud iam workforce-pools create-cred-config \
locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
--credential-source-file=$PATH_TO_SAML_ASSERTION \
--subject-token-type=urn:ietf:params:oauth:token-type:saml2 \
--workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \
--output-file=/path/to/generated/config.json
```

Where the following variables need to be substituted:
- `$WORKFORCE_POOL_ID`: The workforce pool ID.
- `$PROVIDER_ID`: The provider ID.
- `$PATH_TO_SAML_ASSERTION`: The file path used to retrieve the base64-encoded SAML assertion.
- `$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).

These commands generate the configuration file in the specified output file.

**URL-sourced credentials**
For URL-sourced credentials, a local server needs to host a GET endpoint to return the OIDC token.
The response can be in plain text or JSON. Additional required request headers can also be
specified.

To generate a URL-sourced OIDC workforce identity configuration, run the following command:

```bash
# Generate an OIDC configuration file for URL-sourced credentials.
gcloud iam workforce-pools create-cred-config \
locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
--subject-token-type=urn:ietf:params:oauth:token-type:id_token \
--credential-source-url=$URL_TO_RETURN_OIDC_ID_TOKEN \
--credential-source-headers $HEADER_KEY=$HEADER_VALUE \
--workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \
--output-file=/path/to/generated/config.json
```

Where the following variables need to be substituted:
- `$WORKFORCE_POOL_ID`: The workforce pool ID.
- `$PROVIDER_ID`: The provider ID.
- `$URL_TO_RETURN_OIDC_ID_TOKEN`: The URL of the local server endpoint.
- `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to
`$URL_TO_GET_OIDC_TOKEN`, e.g. `Metadata-Flavor=Google`.
- `$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).

To generate a URL-sourced SAML configuration, run the following command:

```bash
# Generate a SAML configuration file for file-sourced credentials.
gcloud iam workforce-pools create-cred-config \
locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
--subject-token-type=urn:ietf:params:oauth:token-type:saml2 \
--credential-source-url=$URL_TO_GET_SAML_ASSERTION \
--credential-source-headers $HEADER_KEY=$HEADER_VALUE \
--workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \
--output-file=/path/to/generated/config.json
```

These commands generate the configuration file in the specified output file.

Where the following variables need to be substituted:
- `$WORKFORCE_POOL_ID`: The workforce pool ID.
- `$PROVIDER_ID`: The provider ID.
- `$URL_TO_GET_SAML_ASSERTION`: The URL of the local server endpoint.
- `$HEADER_KEY` and `$HEADER_VALUE`: The additional header key/value pairs to pass along the GET request to
`$URL_TO_GET_SAML_ASSERTION`, e.g. `Metadata-Flavor=Google`.
- `$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).

#### Using Executable-sourced workforce credentials with OIDC and SAML

**Executable-sourced credentials**
For executable-sourced credentials, a local executable is used to retrieve the 3rd party token.
The executable must handle providing a valid, unexpired OIDC ID token or SAML assertion in JSON format
to stdout.

To use executable-sourced credentials, the `GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES`
environment variable must be set to `1`.

To generate an executable-sourced workforce identity configuration, run the following command:

```bash
# Generate a configuration file for executable-sourced credentials.
gcloud iam workforce-pools create-cred-config \
locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID \
--subject-token-type=$SUBJECT_TOKEN_TYPE \
# The absolute path for the program, including arguments.
# e.g. --executable-command="/path/to/command --foo=bar"
--executable-command=$EXECUTABLE_COMMAND \
# Optional argument for the executable timeout. Defaults to 30s.
# --executable-timeout-millis=$EXECUTABLE_TIMEOUT \
# Optional argument for the absolute path to the executable output file.
# See below on how this argument impacts the library behaviour.
# --executable-output-file=$EXECUTABLE_OUTPUT_FILE \
--workforce-pool-user-project=$WORKFORCE_POOL_USER_PROJECT \
--output-file /path/to/generated/config.json
```
Where the following variables need to be substituted:
- `$WORKFORCE_POOL_ID`: The workforce pool ID.
- `$PROVIDER_ID`: The provider ID.
- `$SUBJECT_TOKEN_TYPE`: The subject token type.
- `$EXECUTABLE_COMMAND`: The full command to run, including arguments. Must be an absolute path to the program.
- `$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).

The `--executable-timeout-millis` flag is optional. This is the duration for which
the auth library will wait for the executable to finish, in milliseconds.
Defaults to 30 seconds when not provided. The maximum allowed value is 2 minutes.
The minimum is 5 seconds.

The `--executable-output-file` flag is optional. If provided, the file path must
point to the 3rd party credential response generated by the executable. This is useful
for caching the credentials. By specifying this path, the Auth libraries will first
check for its existence before running the executable. By caching the executable JSON
response to this file, it improves performance as it avoids the need to run the executable
until the cached credentials in the output file are expired. The executable must
handle writing to this file - the auth libraries will only attempt to read from
this location. The format of contents in the file should match the JSON format
expected by the executable shown below.

To retrieve the 3rd party token, the library will call the executable
using the command specified. The executable's output must adhere to the response format
specified below. It must output the response to stdout.

Refer to the [using executable-sourced credentials with Workload Identity Federation](#using-executable-sourced-credentials-with-oidc-and-saml)
above for the executable response specification.

##### Security considerations
The following security practices are highly recommended:
* 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.
* The configuration file should not be modifiable. Write access should be restricted to avoid processes modifying the executable command portion.

Given the complexity of using executable-sourced credentials, it is recommended to use
the existing supported mechanisms (file-sourced/URL-sourced) for providing 3rd party
credentials unless they do not meet your specific requirements.

You can now [use the Auth library](#using-external-identities) to call Google Cloud
resources from an OIDC or SAML provider.

### Using External Identities

External identities can be used with `Application Default Credentials`. In order to use external identities with
Application Default Credentials, you need to generate the JSON credentials configuration file for your external identity
as described above. Once generated, store the path to this file in the`GOOGLE_APPLICATION_CREDENTIALS` environment variable.

```bash
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/config.json
Expand Down
Expand Up @@ -401,6 +401,7 @@ static ExternalAccountCredentials fromJson(
.setQuotaProjectId(quotaProjectId)
.setClientId(clientId)
.setClientSecret(clientSecret)
.setWorkforcePoolUserProject(userProject)
.build();
}
return IdentityPoolCredentials.newBuilder()
Expand Down
Expand Up @@ -196,8 +196,7 @@ void fromJson_identityPoolCredentialsWorkforce() {
assertEquals("subjectTokenType", credential.getSubjectTokenType());
assertEquals(STS_URL, credential.getTokenUrl());
assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
assertEquals(
"userProject", ((IdentityPoolCredentials) credential).getWorkforcePoolUserProject());
assertEquals("userProject", credential.getWorkforcePoolUserProject());
assertNotNull(credential.getCredentialSource());
}

Expand Down Expand Up @@ -235,6 +234,30 @@ void fromJson_pluggableAuthCredentials() {
assertNull(source.getOutputFilePath());
}

@Test
void fromJson_pluggableAuthCredentialsWorkforce() {
ExternalAccountCredentials credential =
ExternalAccountCredentials.fromJson(
buildJsonPluggableAuthWorkforceCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);

assertTrue(credential instanceof PluggableAuthCredentials);
assertEquals(
"//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider",
credential.getAudience());
assertEquals("subjectTokenType", credential.getSubjectTokenType());
assertEquals(STS_URL, credential.getTokenUrl());
assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
assertEquals("userProject", credential.getWorkforcePoolUserProject());

assertNotNull(credential.getCredentialSource());

PluggableAuthCredentialSource source =
(PluggableAuthCredentialSource) credential.getCredentialSource();
assertEquals("command", source.getCommand());
assertEquals(30000, source.getTimeoutMs()); // Default timeout is 30s.
assertNull(source.getOutputFilePath());
}

@Test
void fromJson_pluggableAuthCredentials_allExecutableOptionsSet() {
GenericJson json = buildJsonPluggableAuthCredential();
Expand Down Expand Up @@ -502,25 +525,35 @@ void exchangeExternalCredentialForAccessToken_withInternalOptions() throws IOExc
@Test
void exchangeExternalCredentialForAccessToken_workforceCred_expectUserProjectPassedToSts()
throws IOException {
ExternalAccountCredentials credential =
ExternalAccountCredentials identityPoolCredential =
ExternalAccountCredentials.fromJson(
buildJsonIdentityPoolWorkforceCredential(), transportFactory);

StsTokenExchangeRequest stsTokenExchangeRequest =
StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
ExternalAccountCredentials pluggableAuthCredential =
ExternalAccountCredentials.fromJson(
buildJsonPluggableAuthWorkforceCredential(), transportFactory);

AccessToken accessToken =
credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
List<ExternalAccountCredentials> credentials =
Arrays.asList(identityPoolCredential, pluggableAuthCredential);

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

// Validate internal options set.
Map<String, String> query =
TestUtils.parseQuery(transportFactory.transport.getLastRequest().getContentAsString());
GenericJson internalOptions = new GenericJson();
internalOptions.setFactory(OAuth2Utils.JSON_FACTORY);
internalOptions.put("userProject", "userProject");
assertEquals(internalOptions.toString(), query.get("options"));
AccessToken accessToken =
credentials.get(i).exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);

assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());

// Validate internal options set.
Map<String, String> query =
TestUtils.parseQuery(transportFactory.transport.getLastRequest().getContentAsString());
GenericJson internalOptions = new GenericJson();
internalOptions.setFactory(OAuth2Utils.JSON_FACTORY);
internalOptions.put("userProject", "userProject");
assertEquals(internalOptions.toString(), query.get("options"));
assertEquals(i + 1, transportFactory.transport.getRequests().size());
}
}

@Test
Expand Down Expand Up @@ -813,6 +846,14 @@ private GenericJson buildJsonPluggableAuthCredential() {
return json;
}

private GenericJson buildJsonPluggableAuthWorkforceCredential() {
GenericJson json = buildJsonPluggableAuthCredential();
json.put(
"audience", "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider");
json.put("workforce_pool_user_project", "userProject");
return json;
}

static class TestExternalAccountCredentials extends ExternalAccountCredentials {
static class TestCredentialSource extends IdentityPoolCredentials.IdentityPoolCredentialSource {
protected TestCredentialSource(Map<String, Object> credentialSourceMap) {
Expand Down

0 comments on commit 7f2c535

Please sign in to comment.