For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Google Cloud
Exchange the incoming token for a Google Cloud access token at the Google Security Token Service, with Workload Identity Federation.
Attaches to:
Exchange the token that a client sends to the gateway for a Google Cloud access token at the Google Security Token Service (STS), so that Google Cloud APIs authorize each request as the end user.
About
The Google Cloud Security Token Service (STS) implements the RFC 8693 token exchange grant for Workload Identity Federation. You register your identity provider (IdP) as a provider in a workload identity pool. The STS then accepts a JWT that your IdP issued as the subject_token, and returns a short-lived Google Cloud access token for the federated identity in the token’s sub claim.
In this guide, agentgateway performs that exchange for each request to a Google Cloud API. The client sends its usual IdP token, and the Google API receives a Google access token. Google Cloud authorizes the request as the end user’s federated identity, so IAM policies and Cloud Audit Logs reflect the user, not the gateway. No Google service account key is stored in the gateway or the client.
sequenceDiagram
participant Client
participant AGW as agentgateway
participant STS as Google STS
participant API as Google Cloud Storage
Client->>AGW: Request + IdP JWT
AGW->>STS: POST /v1/token<br/>subject_token=IdP JWT<br/>audience=//iam.googleapis.com/.../providers/PROVIDER
STS-->>AGW: Federated Google access token
AGW->>API: Request + Authorization: Bearer Google token
API-->>AGW: Response, authorized for the federated user
AGW-->>Client: Response
Google Cloud trusts the token because of the workload identity pool provider, not because of the caller. The STS token endpoint does not take client authentication, so the exchange has no client ID or client secret to store.
This exchange differs from Google Cloud backend authentication, where the gateway calls Google Cloud with its own identity, such as a service account. Use token exchange when Google Cloud must authorize each request as the user who sent it.
Google STS settings
The Google STS uses the standard RFC 8693 grant, so you configure it with the generic oauthTokenExchange method. The following settings are specific to Google.
| Field | Value for Google |
|---|---|
host, path | sts.googleapis.com:443 and /v1/token. |
policies.backendTLS | {}. A host port of 443 does not enable TLS by itself, so set backendTLS to originate TLS to the STS. |
grantType | tokenExchange, the default. |
subjectToken.tokenType | urn:ietf:params:oauth:token-type:jwt or urn:ietf:params:oauth:token-type:id_token for an OIDC provider. Google does not accept the default, urn:ietf:params:oauth:token-type:access_token, for an OIDC provider. |
requestedTokenType | urn:ietf:params:oauth:token-type:access_token. Required by Google. |
audiences | The full resource name of the workload identity pool provider, in the form //iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID. Note the leading // and the project number. |
scopes | Required by Google. Use https://www.googleapis.com/auth/cloud-platform, or a narrower OAuth scope for the API that you call. |
clientAuth | Omit. The STS does not authenticate the client. |
Before you begin
- Install the agentgateway binary.
- Install Docker to run the sample identity provider.
- Install the
gcloudCLI and log in to a Google Cloud project where you can create workload identity pools and Cloud Storage buckets, such as with theroles/iam.workloadIdentityPoolAdminandroles/storage.adminroles. - Install
jq.
Step 1: Run a sample identity provider
Run Keycloak as the identity provider that issues the incoming token. The realm in this example has a confidential client, agent, that adds the audience agentgateway to its tokens, and a user, testuser, with the password testpass.
Google requires the issuer to be an https:// URL. Keycloak in this example runs on plain HTTP on your machine, so the KC_HOSTNAME variable pins the issuer to https://keycloak.example.com. Google never contacts this host, because you upload the realm’s signing keys to Google instead. In production, use your own IdP and its real issuer URL.
Save the realm definition to a file.
cat > agentgateway-realm.json <<'EOF' { "realm": "agentgateway", "enabled": true, "clients": [ { "clientId": "agent", "secret": "agent-secret", "publicClient": false, "standardFlowEnabled": false, "directAccessGrantsEnabled": true, "protocolMappers": [ { "name": "google-audience", "protocol": "openid-connect", "protocolMapper": "oidc-audience-mapper", "config": { "included.custom.audience": "agentgateway", "access.token.claim": "true" } } ] } ], "users": [ { "username": "testuser", "enabled": true, "email": "[email protected]", "emailVerified": true, "firstName": "Test", "lastName": "User", "credentials": [{"type": "password", "value": "testpass", "temporary": false}] } ] } EOFRun Keycloak with the realm on port
7080.docker run -d --name keycloak -p 7080:8080 \ -v "$PWD/agentgateway-realm.json:/opt/keycloak/data/import/agentgateway-realm.json:ro" \ -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \ -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \ -e KC_HOSTNAME=https://keycloak.example.com \ quay.io/keycloak/keycloak:26.7.1 \ start-dev --import-realm --http-port=8080Wait for the realm to be served.
until curl -sf http://localhost:7080/realms/agentgateway/.well-known/openid-configuration > /dev/null; do sleep 2; doneMint a token for
testuserand save it in an environment variable. Keycloak tokens expire after 5 minutes, so mint a new one if you come back to this guide later.export INBOUND_TOKEN=$(curl -s http://localhost:7080/realms/agentgateway/protocol/openid-connect/token \ -u agent:agent-secret -d grant_type=password \ -d username=testuser -d password=testpass | jq -r .access_token)Decode the token’s payload and save its
subclaim. Google uses this claim as the identity of the federated user.echo "$INBOUND_TOKEN" | cut -d. -f2 | jq -R 'gsub("-";"+") | gsub("_";"/") | . + ("=" * ((4 - (length % 4)) % 4)) | @base64d | fromjson | {iss, aud, sub}' export USER_SUB=$(echo "$INBOUND_TOKEN" | cut -d. -f2 | jq -rR 'gsub("-";"+") | gsub("_";"/") | . + ("=" * ((4 - (length % 4)) % 4)) | @base64d | fromjson | .sub')Example output:
{ "iss": "https://keycloak.example.com/realms/agentgateway", "aud": "agentgateway", "sub": "c4bfb640-7fa6-4dcc-a742-190df95dc030" }Save the realm’s signing keys to a file, to upload to Google in the next step. The
jqfilter keeps the signing keys and drops the encryption key.curl -s http://localhost:7080/realms/agentgateway/protocol/openid-connect/certs \ | jq '{keys: [.keys[] | select(.use == "sig")]}' > jwks.jsonKeycloak in this example keeps its keys in memory. If you restart the container, it generates new keys, and you must upload the new
jwks.jsonfile to the provider withgcloud iam workload-identity-pools providers update-oidc.
Step 2: Set up Workload Identity Federation
Register Keycloak as an identity provider in a Google Cloud workload identity pool, and give its user access to a Cloud Storage bucket.
Set environment variables for your Google Cloud project and the resources that you create. Bucket names are globally unique, so the bucket name includes your project ID.
export PROJECT_ID=<your-project-id> export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)") export POOL_ID=agentgateway-pool export PROVIDER_ID=keycloak export BUCKET=agentgateway-te-$PROJECT_IDEnable the APIs that Workload Identity Federation and Cloud Storage use.
gcloud services enable iam.googleapis.com sts.googleapis.com storage.googleapis.com \ --project $PROJECT_IDCreate a workload identity pool.
gcloud iam workload-identity-pools create $POOL_ID \ --project $PROJECT_ID \ --location global \ --display-name "agentgateway token exchange"Create an OIDC provider in the pool for the Keycloak realm.
gcloud iam workload-identity-pools providers create-oidc $PROVIDER_ID \ --project $PROJECT_ID \ --location global \ --workload-identity-pool $POOL_ID \ --issuer-uri "https://keycloak.example.com/realms/agentgateway" \ --allowed-audiences agentgateway \ --attribute-mapping "google.subject=assertion.sub" \ --jwk-json-path jwks.jsonSetting Description --issuer-uriMust match the issclaim of the incoming token exactly. Google requires anhttps://issuer.--allowed-audiencesThe audvalues that Google accepts in the incoming token. The sample realm setsaudtoagentgateway. If you omit this flag, the token’saudmust be the full provider resource name,https://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID.--attribute-mappingMaps the token’s subclaim to the Google subject. IAM policies refer to the federated user by this value.--jwk-json-pathUploads the signing keys that you saved earlier, so Google validates tokens without contacting the issuer. For an IdP with a public https://discovery endpoint, omit this flag and Google fetches the keys itself.Create a bucket with a sample object.
gcloud storage buckets create gs://$BUCKET --project $PROJECT_ID --location us-central1 echo "hello from agentgateway" > hello.txt gcloud storage cp hello.txt gs://$BUCKET/hello.txtGrant the Keycloak user read access to the bucket. The member is the federated identity of
testuser: the workload identity pool plus thesubclaim that you saved earlier. Use the project number, not the project ID.gcloud storage buckets add-iam-policy-binding gs://$BUCKET \ --member "principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/subject/$USER_SUB" \ --role roles/storage.objectViewerIAM changes can take a few minutes to take effect.
Step 3: Configure token exchange
Create a configuration file. The route forwards requests to the Cloud Storage API, and the
oauthTokenExchangepolicy on the backend exchanges the incoming token at the Google STS first. The shell fills in your project number, pool ID, and provider ID.cat > config.yaml <<EOF # yaml-language-server: \$schema=https://agentgateway.dev/schema/config gateways: default: port: 3000 routes: - name: google-storage backends: - host: storage.googleapis.com:443 policies: backendTLS: {} backendAuth: oauthTokenExchange: host: sts.googleapis.com:443 path: /v1/token policies: backendTLS: {} subjectToken: tokenType: urn:ietf:params:oauth:token-type:jwt requestedTokenType: urn:ietf:params:oauth:token-type:access_token audiences: - //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID scopes: - https://www.googleapis.com/auth/cloud-platform EOFFor a description of each setting, see Google STS settings.
In a second terminal, change to the directory that has the
config.yamlfile, and run agentgateway with the configuration. Keep the first terminal for thecurlcommands, so that theINBOUND_TOKEN,USER_SUB, andBUCKETvariables stay available there.agentgateway -f config.yaml
Step 4: Verify the exchange
In the first terminal, mint a new token for
testuser. The token from Step 1 has likely expired, because Keycloak tokens expire after 5 minutes. Thesubclaim of the new token is the same, so the IAM binding from Step 2 still applies.export INBOUND_TOKEN=$(curl -s http://localhost:7080/realms/agentgateway/protocol/openid-connect/token \ -u agent:agent-secret -d grant_type=password \ -d username=testuser -d password=testpass | jq -r .access_token)List the objects in the bucket through the gateway, with the Keycloak token. The gateway exchanges the Keycloak token at the Google STS, and forwards the request to Cloud Storage with the Google access token.
curl -s http://localhost:3000/storage/v1/b/$BUCKET/o \ -H "authorization: Bearer $INBOUND_TOKEN"Cloud Storage authorizes the request as the federated
testuseridentity, and returns the objects in the bucket.{ "kind": "storage#objects", "items": [ { "kind": "storage#object", "name": "hello.txt", "bucket": "agentgateway-te-my-project", ... } ] }Send a request without a token. The gateway has no subject token to exchange, so it rejects the request with a
400response and does not call the STS.curl -s -w " %{http_code}\n" http://localhost:3000/storage/v1/b/$BUCKET/oExample output:
invalid request 400
The exchange does not validate the incoming token itself; Google does. To reject an invalid or expired token at the gateway before it calls the STS, add a JWT authentication policy to the route. Set preserveToken: true on that policy, so that the token is still in the request when the exchange runs.
Troubleshooting
When the exchange fails, the gateway returns 400 with the body invalid request. The response from the STS is logged at debug level. To see it, run agentgateway with the RUST_LOG=info,agentgateway::http::auth::oauth=debug environment variable, and look for oauth token exchange rejected by authorization server in the output.
| Error | Cause |
|---|---|
502 with Connection reset by peer | The gateway connects to the STS or to Cloud Storage over plain HTTP. Set policies.backendTLS: {} on both the oauthTokenExchange method and the backend. |
invalid_request: Invalid value for “audience”. | The audiences value is not the full resource name of a provider. It must start with //iam.googleapis.com/projects/, not https://. |
invalid_target: The target service indicated by the “audience” parameters is invalid. | The audiences value names a provider that does not exist or is disabled. Check the project number, pool ID, and provider ID. |
invalid_request: Invalid value of requested_token_type in request. | Set requestedTokenType to urn:ietf:params:oauth:token-type:access_token. |
invalid_request: Scope(s) must be provided. | Set at least one value in scopes. |
invalid_grant | Google rejected the incoming token. Check that the token’s iss matches the provider’s issuer URI, that its aud is in the provider’s allowed audiences, that the token is not expired, and that the uploaded keys are current. |
If the exchange succeeds but Cloud Storage returns 403, the federated identity has no access to the bucket. Check that the IAM binding uses the project number and the sub claim of the token, and allow a few minutes for IAM changes to take effect.
Cleanup
Stop agentgateway with
Ctrl+Cin its terminal, and remove the Keycloak container.docker rm -f keycloakDelete the Google Cloud resources.
gcloud storage rm --recursive gs://$BUCKET gcloud iam workload-identity-pools delete $POOL_ID --project $PROJECT_ID --location global --quietA deleted pool stays in a soft-deleted state for 30 days, and its ID cannot be reused during that time. To reuse the ID, restore the pool with
gcloud iam workload-identity-pools undelete.