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
Comment thread packages/google-auth/google/auth/compute_engine/_metadata.py Outdated
Comment thread packages/google-auth/tests/compute_engine/test_credentials.py Outdated
Comment thread packages/google-auth/tests/compute_engine/test__metadata.py Outdated
) = _agent_identity_utils.get_agent_identity_certificate_and_bytes()

expected_certs = NON_AGENT_IDENTITY_CERT_BYTES + NON_AGENT_IDENTITY_CERT_BYTES
assert isinstance(cert, x509.Certificate)

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.

Can we use AGENT_IDENTITY_CERT_BYTES as the first block and NON_AGENT_IDENTITY_CERT_BYTES as the second in combined_bundle, and assert _is_agent_identity_certificate(cert)? Right now both blocks are NON_AGENT_IDENTITY_CERT_BYTES, so changing return certs[0] to return certs[-1] in parse_certificate still passes the test suite.

"""Environment variable to prevent agent token sharing for GCP services."""
"""Environment variable to prevent agent token sharing for GCP services.

.. deprecated::

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: .. deprecated:: needs a version argument on the same line like credentials.py line 93, otherwise Sphinx renders the first line of text as the version string.

file. This variable is the fallback of GOOGLE_API_CERTIFICATE_CONFIG."""

GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN = "GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN"
"""Environment variable controlling whether to enable runtime bound tokens."""

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: Can we note in the docstring that this defaults to enabled, only a case-insensitive false disables it, and setting it takes precedence over GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES?

def test_parse_certificate_full_chain_rejects_malformed_intermediate(
self, second_cert_block, monkeypatch
):
monkeypatch.delattr(x509, "load_pem_x509_certificates", raising=False)

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: Can we drop this monkeypatch.delattr line and the unused monkeypatch parameter now that parse_certificate calls load_pem_x509_certificate instead of load_pem_x509_certificates?

"""
try:
from cryptography import x509
from google.auth.transport import _mtls_helper

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: Can we move from google.auth.transport import _mtls_helper outside the try block like lines 264 and 412 so an import error in _mtls_helper does not get reported as CRYPTOGRAPHY_NOT_FOUND_ERROR?

if len(cert_match) != 1:
if not cert_match:
raise exceptions.ClientCertError("Client SSL certificate is missing or invalid")
cert_chain = b"".join(

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: Can we extract a small helper for this 4-line PEM block join and reuse it here and in _read_cert_file at line 543, plus add a test case where the first block has no trailing newline so the else branch is covered?

return get(request, path, params={"recursive": "true"})


def _get_token_request_params(metrics_header_value):

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: Could we rename _get_token_request_params to _build_token_request_options since params in this file refers to the URL query parameters dict right next to it at line 533?

_, kwargs = request.call_args
url = kwargs["url"]
assert "bindCertificateFingerprint" not in url
assert kwargs.get("method", "GET") == "GET"

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: Can we change kwargs.get("method", "GET") == "GET" to kwargs["method"] == "GET" here and at line 870 like test_credentials.py line 979 does, so the test fails if method is omitted?

"https://my-service-123456.us-central1.mtls.run.app/v1",
],
)
def test_cert_rotation_triggered_on_psc_url(self, mtls_url):

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: Can we rename test_cert_rotation_triggered_on_psc_url to test_cert_rotation_triggered_on_mtls_url here and in test_urllib3.py line 705 now that it is parametrized over both PSC and Cloud Run URLs?

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