The federated token carries no roles. The runner uses it to ask IAM
Credentials for an access token as the service account, and
IAM agrees only if a binding grants this repository
roles/iam.workloadIdentityUser on it: the authorization
hinge of the whole pattern.
Call sequence
curl -X POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/ci-deployer@authlab-lab.iam.gserviceaccount.com:generateAccessToken \ -H 'Authorization: Bearer ya29.c.REDACTED-federated-access-token-not-valid-for-use' \ -H 'Content-Type: application/json' \ -d '{ "scope": ["https://www.googleapis.com/auth/cloud-platform"], "lifetime": "3600s" }'
{ "accessToken": "ya29.c.SYNTHETIC-sa-access-token-not-valid-for-use", "expireTime": "2026-05-12T21:46:34Z" }
What IAM checks
The binding IAM evaluates
principalSet://iam.googleapis.com/ projects/1234567890/ locations/global/ workloadIdentityPools/lab-pool/ attribute.repository/authlab/artifacts-visualizer
≡ token
repository: the binding pins this repo (any ref)
$ gcloud iam service-accounts add-iam-policy-binding \ ci-deployer@authlab-lab.iam.gserviceaccount.com \ --role="roles/iam.workloadIdentityUser" \ --member="principalSet://iam.googleapis.com/projects/1234567890/locations/global/workloadIdentityPools/lab-pool/attribute.repository/authlab/artifacts-visualizer"
run once, by an operator — not part of the runtime flow
No binding that names this repository → 403. Step 2 proved who; this binding decides what they may impersonate.