diff --git a/CHANGELOG.md b/CHANGELOG.md index e08322a6a..7b1d296cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ [1]: https://pypi.org/project/google-auth/#history +## [2.11.0](https://github.com/googleapis/google-auth-library-python/compare/v2.10.0...v2.11.0) (2022-08-18) + + +### Features + +* add integration tests for configurable token lifespan ([#1103](https://github.com/googleapis/google-auth-library-python/issues/1103)) ([124bae6](https://github.com/googleapis/google-auth-library-python/commit/124bae60771a8984674a1d7eeab3ec22b2fa0033)) + + +### Bug Fixes + +* Async certificate retrieving ([#1101](https://github.com/googleapis/google-auth-library-python/issues/1101)) ([05f125d](https://github.com/googleapis/google-auth-library-python/commit/05f125def1205a14db52c870f2bfcef47f047206)) + ## [2.10.0](https://github.com/googleapis/google-auth-library-python/compare/v2.9.1...v2.10.0) (2022-08-05) diff --git a/docs/user-guide.rst b/docs/user-guide.rst index 16de58d9d..e689b11c6 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -544,6 +544,251 @@ For AWS providers, use :meth:`aws.Credentials.from_info ['https://www.googleapis.com/auth/cloud-platform']) +External credentials (Workforce identity federation) +++++++++++++++++++++++++++++++++++++++++++++++++++++ + +`Workforce identity federation`_ lets you use an external identify 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)`_, 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`_ 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`_. + +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: + +.. code-block:: 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`_. + +To generate a file-sourced SAML configuration, run the following command: + +.. code-block:: 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`_. + +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: + +.. code-block:: 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`_. + +To generate a URL-sourced SAML configuration, run the following command: + +.. code-block:: 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`_. + +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: + +.. code-block:: 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. + +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 `__ 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. + + +.. _Workforce Identity Federation: + https://cloud.google.com/iam/docs/workforce-identity-federation +.. _OpenID Connect (OIDC): https://openid.net/connect/ +.. _instructions: + https://cloud.google.com/iam/docs/configuring-workforce-identity-federation +.. _gcloud CLI: https://cloud.google.com/sdk/ +.. _workforce pools user project: + https://cloud.google.com/iam/docs/workforce-identity-federation#workforce-pools-user-project + + Impersonated credentials ++++++++++++++++++++++++ diff --git a/google/auth/version.py b/google/auth/version.py index bcf2a36f4..10a1c7fd9 100644 --- a/google/auth/version.py +++ b/google/auth/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.10.0" +__version__ = "2.11.0" diff --git a/google/oauth2/_id_token_async.py b/google/oauth2/_id_token_async.py index b90994cdc..c32dfa47d 100644 --- a/google/oauth2/_id_token_async.py +++ b/google/oauth2/_id_token_async.py @@ -93,9 +93,9 @@ async def _fetch_certs(request, certs_url): "Could not fetch certificates at {}".format(certs_url) ) - data = await response.data.read() + data = await response.content() - return json.loads(data.decode("utf-8")) + return json.loads(data) async def verify_token( diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index f5652a5a4..ec376950b 100644 Binary files a/system_tests/secrets.tar.enc and b/system_tests/secrets.tar.enc differ diff --git a/system_tests/system_tests_sync/test_external_accounts.py b/system_tests/system_tests_sync/test_external_accounts.py index 59fbd4bef..29355f479 100644 --- a/system_tests/system_tests_sync/test_external_accounts.py +++ b/system_tests/system_tests_sync/test_external_accounts.py @@ -42,6 +42,7 @@ import sys import google.auth +from google.auth import _helpers from googleapiclient import discovery from six.moves import BaseHTTPServer from google.oauth2 import service_account @@ -132,7 +133,6 @@ def get_project_dns(dns_access, credential_data): with NamedTemporaryFile() as credfile: credfile.write(json.dumps(credential_data).encode("utf-8")) credfile.flush() - old_credentials = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") with patch.dict(os.environ, {"GOOGLE_APPLICATION_CREDENTIALS": credfile.name}): # If our setup and credential file are correct, @@ -150,9 +150,7 @@ def get_xml_value_by_tagname(data, tagname): # This test makes sure that setting an accesible credential file # works to allow access to Google resources. -def test_file_based_external_account( - oidc_credentials, service_account_info, dns_access -): +def test_file_based_external_account(oidc_credentials, dns_access): with NamedTemporaryFile() as tmpfile: tmpfile.write(oidc_credentials.token.encode("utf-8")) tmpfile.flush() @@ -173,10 +171,11 @@ def test_file_based_external_account( }, ) + # This test makes sure that setting a token lifetime works # for service account impersonation. def test_file_based_external_account_with_configure_token_lifetime( - oidc_credentials, service_account_info, dns_access + oidc_credentials, dns_access ): with NamedTemporaryFile() as tmpfile: tmpfile.write(oidc_credentials.token.encode("utf-8")) @@ -202,6 +201,47 @@ def test_file_based_external_account_with_configure_token_lifetime( ) +def test_configurable_token_lifespan(oidc_credentials, http_request): + TOKEN_LIFETIME_SECONDS = 2800 + BUFFER_SECONDS = 5 + + def check_impersonation_expiration(): + # First, get the default credentials. + credentials, _ = google.auth.default( + scopes=["https://www.googleapis.com/auth/cloud-platform.read-only"], + request=http_request, + ) + + utcmax = _helpers.utcnow() + datetime.timedelta(seconds=TOKEN_LIFETIME_SECONDS) + utcmin = utcmax - datetime.timedelta(seconds=BUFFER_SECONDS) + assert utcmin < credentials._impersonated_credentials.expiry <= utcmax + + return True + + with NamedTemporaryFile() as tmpfile: + tmpfile.write(oidc_credentials.token.encode("utf-8")) + tmpfile.flush() + + assert get_project_dns( + check_impersonation_expiration, + { + "type": "external_account", + "audience": _AUDIENCE_OIDC, + "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", + "token_url": "https://sts.googleapis.com/v1/token", + "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken".format( + oidc_credentials.service_account_email + ), + "service_account_impersonation": { + "token_lifetime_seconds": TOKEN_LIFETIME_SECONDS, + }, + "credential_source": { + "file": tmpfile.name, + }, + }, + ) + + # This test makes sure that setting up an http server to provide credentials # works to allow access to Google resources. def test_url_based_external_account(dns_access, oidc_credentials, service_account_info): @@ -337,9 +377,7 @@ def test_aws_based_external_account( # This test makes sure that setting up an executable to provide credentials # works to allow access to Google resources. -def test_pluggable_external_account( - oidc_credentials, service_account_info, dns_access -): +def test_pluggable_external_account(oidc_credentials, service_account_info, dns_access): now = datetime.datetime.now() unix_seconds = time.mktime(now.timetuple()) expiration_time = (unix_seconds + 1 * 60 * 60) * 1000 @@ -354,7 +392,7 @@ def test_pluggable_external_account( tmpfile = NamedTemporaryFile(delete=True) with open(tmpfile.name, "w") as f: f.write("#!/bin/bash\n") - f.write("echo \"{}\"\n".format(json.dumps(credential).replace('"', '\\"'))) + f.write('echo "{}"\n'.format(json.dumps(credential).replace('"', '\\"'))) tmpfile.file.close() os.chmod(tmpfile.name, 0o777) diff --git a/tests_async/oauth2/test_id_token.py b/tests_async/oauth2/test_id_token.py index a52b8b4e0..9be086dca 100644 --- a/tests_async/oauth2/test_id_token.py +++ b/tests_async/oauth2/test_id_token.py @@ -33,7 +33,7 @@ def make_request(status, data=None): if data is not None: response.data = mock.AsyncMock(spec=["__call__", "read"]) - response.data.read = mock.AsyncMock( + response.content = mock.AsyncMock( spec=["__call__"], return_value=json.dumps(data).encode("utf-8") )