Repository navigation
Commit acc7c9d
feat: Enable Bound Token for Agentic Identities (#13873)
This PR introduces a feature which enables the auth library to acquire
bound access-tokens and bound id-tokens in Agentic Environments.
1) We detect certs in default paths and check if they match the SPIFFE
format for agents.
2) If 1. is a yes then we call the MDS endpoint in a POST request with
the certificate in the body.
Note this PR was based on
#13169
---
## Manual Testing & End-to-End Verification
We verified this feature end-to-end across both a **Live Cloud Run Agent
Identity environment** (testing against the live Google Metadata Server,
Security Token Service, and Vertex AI with the **Java Agent Development
Kit (`com.google.adk:google-adk:1.9.0`)**) and a **10-Scenario Local
Mock MDS Simulation Harness** (testing exact HTTP request payloads, true
cryptographic certificate/key rotation, combined bundle private-key
stripping, well-known directory discovery, non-agent SPIFFE fallback,
environment variable precedence, non-atomic rotation retries, and
asynchronous container startup polling).
### 1. Live Cloud Run Agent Identity Verification (`<PROJECT_ID>`,
`us-central1`)
We deployed a containerized Java test application built against this
branch (`google-auth-library-oauth2-http:1.50.0-SNAPSHOT` @
`27776f6d62a`) + Java ADK (`com.google.adk:google-adk:1.9.0`) to Cloud
Run with Agent Identity enabled (`--functional-type=agent
--identity-type=agent-identity`).
#### Execution A: Default Bound Token Acquisition
(`GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN` unset / default `true`)
- **Cloud Run Job Executions**: `agent-bound-token-java-test-cjrgp`
(latest run with Step 4D) & `agent-bound-token-java-test-dd9gm`
- **Workload Certificate Discovery**:
- Detected
`/var/run/secrets/workload-spiffe-credentials/credentials.json`
- Extracted Leaf Certificate SAN URI:
`spiffe://agents.global.org-<ORG_NUMBER>.system.id.goog/resources/run/projects/<PROJECT_NUMBER>/locations/us-central1/jobs/agent-bound-token-java-test`
- Leaf Certificate SHA-256 (`x5t#S256`):
`5Ggf2ofOIkHkvBAwfku6eKQVI5pYzW50whzv-VwSAM8`
- **Bound Access Token (`ComputeEngineCredentials`)**:
- Successfully acquired bound OAuth2 Access Token via `POST` to Metadata
Server (`ya29.d.c0AZ4bNp...`).
- **Bound ID Token (`IdTokenCredentials`) & Cryptographic Binding
Verification (RFC 8705 § 3.1)**:
- Acquired bound ID Token for target audience
`https://example-target-service.run.app`.
- Decoded JWT payload inline (`textPayload`) and verified live Google
STS embedded the `cnf` (Confirmation) claim containing the SHA-256
thumbprint (`x5t#S256`) of the workload's leaf X.509 certificate:
```json
"cnf": {
"x5t#S256": "5Ggf2ofOIkHkvBAwfku6eKQVI5pYzW50whzv-VwSAM8"
}
```
- Verified an **exact byte-for-byte match** (`[PASS] JWT x5t#S256
thumbprint EXACTLY matches local leaf certificate SHA-256!`).
- **Java ADK (`com.google.adk:google-adk:1.9.0`) + `google-genai` & mTLS
Proof-of-Possession Verification (Steps 4A, 4B, 4C, 4D)**:
- **Step 4A (ADK `HttpClientFactory` Non-mTLS Transport)**: Inspected
ADK's shared `OkHttpClient` (`sun.security.ssl.SSLSocketFactoryImpl`, no
client cert). Calling Google APIs over this non-mTLS channel with the
bound token is rejected at the auth layer with **`HTTP 401
UNAUTHENTICATED`**.
- **Step 4B (Live ADK `LlmAgent` + `Gemini` Turn on Vertex AI)**:
Invoking `InMemoryRunner.runAsync(...)` with `Gemini`
(`gemini-2.5-flash`) fails on turn 1 with
`com.google.genai.errors.ClientException: 401 . Request had invalid
authentication credentials`, confirming the known limitation where
`google-genai` sends bound tokens over a non-mTLS channel.
- **Step 4C (True mTLS Contrast Test — Matching Workload Certificate)**:
Presenting the **exact same bound access token** over a true mTLS
`OkHttpClient` (configured with
`/var/run/secrets/workload-spiffe-credentials/certificates.pem` +
`private_key.pem`, `x5t#S256 =
5Ggf2ofOIkHkvBAwfku6eKQVI5pYzW50whzv-VwSAM8`) to
`https://cloudresourcemanager.mtls.googleapis.com/v1/projects/<PROJECT_ID>`
**passes authentication** (`HTTP 403 PERMISSION_DENIED` IAM check
instead of `401 UNAUTHENTICATED`), proving Google API Frontend verified
the token binding against the TLS client certificate handshake.
- **Step 4D (Sender-Constraining / Proof-of-Possession Negative Test —
Mismatched Client Certificate)**: Presenting the **exact same bound
access token** to
`https://cloudresourcemanager.mtls.googleapis.com/v1/projects/<PROJECT_ID>`
over an mTLS `OkHttpClient` configured with a **different** X.509 client
certificate (`x5t#S256 = -57ZjYVm89oczfdO02Hf3Sz-FaYQIVH1DD3c9zaBIKA`)
is **rejected at the auth layer with `HTTP 401 UNAUTHENTICATED`**,
confirming that Google API Frontend enforces cryptographic thumbprint
matching (`cnf.x5t#S256 == SHA256(TLS client cert)`).
#### Execution B: Opt-Out Unbound Token Acquisition
(`GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false`)
- **Cloud Run Job Execution**: `agent-bound-token-java-test-t7b98`
- Updated job environment variable
`--set-env-vars="GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false,GOOGLE_CLOUD_LOCATION=global,GOOGLE_GENAI_USE_VERTEXAI=true"`.
- Verified `ComputeEngineCredentials` and `IdTokenCredentials` fell back
to standard `HTTP GET` requests against MDS and issued standard unbound
tokens (decoded JWT payload confirmed absence of the `cnf` claim).
- **Java ADK Live Agent Turn (`Step 4B`)**: With
`GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false`, the unbound token works
over `google-genai`'s non-mTLS channel and the live ADK `LlmAgent` +
`Gemini` turn **SUCCEEDS**:
`[ADK Event] author=bound-token-verify-agent, content=Hello! Yes, as an
agent designed to test bound token behavior with the Java ADK, I am
expected to receive and process bound tokens.`
---
### 2. Local End-to-End Simulation & Wire Verification (10 Scenarios)
To verify internal wire-level, discovery, rotation,
environment-variable, and error-handling behavior between the client and
MDS, we executed our local simulation suite
(`LocalSimulationRunner.java`) spinning up a local Mock MDS
(`HttpServer`) across **10 end-to-end scenarios**:
1. **Bound Access Token (`ComputeEngineCredentials` +
`GOOGLE_API_CERTIFICATE_CONFIG`)**: Verified `HTTP POST` to
`/computeMetadata/v1/instance/service-accounts/default/token?scopes=https://www.googleapis.com/auth/cloud-platform`,
verified JSON body `{"certificate_chain": "-----BEGIN
CERTIFICATE-----\n..."}` (serialized as a single PEM string), and
verified no extra fields are included in the JSON payload.
2. **Bound ID Token (`IdTokenCredentials` +
`GOOGLE_API_CERTIFICATE_CONFIG`)**: Verified `HTTP POST` to
`/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://target.run.app`
(with `audience` passed as a URL query parameter) and JSON body
`{"certificate_chain": "-----BEGIN CERTIFICATE-----\n..."}`.
3. **True Live Cryptographic Certificate & Key Rotation on Disk (`Gen-1`
$\rightarrow$ `Gen-2`)**: Generated a new X.509 SPIFFE certificate and
matching 2048-bit RSA key pair on disk, updated file `mtime`, and
verified `AgentIdentityUtils.getAgentIdentityCertInfo()` invalidated its
cache, verified the new key pair, and transmitted the rotated `Gen-2`
certificate chain (`!req3.body.equals(req1.body)`).
4. **Opt-Out Override (`GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false`)**:
Verified setting `GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false` switches
requests back to `HTTP GET` with an empty request body.
5. **Well-Known Directory Combined Bundle (`credentialbundle.pem`) +
Private Key Stripping**: Unset `GOOGLE_API_CERTIFICATE_CONFIG`, wrote a
combined `credentialbundle.pem` containing **both** `-----BEGIN
CERTIFICATE-----` and `-----BEGIN PRIVATE KEY-----` in the well-known
directory, and verified that `AgentIdentityUtils` resolved `certPath ==
keyPath`, verified the key pair, and **stripped the `PRIVATE KEY`
block** from the transmitted `POST` payload.
6. **Well-Known Directory Separate Files (`certificates.pem` +
`private_key.pem`)**: Unset `GOOGLE_API_CERTIFICATE_CONFIG`, removed
`credentialbundle.pem`, placed separate `certificates.pem` and
`private_key.pem` in the well-known directory, and verified bound `HTTP
POST` acquisition.
7. **Non-Agent SPIFFE Trust Domain Fallback (`shouldRequestBoundToken ==
false`)**: Configured a valid X.509 certificate with a standard GKE
Workload Identity SAN
(`spiffe://my-standard-gke-project.svc.id.goog/ns/default/sa/my-ksa`);
verified `AgentIdentityUtils` did not throw, cached
`shouldRequestBoundToken = false`, and fell back to standard `HTTP GET`.
8. **Environment Variable Precedence &
`GOOGLE_API_USE_CLIENT_CERTIFICATE` Matrix**:
- **8a**: Legacy
`GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES=false` (with
`GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN` unset) $\rightarrow$ falls back
to `HTTP GET`.
- **8b**: Modern `GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=true` overrides
legacy `GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES=false`
$\rightarrow$ sends bound `HTTP POST`.
- **8c**: `GOOGLE_API_USE_CLIENT_CERTIFICATE=false` with valid agent
certs on disk $\rightarrow$ disables bound token (`HTTP GET`) without
startup polling.
- **8d**: `GOOGLE_API_USE_CLIENT_CERTIFICATE=true` with missing cert
files $\rightarrow$ fails closed with `IOException` after retries
instead of silently downgrading to an unbound token.
9. **Non-Atomic Rotation / Transient Key-Pair Mismatch Recovery
(`CERT_KEY_MATCH_RETRIES`)**: Wrote a new `Gen-3` non-prod SPIFFE
certificate
(`spiffe://agents-nonprod.global.org-54321.system.id.goog/...`) first
while delaying the matching `private_key.pem` update on a background
thread; verified `loadAndVerifyCredentials()` retried cleanly via
`CERT_KEY_MATCH_RETRIES` and transmitted `Gen-3`.
10. **Asynchronous Container Startup Credential Delivery Polling
(`TOTAL_POLL_CYCLES`)**: Started with an empty well-known directory on
initial startup (`GOOGLE_API_USE_CLIENT_CERTIFICATE=true`), delivered
`certificates.pem` + `private_key.pem` asynchronously from a background
thread after ~180ms, and verified the initial `refreshAccessToken()`
polled until the files arrived and succeeded with a bound `HTTP POST`.
---
### 3. Reproducible Test Artifacts & Execution Logs (`gpaste` - Internal
Corp Access Only)
| Artifact / Log | Description | Link |
| :--- | :--- | :--- |
| **Combined Verification Summary & Logs** | Complete report & console
outputs from Live Cloud Run (Bound `cjrgp` & Opt-Out `t7b98`) + Java ADK
+ Local 10-Scenario Simulation |
https://paste.googleplex.com/4671448266964992 |
| **`CloudRunAgentVerifyApp.java`** | Live Cloud Run + Java ADK
verification app (validates SPIFFE cert, ADC access token, ID token
`cnf.x5t#S256` match, and ADK Steps 4A/4B/4C/4D) |
https://paste.googleplex.com/5431546950057984 |
| **`LocalSimulationRunner.java`** | Self-contained local E2E test
harness with Mock MDS (`HttpServer`) testing all 10 scenarios |
https://paste.googleplex.com/4541159570014208 |
| **`Dockerfile`** | Multi-stage Docker build resolving Java ADK 1.9.0 +
`google-genai` and overlaying our local PR JAR |
https://paste.googleplex.com/6569302979903488 |
| **`deploy_cloud_run_job.sh`** | Turnkey script to build via Cloud
Build, deploy Cloud Run Job with `--identity-type=agent-identity`, and
execute | https://paste.googleplex.com/4908078735163392 |
| **`build_and_run_local.sh`** | Turnkey script to compile and run the
local simulation | https://paste.googleplex.com/6449866457350144 |
| **Raw Cloud Logging — Bound Run (`cjrgp`)** | Plain-text Cloud Run
logs from `gcloud logging read` for default Bound Token execution (with
inline decoded JWT & ADK Steps 4A/4B/4C/4D) |
https://paste.googleplex.com/5404054864396288 |
| **Raw Cloud Logging — Opt-Out Run (`t7b98`)** | Plain-text Cloud Run
logs from `gcloud logging read` for
`GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false` execution (showing live
ADK Gemini response) | https://paste.googleplex.com/5990735250325504 |
#### Quick Reproduction Steps
To replicate the live Cloud Run test in any GCP project with Agent
Identity enabled:
```bash
# 1. Place CloudRunAgentVerifyApp.java, LocalSimulationRunner.java, Dockerfile, and deploy_cloud_run_job.sh
# in a directory alongside your local google-cloud-java checkout.
chmod +x deploy_cloud_run_job.sh build_and_run_local.sh
# 2. Run the local 10-scenario simulation suite:
./build_and_run_local.sh
# 3. Deploy and execute the Cloud Run Job with Agent Identity enabled (Default Bound Token mode):
./deploy_cloud_run_job.sh <PROJECT_ID> us-central1
# 4. To test opt-out behavior (unblocks ADK non-mTLS Gemini calls):
gcloud alpha run jobs update agent-bound-token-java-test \
--project=<PROJECT_ID> --region=us-central1 \
--set-env-vars="GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false,GOOGLE_CLOUD_LOCATION=global,GOOGLE_GENAI_USE_VERTEXAI=true"
gcloud alpha run jobs execute agent-bound-token-java-test \
--project=<PROJECT_ID> --region=us-central1 --wait
```
---------
Co-authored-by: Pranav Iyer <pjiyer@google.com>1 parent a86cc11 commit acc7c9d
16 files changed
Lines changed: 3860 additions & 26 deletions
File tree
- google-auth-library-java/oauth2_http
- javatests/com/google/auth/oauth2
- java/com/google/auth/oauth2
- testresources/agent
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
0 commit comments