Conversation
There was a problem hiding this comment.
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.
| # look up the certificate. | ||
| is_opted_out = ( | ||
| os.environ.get( | ||
| environment_vars.GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES, |
There was a problem hiding this comment.
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_AGENTinfix) as well as the legacyGOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICESflag.
Could we update this check to look for both env vars so we maintain backward compatibility while adopting the unified standard?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Just double checking - did this get finalized now?
|
Both ID token unit tests ( |
|
Can't be merged before CR MDS is ready. Currently targeting a date between July 31 and Aug 7. |
2766928 to
ccc2a5d
Compare
| # look up the certificate. | ||
| is_opted_out = ( | ||
| os.environ.get( | ||
| environment_vars.GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES, |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Yes! that's a great suggestion! Done!
| ( | ||
| cert, | ||
| cert_bytes, | ||
| ) = _agent_identity_utils.get_agent_identity_certificate_and_bytes() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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.
…and update docstrings and comments
…eplace outdated mocks
…llback to deprecated var
…certificate_and_bytes
ccc2a5d to
f7fdd75
Compare
| metrics_header = { | ||
| metrics.API_CLIENT_HEADER: metrics.token_request_access_token_mds() | ||
| } | ||
| cert, cert_bytes = _agent_identity_utils.get_agent_identity_certificate_and_bytes() |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
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) andIDTokenCredentials.refresh()(for ID tokens) to send a POST request with thecertificate_chainpayload instead of a GET request when bound tokens are supported.Update
_metadata.get()helper to supportmethodandbodyparams.Add and update unit tests to verify the new POST request flows.
design: go/sdk-mds-bound-token
id token verification:
Note:
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.
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.
it still uses the existing
GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICESflag to opt out. We might update it and add a new env var, but will keep the old one for backward compatibility.