Skip to content

feat(auth): add bound token support for access and JWT id tokens for Cloud Run - #17698

Open
nbayati wants to merge 13 commits into
googleapis:mainfrom
nbayati:bound_token_post
Open

nbayati wants to merge 13 commits into
googleapis:mainfrom
nbayati:bound_token_post

Conversation

@nbayati

@nbayati nbayati commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

This PR adds the following:

  • Switch the MDS token acquisition from a GET to a POST request when the agentic cert is detected.

  • Add get_agent_identity_certificate_and_bytes() utility to read the raw certificate bytes alongside the parsed cert.

  • Update _metadata.get_service_account_token() (for access tokens) and IDTokenCredentials.refresh() (for ID tokens) to send a POST request with the certificate_chain payload instead of a GET request when bound tokens are supported.

  • Update _metadata.get() helper to support method and body params.

  • Add and update unit tests to verify the new POST request flows.


design: go/sdk-mds-bound-token

id token verification:

  • test script: paste/4514812804595712
  • log results: paste/6316867684794368

Note:

  1. This PR relies on the existing pattern of locating the certificates using the path provided by the config file available at GOOGLE_API_CERTIFICATE_CONFIG. It does not currently fallback on checking the well known location if the env var is not set, which would limit the scope to CR, as GKE and GCE don't set this env var.

  2. It uses the same condition to decide if a bound token should be requested for both access token and id token. We might decide to add a separate env var to opt out.

  3. it still uses the existing GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES flag to opt out. We might update it and add a new env var, but will keep the old one for backward compatibility.

@nbayati
nbayati requested review from a team as code owners July 13, 2026 05:15
@nbayati
nbayati requested a review from lsirac July 13, 2026 05:16

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Google Auth library to request bound tokens from the Compute Engine metadata server using a POST request with the certificate chain in the body, rather than passing a fingerprint in the URL. To support this, get_agent_identity_certificate_and_bytes was introduced to retrieve both the parsed certificate and its raw bytes, and the metadata get helper was updated to support POST requests and bodies. Feedback on the changes suggests simplifying a redundant tuple check in credentials.py by directly unpacking the returned value from get_agent_identity_certificate_and_bytes.

Comment thread packages/google-auth/google/auth/compute_engine/credentials.py Outdated
Comment thread packages/google-auth/google/auth/compute_engine/_metadata.py
Comment thread packages/google-auth/google/auth/compute_engine/_metadata.py Outdated
# look up the certificate.
is_opted_out = (
os.environ.get(
environment_vars.GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From AI code review

To align with the cross-SDK standardization for certificate-bound tokens, we should support both the standard environment variable GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES (without the _AGENT infix) as well as the legacy GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES flag.

Could we update this check to look for both env vars so we maintain backward compatibility while adopting the unified standard?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are waiting for the product decision on the new env var name as GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES is not finalized yet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just double checking - did this get finalized now?

Comment thread packages/google-auth/google/auth/compute_engine/credentials.py Outdated
Comment thread packages/google-auth/google/auth/compute_engine/credentials.py Outdated
Comment thread packages/google-auth/google/auth/compute_engine/_metadata.py Outdated
Comment thread packages/google-auth/google/auth/compute_engine/_metadata.py Outdated
Comment thread packages/google-auth/tests/compute_engine/test__metadata.py Outdated
Comment thread packages/google-auth/tests/compute_engine/test_credentials.py
Comment thread packages/google-auth/tests/compute_engine/test__metadata.py Outdated
@lsirac

lsirac commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Both ID token unit tests (test_refresh_with_agent_identity and test_refresh_with_agent_identity_opt_out_or_not_agent) return fake certificates from get_agent_identity_certificate_and_bytes(). Please add a unit test where get_agent_identity_certificate_and_bytes() returns (None, None) so the standard fallback path (running without an agent identity certificate on disk) is fully covered.

@nbayati nbayati added the do not merge Indicates a pull request not ready for merge, due to either quality or timing. label Jul 17, 2026
@nbayati

nbayati commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Can't be merged before CR MDS is ready. Currently targeting a date between July 31 and Aug 7.

# look up the certificate.
is_opted_out = (
os.environ.get(
environment_vars.GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just double checking - did this get finalized now?

return_none_for_not_found_error (Optional[bool]): If True, returns None
for 404 error instead of throwing an exception.
method (str): The HTTP method to use for the request. Defaults to "GET".
body (Optional[bytes]): The HTTP request body payload to send. Defaults to None.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to raise a ValueError (or similar) here to "exit early" if a body is specified byt the method is GET. While I think technically valid to include a body in GET requests (most often I think the body just gets ignored), it may lead a caller to think it is getting a bound token when in reality it isn't?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! that's a great suggestion! Done!

(
cert,
cert_bytes,
) = _agent_identity_utils.get_agent_identity_certificate_and_bytes()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It looks like both this and should_request_bound_token check GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES and call _mtls_helper._check_use_client_cert_env() - I wonder if we can optimize this in any way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you're right, they do both check the env var but I don't think we can eliminate it because the two methods have different callers and come from different paths (compute engine and identity pool) so we need to have the check in both places. We could probably do some refactoring, but I'm leaning toward keeping the code as is since the env var reading is not an expensive operation and this way we can keep the methods self contained.

return None
return None, None

return parse_certificate(cert_bytes), cert_bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If cert_path points to a combined bundle (credentialbundle.pem), sending raw cert_file.read() puts the private key into certificate_chain over plain HTTP (and GKE MDS rejects non-CERTIFICATE PEM blocks with 400). Also, cert_bytes.decode("utf-8") will raise an uncaught UnicodeDecodeError if there are non-UTF-8 OpenSSL bag attributes outside the PEM boundaries.

We should extract only the CERTIFICATE blocks before returning, e.g. with a non-greedy r"-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----\r?\n?" (_mtls_helper._CERT_REGEX is greedy and would still grab an interleaved key).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this PR discovery only resolves cert_path from GOOGLE_API_CERTIFICATE_CONFIG (which on Cloud Run points to the standalone certificates.pem file). Automatic discovery of GKE's combined credentialbundle.pem is not active here.

In our follow-up PR adding GKE support, get_agent_identity_certificate_and_bytes() will be updated to extract only -----BEGIN CERTIFICATE-----...-----END CERTIFICATE----- blocks via non-greedy regex. That strips any private key blocks from combined bundles and discards any non-UTF-8 OpenSSL bag attributes outside the PEM boundaries prior to UTF-8 decoding.

I'll mark this as resolved since it's out of the scope of this PR and will be addressed in the GKE PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix this in this PR before merging rather than deferring to the GKE follow-up. GOOGLE_API_CERTIFICATE_CONFIG is not Cloud Run specific. GKE/GCE can set this today with cert_path and key_path pointing to the same combined PEM bundle.

Comment thread packages/google-auth/google/auth/_agent_identity_utils.py
nbayati and others added 7 commits September 17, 2026 21:02
Switch the MDS token acquisition from a GET to a POST request when the agentic cert is detected.

* Add `get_agent_identity_certificate_and_bytes()` utility to read the raw certificate bytes alongside the parsed cert.

* Update `_metadata.get_service_account_token()` (for access tokens) and `IDTokenCredentials.refresh()` (for ID tokens) to send a POST request with the `certificate_chain` payload instead of a GET request when bound tokens are supported.

* Update `_metadata.get()` helper to support `method` and `body` params.

* Add and update unit tests to verify the new POST request flows.
@nbayati nbayati removed the do not merge Indicates a pull request not ready for merge, due to either quality or timing. label Sep 18, 2026
@nbayati nbayati removed their assignment Sep 18, 2026
Comment thread packages/google-auth/tests/test_agent_identity_utils.py
Comment thread packages/google-auth/google/auth/_agent_identity_utils.py Outdated
Comment thread packages/google-auth/google/auth/compute_engine/_metadata.py
Comment thread packages/google-auth/google/auth/_agent_identity_utils.py Outdated
Comment thread packages/google-auth/google/auth/_agent_identity_utils.py Outdated
metrics_header = {
metrics.API_CLIENT_HEADER: metrics.token_request_access_token_mds()
}
cert, cert_bytes = _agent_identity_utils.get_agent_identity_certificate_and_bytes()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the 12-line block that upgrades method to POST, encodes certificate_chain, and sets headers["Content-Type"] is identical in _metadata.get_service_account_token() and IDTokenCredentials._call_metadata_identity_endpoint(). Could we extract a small helper so both endpoints share one implementation?

cert_path = tmpdir.join("cert.pem")
cert_path.write(b"cert_content")
mock_get_path.return_value = str(cert_path)
import json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: base64 is already imported at the top of test_credentials.py on line 14. Let's move import json and from google.auth import metrics to top-level imports as well and drop the duplicate inline imports in these test methods.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

def test_get_service_account_token_no_cert(mock_get_and_parse):
# Test that no fingerprint is added when no certificate is found.
mock_get_and_parse.return_value = None
mock_get_and_parse.return_value = (None, None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the comments on lines 840 and 860 still say no fingerprint is added from the removed query-param design. Let's update them to say a standard GET request with body=None is sent, and rename mock_get_and_parse to mock_get_cert_and_bytes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants