From 7f2c535ab7c842a672d6761f4cd80df88e1a37ed Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Fri, 5 Aug 2022 11:10:58 -0700 Subject: [PATCH] feat: workforce identity federation for pluggable auth (#959) * feat: add workforce support to ADC with pluggable auth * feat: document workforce identity federation --- README.md | 212 +++++++++++++++++- .../oauth2/ExternalAccountCredentials.java | 1 + .../ExternalAccountCredentialsTest.java | 71 ++++-- 3 files changed, 262 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 8882f7a74..762929749 100644 --- a/README.md +++ b/README.md @@ -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) @@ -446,6 +449,7 @@ 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. @@ -453,7 +457,7 @@ These environment variables can be used by the executable to avoid hard-coding t ##### 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 @@ -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 diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 85af46335..0d6f3419b 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -401,6 +401,7 @@ static ExternalAccountCredentials fromJson( .setQuotaProjectId(quotaProjectId) .setClientId(clientId) .setClientSecret(clientSecret) + .setWorkforcePoolUserProject(userProject) .build(); } return IdentityPoolCredentials.newBuilder() diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 1b2b53a1c..75b88dcfa 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -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()); } @@ -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(); @@ -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 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 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 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 @@ -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 credentialSourceMap) {