Skip to content
Help guide the future of agentgateway — Take the survey

For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.

Keycloak

Page as Markdown

Keep user context in requests with Keycloak standard token exchange.

Attaches to:

Exchange the user’s Keycloak access token that a client sends to the gateway for a new access token at Keycloak, so that the backend receives a token that is issued for its own audience.

About

Keycloak implements the RFC 8693 token exchange grant as standard token exchange, which is supported in Keycloak 26.2 and later and is turned on per client. The gateway is a confidential client of the realm. It sends the user’s access token as the subject_token, and Keycloak returns a new access token for the same user (sub), issued for the backend’s audience, with the gateway’s client as the authorized party (azp).

    sequenceDiagram
    participant Client as Client (agent)
    participant AGW as agentgateway
    participant KC as Keycloak
    participant API as Backend

    Client->>AGW: Request + user's access token<br/>aud includes agentgateway
    AGW->>KC: POST /realms/REALM/protocol/openid-connect/token<br/>subject_token=user's token, audience=backend<br/>client ID and secret of agentgateway
    KC-->>AGW: Access token<br/>sub=user, aud=backend, azp=agentgateway
    AGW->>API: Request + Authorization: Bearer exchanged token
    API-->>AGW: Response
    AGW-->>Client: Response
  

The guide uses three Keycloak clients.

  • agent is the caller. It signs in the user and gets the user’s token, called the incoming token. In the test realm, you play this client yourself and use curl to get the token.
  • agentgateway is the gateway. It sends the incoming token to Keycloak and gets the exchanged token back.
  • backend represents the backend API. Keycloak issues the exchanged token for this audience, and the gateway sends the token to the backend.

Keycloak checks two audiences during the exchange.

  • The incoming token must list agentgateway in its aud claim.
  • agentgateway must be allowed to get backend in its own tokens.

The standard token exchange guide runs this exchange end to end against a sample Keycloak realm that comes preconfigured. This guide shows the realm settings that make the exchange work, so that you can set up your own realm.

Keycloak settings

Keycloak uses the standard RFC 8693 grant, so you configure it with the generic oauthTokenExchange method. The following settings are specific to Keycloak.

FieldValue for Keycloak
host, pathThe host and port of Keycloak, and /realms/REALM/protocol/openid-connect/token. Keycloak checks the iss claim of the incoming token against this URL, so the gateway must reach Keycloak through the same hostname that issued the token.
policies.backendTLS{} if Keycloak serves HTTPS. A host port of 443 does not enable TLS by itself.
grantTypetokenExchange, the default.
subjectToken.tokenTypeurn:ietf:params:oauth:token-type:access_token, the default. Keycloak accepts only access tokens as the subject token.
requestedTokenTypeOmit, or set urn:ietf:params:oauth:token-type:access_token. Keycloak also issues an ID token, but rejects the JWT type.
audiencesThe client IDs of the backends that the exchanged token is for, such as backend. Each audience must be available to the gateway’s client. If you omit this field, the exchanged token has every audience that is available to the gateway’s client.
scopesOptional. Default or optional client scopes of the gateway’s client.
clientAuthRequired. The client ID and client secret of the gateway’s confidential client. Set method to clientSecretBasic, the default, or clientSecretPost.

Before you begin

  1. Install the agentgateway binary.

  2. Get admin access to a Keycloak server, version 26.2 or later. To run one locally for this guide, start Keycloak in development mode on port 7080.

    docker run -d --name keycloak -p 7080:8080 \
      -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
      -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
      quay.io/keycloak/keycloak:26.7.1 start-dev
  3. Install jq.

  4. Download the Keycloak distribution and extract its bin directory, which has the kcadm.sh admin CLI. Then, add the directory to your PATH. The admin CLI requires Java 17 or later.

    export KEYCLOAK_VERSION=26.7.1
    curl -sL https://github.com/keycloak/keycloak/releases/download/$KEYCLOAK_VERSION/keycloak-$KEYCLOAK_VERSION.tar.gz \
      | tar xz keycloak-$KEYCLOAK_VERSION/bin
    export PATH="$PWD/keycloak-$KEYCLOAK_VERSION/bin:$PATH"

Step 1: Configure the realm

Standard token exchange needs a few settings in the realm: a confidential client for the gateway with token exchange turned on, the gateway in the audience of the user’s token, the backend’s audience available to the gateway, and a client scope to down-scope the exchanged token. The following steps use the kcadm.sh admin CLI, which is in the bin directory of the Keycloak distribution. Each step also names the matching setting in the Admin Console.

  1. Save the URL of your Keycloak server and the name of your realm in environment variables, and log in to the admin CLI.

    export KEYCLOAK_URL=http://localhost:7080
    export REALM=agentgateway
    kcadm.sh config credentials --server $KEYCLOAK_URL --realm master --user admin

    When prompted, enter the admin password, such as admin.

  2. Optional: If you do not have a realm to try the exchange in, create a test realm, and set up the required clients and user. The realm has a client that issues user tokens, agent, with direct access grants so that you can get a token with curl, a client that represents the backend API, backend, and a user, testuser. If you use your own realm, replace agent, backend, and the user in the following steps.

    kcadm.sh create realms -s realm=$REALM -s enabled=true
    kcadm.sh create clients -r $REALM -s clientId=agent -s secret=agent-secret \
      -s publicClient=false -s standardFlowEnabled=false -s directAccessGrantsEnabled=true
    kcadm.sh create clients -r $REALM -s clientId=backend -s publicClient=false -s standardFlowEnabled=false
    kcadm.sh create users -r $REALM -s username=testuser -s enabled=true \
      -s email=[email protected] -s emailVerified=true -s firstName=Test -s lastName=User
    kcadm.sh set-password -r $REALM --username testuser --new-password testpass
  3. Create a confidential client for the gateway, agentgateway, with standard token exchange turned on. In the Admin Console, turn on Client authentication and select Standard Token Exchange in the Capability config of the client. The gateway does not sign in users, so the standard flow is off.

    kcadm.sh create clients -r $REALM -s clientId=agentgateway \
      -s publicClient=false -s standardFlowEnabled=false \
      -s 'attributes."standard.token.exchange.enabled"=true'
    export GATEWAY_ID=$(kcadm.sh get clients -r $REALM -q clientId=agentgateway --fields id --format csv --noquotes)
    export GATEWAY_CLIENT_SECRET=$(kcadm.sh get clients/$GATEWAY_ID/client-secret -r $REALM --fields value --format csv --noquotes)
  4. Make the user’s token name the gateway as a recipient. Keycloak exchanges a token only if the client that asks for the exchange, agentgateway, is in the token’s aud claim. Create a client scope with an audience mapper for agentgateway, and make it a default client scope of the agent client, which issues the user’s token. In the Admin Console, the mapper is an Audience mapper with Included Client Audience set to agentgateway. Tokens from agent now include agentgateway in aud.

    SCOPE_ID=$(kcadm.sh create client-scopes -r $REALM -s name=agentgateway-audience \
      -s protocol=openid-connect -s 'attributes."include.in.token.scope"=false' -i)
    kcadm.sh create client-scopes/$SCOPE_ID/protocol-mappers/models -r $REALM \
      -s name=agentgateway-audience -s protocol=openid-connect -s protocolMapper=oidc-audience-mapper \
      -s 'config."included.client.audience"=agentgateway' -s 'config."access.token.claim"=true'
    AGENT_ID=$(kcadm.sh get clients -r $REALM -q clientId=agent --fields id --format csv --noquotes)
    kcadm.sh update clients/$AGENT_ID/default-client-scopes/$SCOPE_ID -r $REALM
  5. Allow the gateway to request a token for the backend. Keycloak issues the exchanged token only for an audience that the gateway’s client can get in its own tokens. Create a client scope with an audience mapper for backend, and make it a default client scope of the agentgateway client. This step is the counterpart to step 4, which covers the incoming token.

    SCOPE_ID=$(kcadm.sh create client-scopes -r $REALM -s name=backend-audience \
      -s protocol=openid-connect -s 'attributes."include.in.token.scope"=false' -i)
    kcadm.sh create client-scopes/$SCOPE_ID/protocol-mappers/models -r $REALM \
      -s name=backend-audience -s protocol=openid-connect -s protocolMapper=oidc-audience-mapper \
      -s 'config."included.client.audience"=backend' -s 'config."access.token.claim"=true'
    kcadm.sh update clients/$GATEWAY_ID/default-client-scopes/$SCOPE_ID -r $REALM
  6. Create a client scope that the gateway can request in the exchange, such as backend.read, and make it an optional client scope of the agentgateway client. When the gateway sends the scope, Keycloak adds it to the scope claim of the exchanged token. The scope limits what the exchanged token allows, so the backend can check the scope claim and grant the request only read access. The gateway configuration in this guide requests backend.read.

    SCOPE_ID=$(kcadm.sh create client-scopes -r $REALM -s name=backend.read -s protocol=openid-connect -i)
    kcadm.sh update clients/$GATEWAY_ID/optional-client-scopes/$SCOPE_ID -r $REALM
  7. Get an access token for the test user from the agent client, and save it in an environment variable. You use this token as the incoming token that a real client would send to the gateway. Keycloak access tokens expire after 5 minutes by default, so get a new one if you come back to this guide later.

    export INBOUND_TOKEN=$(curl -s $KEYCLOAK_URL/realms/$REALM/protocol/openid-connect/token \
      -u agent:agent-secret -d grant_type=password \
      -d username=testuser -d password=testpass | jq -r .access_token)
  8. Exchange the token at Keycloak as the gateway, and decode the result. This request is the same one that agentgateway sends, so it confirms the realm settings before you configure the gateway.

    curl -s $KEYCLOAK_URL/realms/$REALM/protocol/openid-connect/token \
      -u agentgateway:$GATEWAY_CLIENT_SECRET \
      -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
      -d subject_token_type=urn:ietf:params:oauth:token-type:access_token \
      -d subject_token=$INBOUND_TOKEN \
      -d audience=backend -d scope=backend.read \
      | jq -r .access_token | cut -d. -f2 \
      | jq -R 'gsub("-";"+") | gsub("_";"/") | . + ("=" * ((4 - (length % 4)) % 4)) | @base64d | fromjson | {aud, azp, sub, scope}'

    Example output:

    {
      "aud": "backend",
      "azp": "agentgateway",
      "sub": "d7386852-9a23-44b2-b8ea-a2d81f73a014",
      "scope": "backend.read profile email"
    }

    The output confirms that the realm is ready for the gateway. The aud claim is backend, the azp claim is agentgateway, and the sub claim is the same user as in the incoming token. The scope claim includes backend.read, plus the default client scopes of the agentgateway client. If the exchange fails, Keycloak returns an error instead, such as Client is not within the token audience. For more errors, see Troubleshooting.

Step 2: Configure token exchange

  1. Run the httpbin sample app on port 8000. The app returns the headers of each request, so you can see the token that the gateway forwards.

    docker run -d --name httpbin -p 8000:8080 mccutchen/go-httpbin
  2. Create a configuration file. The route forwards requests to httpbin, and the oauthTokenExchange policy on the backend exchanges the incoming token at Keycloak first. The shell fills in your realm and client secret. If your Keycloak is not at localhost:7080, change host, and add backendTLS if it serves HTTPS.

    cat > config.yaml <<EOF
    # yaml-language-server: \$schema=https://agentgateway.dev/schema/config
    gateways:
      default:
        port: 3000
    routes:
    - name: keycloak-backend
      backends:
      - host: localhost:8000
        policies:
          backendAuth:
            oauthTokenExchange:
              host: localhost:7080
              path: /realms/$REALM/protocol/openid-connect/token
              audiences:
              - backend
              scopes:
              - backend.read
              clientAuth:
                method: clientSecretBasic
                clientId: agentgateway
                clientSecret: $GATEWAY_CLIENT_SECRET
    EOF

    For a description of each setting, see Keycloak settings.

  3. In a second terminal, change to the directory that has the config.yaml file, and run agentgateway with the configuration. Keep the first terminal for the curl commands, so that the INBOUND_TOKEN variable stays available there.

    agentgateway -f config.yaml

Step 3: Verify the exchange

In the first terminal, send a request to the httpbin /headers endpoint through the gateway, with the user’s token, and decode the token that httpbin received. If more than 5 minutes passed since you saved the token in the $INBOUND_TOKEN environment variable, get a new token by repeating step 7 of the realm setup.

curl -s http://localhost:3000/headers \
  -H "authorization: Bearer $INBOUND_TOKEN" \
  | jq -r '.headers.Authorization[0] | sub("^Bearer ";"")' \
  | cut -d. -f2 \
  | jq -R 'gsub("-";"+") | gsub("_";"/") | . + ("=" * ((4 - (length % 4)) % 4)) | @base64d | fromjson | {aud, azp, sub, scope}'

The backend received the exchanged token, for the backend audience with agentgateway as the authorized party, not the token that you sent.

{
  "aud": "backend",
  "azp": "agentgateway",
  "sub": "d7386852-9a23-44b2-b8ea-a2d81f73a014",
  "scope": "backend.read profile email"
}

Next steps

The exchange does not validate the incoming token itself; Keycloak does. To reject an invalid or expired token at the gateway before it calls Keycloak, 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 Keycloak rejects the exchange, the gateway returns 400 with the body invalid request. The response from Keycloak 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 or oauth token exchange rejected gateway credentials in the output.

Error from KeycloakCause
access_denied: Client is not within the token audienceThe aud claim of the incoming token does not include the gateway’s client. Add an audience mapper for agentgateway to the client that issues the token. Keycloak returns 403, so the gateway returns 500 with backend authentication failed: token exchange returned status 403 Forbidden.
invalid_request: Standard token exchange is not enabled for the requested clientThe client in clientAuth does not have Standard Token Exchange turned on, or clientAuth names a different client than you configured.
invalid_request: Requested audience not available: <client>The gateway’s client cannot get the requested audience in its own tokens. Add an audience mapper for the backend client to a default client scope of the gateway’s client.
invalid_client: Audience not foundA value in audiences is not a client ID in the realm.
invalid_scope: Invalid scopes: <scope>A value in scopes is not a default or optional client scope of the gateway’s client.
invalid_request: Invalid tokenKeycloak rejected the incoming token: it expired, comes from another realm, or its iss claim does not match the issuer of the token endpoint as the gateway reaches it. The Keycloak log shows subject_token validation failure.
invalid_request: Parameter ‘subject_token’ supports access tokens onlysubjectToken.tokenType is set to a JWT or ID token type. Remove the field to send the default, the access token type.
invalid_request: requested_token_type unsupportedrequestedTokenType is set to the JWT type. Remove the field, or request an access token or an ID token.
unauthorized_client: Invalid client or Invalid client credentialsThe client secret does not match the gateway’s client. Keycloak returns 401, so the gateway returns 500 with backend authentication failed.

Cleanup

  1. Stop agentgateway with Ctrl+C in its terminal, and remove the httpbin container.

    docker rm -f httpbin
  2. Delete the Keycloak resources. If you created the test realm, delete the realm. Otherwise, delete the agentgateway client and the client scopes that you created. If you started the local Keycloak container, remove it instead.

    kcadm.sh delete realms/$REALM
    docker rm -f keycloak
  3. Optional: Remove the Keycloak distribution that you downloaded.

    rm -rf keycloak-$KEYCLOAK_VERSION
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/.