google-auth-library-python icon indicating copy to clipboard operation
google-auth-library-python copied to clipboard

fetch_id_token_credentials doesn't follow AIP-4110

Open juzna opened this issue 2 years ago • 2 comments

AIP-4110 specifies where should client libraries load credentials from. It's correctly used by google.auth.default(), which loads the OAuth credentials (ie credentials with access token).

In our service, we need the same default credentials, but our use case requires OpenID credentials. google.oauth2.id_token.fetch_id_token_credentials() pretty much does this, as it also states in the doc string:

Create the ID Token credentials from the current environment

This function acquires ID token from the environment in the following order. See https://google.aip.dev/auth/4110.

However, there are notable differences in the OpenID flow:

  1. it doesn't support impersonated or external credentials
  2. it doesn't check "gcloud default credentials through its default path", as stated in the AIP

It would be good if fetch_id_token_credentials() would match google.auth.default().

juzna avatar Jun 10 '23 08:06 juzna

We currently use a workaround:

  1. we get default OAuth credentials from google.auth.default()
  2. based on the type, we decide what OpenID credentials should be returned

The following code works, but it's not complete (doesn't support all kinds of credentials) and also does some redundant requests to metadata server.

def fixed_fetch_id_token_credentials(audience: str, request=None):
    """Get OpenID credentials from the current environment.

    NOTE: This is needed only because google.oauth2.id_token.fetch_id_token_credentials doesn't support impersonated
    credentials. Once it does, this function can be removed.
    """
    creds, _project_id = google.auth.default()

    if request is None:
        request = google.auth.transport.requests.Request()

    if isinstance(creds, google.auth.impersonated_credentials.Credentials):
        id_creds = google.auth.impersonated_credentials.IDTokenCredentials(
            creds, audience, include_email=True
        )

    elif isinstance(creds, google.oauth2.service_account.Credentials):
        id_creds = google.oauth2.service_account.IDTokenCredentials(
            signer=creds.signer,
            service_account_email=creds.service_account_email,
            token_uri=creds._token_uri,
            quota_project_id=creds.quota_project_id,
            target_audience=audience,
        )

    elif isinstance(creds, google.auth.compute_engine.credentials.Credentials):
        id_creds = google.auth.compute_engine.credentials.IDTokenCredentials(
            request,
            audience,
            use_metadata_identity_endpoint=True,
            quota_project_id=creds.quota_project_id,
        )

    elif isinstance(creds, google.oauth2.credentials.Credentials):
        raise ValueError(
            "IDTokens are not supported for human accounts. Provide a service account instead."
        )

    else:
        raise ValueError(f"Unknown credentials type {type(creds)}")

    return id_creds

We'd appreciate the library supporting this natively so that we could delete the code.

juzna avatar Jun 10 '23 08:06 juzna

Indeed! I just came across this difference as well, as in the Go library it's been implemented adherent to the AIP spec.

stijntratsaertit avatar Oct 30 '23 11:10 stijntratsaertit