Skip to content
agentgateway has joined the Agentic AI Foundation — Learn more

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

Page as Markdown

Exchange the incoming token for a Google Cloud access token at the Google Security Token Service, with Workload Identity Federation.

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.

FieldValue for Google
urlhttps://sts.googleapis.com/v1/token. Because the scheme is https, the gateway originates TLS to the STS.
grantTypeTokenExchange, the default.
subjectToken.tokenTypeJwt or IdToken for an OIDC provider. Google does not accept the default, AccessToken, for an OIDC provider.
requestedTokenTypeAccessToken. Required by Google.
audiencesThe 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.
scopesRequired by Google. Use https://www.googleapis.com/auth/cloud-platform, or a narrower OAuth scope for the API that you call.
clientAuthOmit. The STS does not authenticate the client.

Before you begin

  1. Follow the Get started guide to install agentgateway.

  2. Follow the Sample app guide to create a gateway proxy with an HTTP listener and deploy the httpbin sample app.

  3. Get the external address of the gateway and save it in an environment variable.

    Tip

    Kind cluster? Kind does not support LoadBalancer services by default. To use this option with a Kind cluster, install and run cloud-provider-kind.

    export INGRESS_GW_ADDRESS=$(kubectl get svc -n agentgateway-system agentgateway-proxy -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
    echo $INGRESS_GW_ADDRESS  

  1. Install the gcloud CLI and log in to a Google Cloud project where you can create workload identity pools and Cloud Storage buckets, such as with the roles/iam.workloadIdentityPoolAdmin and roles/storage.admin roles.

  2. Install jq.

Step 1: Deploy a sample identity provider

Deploy 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 inside the cluster, 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.

  1. Save the realm definition to a file and load it into a ConfigMap.

    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}]
        }
      ]
    }
    EOF
    
    kubectl create configmap keycloak-realm -n agentgateway-system \
      --from-file=agentgateway-realm.json
  2. Deploy Keycloak and its Service.

    kubectl apply -f- <<EOF
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: keycloak
      namespace: agentgateway-system
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: keycloak
      template:
        metadata:
          labels:
            app: keycloak
        spec:
          containers:
          - name: keycloak
            image: quay.io/keycloak/keycloak:26.7.1
            args: ["start-dev", "--import-realm", "--http-port=8080"]
            env:
            - name: KC_BOOTSTRAP_ADMIN_USERNAME
              value: admin
            - name: KC_BOOTSTRAP_ADMIN_PASSWORD
              value: admin
            - name: KC_HOSTNAME
              value: https://keycloak.example.com
            ports:
            - containerPort: 8080
            volumeMounts:
            - name: realm
              mountPath: /opt/keycloak/data/import
              readOnly: true
          volumes:
          - name: realm
            configMap:
              name: keycloak-realm
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: keycloak
      namespace: agentgateway-system
    spec:
      selector:
        app: keycloak
      ports:
      - name: http
        port: 8080
        targetPort: 8080
    EOF
  3. Wait for Keycloak to be ready.

    kubectl rollout status deployment/keycloak -n agentgateway-system --timeout=180s
  4. Port-forward the Keycloak Service so that you can reach it locally.

    kubectl port-forward -n agentgateway-system svc/keycloak 8081:8080
  5. In another terminal, mint a token for testuser and 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:8081/realms/agentgateway/protocol/openid-connect/token \
      -u agent:agent-secret -d grant_type=password \
      -d username=testuser -d password=testpass | jq -r .access_token)
  6. Decode the token’s payload and save its sub claim. 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": "c44ff2c5-1833-4e88-ad28-780f41d48e47"
    }
  7. Save the realm’s signing keys to a file, to upload to Google in the next step. The jq filter keeps the signing keys and drops the encryption key.

    curl -s http://localhost:8081/realms/agentgateway/protocol/openid-connect/certs \
      | jq '{keys: [.keys[] | select(.use == "sig")]}' > jwks.json

    Keycloak in this example keeps its keys in memory. If the Keycloak pod restarts, it generates new keys, and you must upload the new jwks.json file to the provider with gcloud 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.

  1. 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_ID
  2. Enable the APIs that Workload Identity Federation and Cloud Storage use.

    gcloud services enable iam.googleapis.com sts.googleapis.com storage.googleapis.com \
      --project $PROJECT_ID
  3. Create a workload identity pool.

    gcloud iam workload-identity-pools create $POOL_ID \
      --project $PROJECT_ID \
      --location global \
      --display-name "agentgateway token exchange"
  4. 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.json
    SettingDescription
    --issuer-uriMust match the iss claim of the incoming token exactly. Google requires an https:// issuer.
    --allowed-audiencesThe aud values that Google accepts in the incoming token. The sample realm sets aud to agentgateway. If you omit this flag, the token’s aud must 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 sub claim 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.
  5. 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.txt
  6. Grant the Keycloak user read access to the bucket. The member is the federated identity of testuser: the workload identity pool plus the sub claim 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.objectViewer

    IAM changes can take a few minutes to take effect.

Step 3: Route to Google Cloud Storage

  1. Create an AgentgatewayBackend for the Cloud Storage API. The tls policy originates TLS to the backend.

    kubectl apply -f- <<EOF
    apiVersion: agentgateway.dev/v1alpha1
    kind: AgentgatewayBackend
    metadata:
      name: google-storage
      namespace: agentgateway-system
    spec:
      static:
        host: storage.googleapis.com
        port: 443
      policies:
        tls:
          sni: storage.googleapis.com
    EOF
  2. Create an HTTPRoute that sends requests for the storage.example.com host to the backend, and rewrites the host to storage.googleapis.com.

    kubectl apply -f- <<EOF
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: google-storage
      namespace: agentgateway-system
    spec:
      parentRefs:
      - name: agentgateway-proxy
        namespace: agentgateway-system
      hostnames:
      - storage.example.com
      rules:
      - backendRefs:
        - group: agentgateway.dev
          kind: AgentgatewayBackend
          name: google-storage
        filters:
        - type: URLRewrite
          urlRewrite:
            hostname: storage.googleapis.com
    EOF
  3. List the objects in the bucket through the gateway, with the Keycloak token. Without a token exchange, the gateway forwards the Keycloak token unchanged, and Cloud Storage rejects it with a 401 response, because it is not a Google credential.

    curl -s http://$INGRESS_GW_ADDRESS/storage/v1/b/$BUCKET/o \
      -H "host: storage.example.com" \
      -H "authorization: Bearer $INBOUND_TOKEN"

    Example output:

    {
      "error": {
        "code": 401,
        "message": "Invalid Credentials",
        "errors": [
          {
            "message": "Invalid Credentials",
            "domain": "global",
            "reason": "authError",
            "locationType": "header",
            "location": "Authorization"
          }
        ]
      }
    }

Step 4: Configure token exchange

  1. Create an AgentgatewayPolicy that exchanges the incoming token at the Google STS for every request to the Cloud Storage backend.

    kubectl apply -f- <<EOF
    apiVersion: agentgateway.dev/v1alpha1
    kind: AgentgatewayPolicy
    metadata:
      name: google-token-exchange
      namespace: agentgateway-system
    spec:
      targetRefs:
      - group: agentgateway.dev
        kind: AgentgatewayBackend
        name: google-storage
      backend:
        auth:
          oauthTokenExchange:
            url: https://sts.googleapis.com/v1/token
            grantType: TokenExchange
            subjectToken:
              tokenType: Jwt
            requestedTokenType: AccessToken
            audiences:
            - //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID
            scopes:
            - https://www.googleapis.com/auth/cloud-platform
    EOF

    For a description of each setting, see Google STS settings.

  2. Confirm that the policy is accepted and attached.

    kubectl get AgentgatewayPolicy google-token-exchange -n agentgateway-system \
      -o jsonpath='{range .status.ancestors[0].conditions[*]}{.type}={.status}{"\n"}{end}'

    Example output:

    Accepted=True
    Attached=True

Step 5: Verify the exchange

  1. Mint a new token for testuser. The token from Step 1 has likely expired, because Keycloak tokens expire after 5 minutes. The sub claim of the new token is the same, so the IAM binding from Step 2 still applies. The Keycloak port-forward from Step 1 must still be running.

    export INBOUND_TOKEN=$(curl -s http://localhost:8081/realms/agentgateway/protocol/openid-connect/token \
      -u agent:agent-secret -d grant_type=password \
      -d username=testuser -d password=testpass | jq -r .access_token)
  2. Send the same request again. 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://$INGRESS_GW_ADDRESS/storage/v1/b/$BUCKET/o \
      -H "host: storage.example.com" \
      -H "authorization: Bearer $INBOUND_TOKEN"

    Cloud Storage authorizes the request as the federated testuser identity, and returns the objects in the bucket.

    {
      "kind": "storage#objects",
      "items": [
        {
          "kind": "storage#object",
          "name": "hello.txt",
          "bucket": "agentgateway-te-my-project",
          ...
        }
      ]
    }
  3. Send a request without a token. The gateway has no subject token to exchange, so it rejects the request with a 400 response and does not call the STS.

    curl -s -w " %{http_code}\n" http://$INGRESS_GW_ADDRESS/storage/v1/b/$BUCKET/o \
      -H "host: storage.example.com"

    Example 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, as described in Validate the incoming token at the edge. 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, raise the log level of the token exchange module on the proxy, send the request again, and check the proxy logs.

kubectl port-forward deployment/agentgateway-proxy -n agentgateway-system 15000 &
curl -X POST "http://localhost:15000/logging?level=info,agentgateway::http::auth::oauth=debug"
kubectl logs deployment/agentgateway-proxy -n agentgateway-system | grep "oauth token exchange"

The change lasts until the proxy restarts. Look for a message such as oauth token exchange rejected by authorization server, which includes the error from the STS.

Error from the STSCause
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 AccessToken.
invalid_request: Scope(s) must be provided.Set at least one value in scopes.
invalid_grantGoogle 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

You can remove the resources that you created in this guide.
  1. Delete the Kubernetes resources.

    kubectl delete AgentgatewayPolicy google-token-exchange -n agentgateway-system
    kubectl delete httproute google-storage -n agentgateway-system
    kubectl delete AgentgatewayBackend google-storage -n agentgateway-system
    kubectl delete deployment keycloak -n agentgateway-system
    kubectl delete service keycloak -n agentgateway-system
    kubectl delete configmap keycloak-realm -n agentgateway-system
  2. Stop the port-forwards. Press Ctrl+C in the terminal that runs the Keycloak port-forward from Step 1. If you ran the admin port-forward in the troubleshooting section, stop it too.

    pkill -f "port-forward deployment/agentgateway-proxy" || true
  3. Delete the Google Cloud resources.

    gcloud storage rm --recursive gs://$BUCKET
    gcloud iam workload-identity-pools delete $POOL_ID --project $PROJECT_ID --location global --quiet

    A 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.

Was this page helpful?
Agentgateway assistant

Ask me anything about agentgateway configuration, features, or usage.

Note: AI-generated content might contain errors; please verify and test all returned information.

Tip: one topic per conversation gives the best results. Use the + button in the chat header to start a new conversation.

Switching topics? Starting a new conversation improves accuracy.
↑↓ navigate ↵ select esc dismiss

What could be improved?

Your feedback helps us improve assistant answers and identify docs gaps we should fix.

Need more help? Join us on Discord: https://discord.gg/y9efgEmppm

Want to use your own agent? Add the Solo MCP server to query our docs directly. Get started here: https://search.solo.io/.