For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
JWT auth
Set up JWT authentication with an identity provider like Keycloak.
Secure your applications with JSON Web Token (JWT) authentication by using the agentgateway proxy and an identity provider like Keycloak. To learn more about JWT auth, see About JWT authentication.
Before you begin
- Set up an agentgateway proxy.
- Install the httpbin sample app.
Install Keycloak
You might want to test how to restrict access to your applications to authenticated users, such as with external auth or JWT policies. You can install Keycloak in your cluster as an OpenID Connect (OIDC) provider.
The following steps install Keycloak in your cluster and configure a users group with two members.
- Username:
user1, password:password, email:[email protected] - Username:
user2, password:password, email:[email protected]
Warning
This example uses default credentials and removes Keycloak policies that restrict anonymous dynamic client registration (DCR). Use the example only in a local test environment.
You can keep DCR enabled in production. Restrict redirect hosts, client templates, scopes, protocol mappers, full-scope access, and client limits. Require user consent, and prevent DCR clients from using service accounts or the client credentials grant.
Install and configure Keycloak:
- Create a namespace for your Keycloak deployment.
kubectl create namespace keycloak - Create the Keycloak deployment and service. The service is of type
LoadBalancerso that you can reach Keycloak from outside the cluster.kubectl apply -f- <<EOF apiVersion: v1 kind: Service metadata: name: keycloak namespace: keycloak labels: app: keycloak spec: ports: - name: http port: 8080 targetPort: 8080 selector: app: keycloak type: LoadBalancer --- apiVersion: apps/v1 kind: Deployment metadata: name: keycloak namespace: keycloak labels: app: keycloak 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"] env: - name: KC_BOOTSTRAP_ADMIN_USERNAME value: "admin" - name: KC_BOOTSTRAP_ADMIN_PASSWORD value: "admin" ports: - name: http containerPort: 8080 readinessProbe: httpGet: path: /realms/master port: 8080 EOF - Wait for the Keycloak rollout to finish.
kubectl -n keycloak rollout status deploy/keycloak
Set the Keycloak endpoint details from the load balancer service. If you are running locally in kind and need a local IP address for the load balancer service, consider using
cloud-provider-kind.export ENDPOINT_KEYCLOAK=$(kubectl -n keycloak get service keycloak -o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}'):8080 export HOST_KEYCLOAK=$(echo ${ENDPOINT_KEYCLOAK} | cut -d: -f1) export PORT_KEYCLOAK=$(echo ${ENDPOINT_KEYCLOAK} | cut -d: -f2) export KEYCLOAK_URL=http://${ENDPOINT_KEYCLOAK} echo $KEYCLOAK_URLSet the Keycloak admin token. If you see a parsing error, try running the
curlcommand by itself. You might notice that your internet provider or network rules are blocking the requests. If so, you can update your security settings or change the network so that the request can be processed.export KEYCLOAK_TOKEN=$(curl --fail --silent --show-error \ -d client_id=admin-cli \ -d username=admin \ -d password=admin \ -d grant_type=password \ "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \ | jq -r .access_token)Use the administrator token to configure Keycloak for MCP authentication. If this command returns
401 Unauthorized, refresh the token in the previous step.# Use the exact public MCP server URL that clients connect to export MCP_RESOURCE=${MCP_RESOURCE:-http://localhost:8080/mcp} # Create a realm-default scope with audience and group mappers curl --fail --silent --show-error \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg audience "$MCP_RESOURCE" '{ name: "mcp", protocol: "openid-connect", protocolMappers: [ { name: "mcp-audience", protocol: "openid-connect", protocolMapper: "oidc-audience-mapper", config: { "included.custom.audience": $audience, "access.token.claim": "true" } }, { name: "groups", protocol: "openid-connect", protocolMapper: "oidc-group-membership-mapper", config: { "claim.name": "groups", "full.path": "false", "access.token.claim": "true" } } ] }')" \ "$KEYCLOAK_URL/admin/realms/master/client-scopes" export KEYCLOAK_SCOPE_ID=$(curl --fail --silent --show-error \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "$KEYCLOAK_URL/admin/realms/master/client-scopes" \ | jq -r '.[] | select(.name == "mcp") | .id') # Add the scope to all current and future clients curl --fail --silent --show-error -X PUT \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "$KEYCLOAK_URL/admin/realms/master/default-default-client-scopes/$KEYCLOAK_SCOPE_ID" # Create a group for users who can access the MCP server curl --fail --silent --show-error \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"name":"users"}' \ "$KEYCLOAK_URL/admin/realms/master/groups" export KEYCLOAK_GROUP_ID=$(curl --fail --silent --show-error \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "$KEYCLOAK_URL/admin/realms/master/groups?search=users&exact=true" \ | jq -r '.[] | select(.name == "users") | .id') # Create first user curl --fail --silent --show-error \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"username":"user1","email":"[email protected]","firstName":"Alice","lastName":"Doe","enabled":true,"credentials":[{"type":"password","value":"password","temporary":false}]}' \ "$KEYCLOAK_URL/admin/realms/master/users" # Create second user curl --fail --silent --show-error \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"username":"user2","email":"[email protected]","firstName":"Bob","lastName":"Doe","enabled":true,"credentials":[{"type":"password","value":"password","temporary":false}]}' \ "$KEYCLOAK_URL/admin/realms/master/users" # Add both users to the group for username in user1 user2; do user_id=$(curl --fail --silent --show-error \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "$KEYCLOAK_URL/admin/realms/master/users?username=$username&exact=true" \ | jq -r '.[0].id') curl --fail --silent --show-error -X PUT \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "$KEYCLOAK_URL/admin/realms/master/users/$user_id/groups/$KEYCLOAK_GROUP_ID" done # Relax anonymous DCR policies for this local test only registration_policies=$(curl --fail --silent --show-error \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "$KEYCLOAK_URL/admin/realms/master/components?type=org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy") for policy_id in $(jq -r '.[] | select( (.providerId == "trusted-hosts") or (.providerId == "allowed-client-templates" and .subType == "anonymous") ) | .id' <<<"$registration_policies"); do curl --fail --silent --show-error -X DELETE \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "$KEYCLOAK_URL/admin/realms/master/components/$policy_id" done
Open the Keycloak frontend.
open $KEYCLOAK_URLLog in to the admin console, and enter
adminas the username andadminas your password.In the Keycloak admin console, go to Users, and verify that the users that you created are displayed. You might need to click View all users.
Go to Groups, select users, and verify that both users are listed on the Members tab.
Retrieve JWKS path and issuer URL
You might integrate OIDC with your apps. In such cases, you might need particular details from the OIDC provider to fully set up your apps. To use Keycloak for OAuth protection of these apps, you need certain settings and information from Keycloak.
The following instructions assume that you are still logged into the Administration Console from the previous step.
Confirm that you have the following environmental variables set. If not, refer to Step 1: Install Keycloak section.
echo $KEYCLOAK_URLGet the issuer and JWKS path. The agentgateway proxy uses these values to validate the JWTs.
- From the sidebar menu options, click Realm Settings.
- From the General tab, scroll down to the Endpoints section and open the OpenID Endpoint Configuration link. In a new tab, your browser opens to a URL similar to
http://$KEYCLOAK_URL:8080/realms/master/.well-known/openid-configuration. - In the OpenID configuration, search for the
issuerfield. Save the value as an environment variable, such as the following example.export KEYCLOAK_ISSUER=$KEYCLOAK_URL/realms/master - In the OpenID configuration, search for the
jwks_urifield, and copy the path without the Keycloak URL that you retrieved earlier. For example, the path might be set to/realms/master/protocol/openid-connect/certs.export KEYCLOAK_JWKS_PATH=/realms/master/protocol/openid-connect/certs
Set up JWT authentication
Configure an AgentgatewayPolicy to validate JWTs using a remote JWKS endpoint from Keycloak. This approach is recommended for production as it supports automatic key rotation.
Create an AgentgatewayPolicy with JWT authentication configuration.
Review the following table to understand this configuration.kubectl apply -f - <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayPolicy metadata: name: jwt-auth-policy namespace: agentgateway-system spec: # Target the Gateway to apply JWT authentication to all routes targetRefs: - group: gateway.networking.k8s.io kind: Gateway name: agentgateway-proxy # Configure JWT authentication traffic: jwtAuthentication: # Validation mode - determines how strictly JWTs are validated mode: Strict # List of JWT providers (identity providers) providers: - # Issuer URL - must match the 'iss' claim in JWT tokens issuer: "${KEYCLOAK_ISSUER}" # JWKS configuration for remote key fetching jwks: remote: # Path to the JWKS endpoint, relative to the backend root jwksPath: "${KEYCLOAK_JWKS_PATH}" # Cache duration for JWKS keys (reduces load on identity provider) cacheDuration: "5m" # Reference to the Keycloak service backendRef: group: "" kind: Service name: keycloak namespace: keycloak port: 8080 EOFField Description modeValidation mode for JWT authentication. Strictrequires a valid JWT for all requests.Optionalvalidates JWTs if present but allows requests without tokens.Permissiveis the least strict mode.
Example value:StrictissuerThe issuer URL that must match the issclaim in JWT tokens exactly. Agentgateway rejects tokens from other issuers.
Example value:http://keycloak:8080/realms/masteraudiencesList of allowed audience values. The JWT’s audclaim must contain at least one of these values. Omit the field to accept any audience.
Example value:["my-application"]jwks.remote.jwksPathThe path to the JWKS endpoint on the identity provider, relative to the backend root. This endpoint returns the public keys used to verify JWT signatures.
Example value:/realms/master/protocol/openid-connect/certsjwks.remote.cacheDurationHow long to cache the JWKS keys locally. This setting reduces load on the identity provider and improves performance. Keys are automatically refreshed when the cache expires.
Example value:5m(5 minutes)jwks.remote.backendRefReference to the backend that hosts the identity provider. Agentgateway uses this value to fetch the JWKS keys from the identity provider. For an in-cluster provider, reference a Kubernetes Service. For an external provider that is reached over TLS, reference an AgentgatewayBackend instead. See External identity provider over TLS.
Example value: The details of the Keycloak serviceView the details of the policy. Verify that the policy is accepted.
kubectl get AgentgatewayPolicy jwt-auth-policy -n agentgateway-system -o json | jq '.status'
Verify JWT authentication
Now that JWT authentication is configured, test the setup by obtaining a token from Keycloak and making authenticated requests.
Send a request to the httpbin app without any JWT token. Verify that the request fails with a 401 HTTP response code.
curl -v "${INGRESS_GW_ADDRESS}:80/headers" -H "host: www.example.com"Example output:
HTTP/1.1 401 Unauthorized content-type: text/plain response-gateway: response path /headers content-length: 45 date: Mon, 19 Jan 2026 16:07:12 GMT authentication failure: no bearer token found%Register a client with Keycloak. The client uses dynamic client registration (DCR), so no administrator creates it, and Keycloak returns the client ID and secret that the next step uses.
Warning
The Keycloak instance in this guide allows anonymous DCR, which is why you can register this client without an administrator credential. Use this shortcut only in a local test environment. In production, create machine clients through your identity provider’s normal process, and do not allow DCR clients to use the client credentials grant.
REGISTRATION=$(curl --fail --silent --show-error \ -H "Content-Type: application/json" \ -d '{ "client_name": "jwt-auth-guide", "grant_types": ["client_credentials"], "token_endpoint_auth_method": "client_secret_basic" }' \ "$KEYCLOAK_URL/realms/master/clients-registrations/openid-connect") KEYCLOAK_CLIENT=$(jq -r .client_id <<<"$REGISTRATION") KEYCLOAK_SECRET=$(jq -r .client_secret <<<"$REGISTRATION") echo $KEYCLOAK_CLIENTGet an access token for the client by using the client credentials grant. A service that calls your API uses this grant to authenticate as itself, with no user involved. This token identifies the client, not a person, so it carries no username.
ACCESS_TOKEN=$(curl -s -u "${KEYCLOAK_CLIENT}:${KEYCLOAK_SECRET}" \ -d grant_type=client_credentials \ "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \ | jq -r '.access_token') echo $ACCESS_TOKENRepeat the request to the httpbin app. This time, include the JWT token that you received in the previous step. Verify that the request succeeds and you get back a 200 HTTP response code.
curl -v "${INGRESS_GW_ADDRESS}:80/headers" -H "host: www.example.com" -H "Authorization: Bearer ${ACCESS_TOKEN}"Example output:
... < HTTP/1.1 200 OK ... { "headers": { "Accept": [ "*/*" ], "Host": [ "www.example.com" ], "User-Agent": [ "curl/8.7.1" ] } }
Authorize requests by a JWT claim
Authentication proves who sent the request. Authorization decides what that identity is allowed to do. After agentgateway validates the JWT, the claims are available to Common Expression Language (CEL) expressions through the jwt variable, so you can write access rules against them in the same AgentgatewayPolicy.
This distinction matters when DCR is enabled. Registration lets a client obtain a token from the identity provider, but it does not grant that client access to your backends. Authorization is what decides what actions the client can perform.
Update the
jwt-auth-policyto add an authorization rule. The following example allows only the client that you registered, and denies every other identity.kubectl apply -f - <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayPolicy metadata: name: jwt-auth-policy namespace: agentgateway-system spec: targetRefs: - group: gateway.networking.k8s.io kind: Gateway name: agentgateway-proxy traffic: jwtAuthentication: mode: Strict providers: - issuer: "${KEYCLOAK_ISSUER}" jwks: remote: jwksPath: "${KEYCLOAK_JWKS_PATH}" cacheDuration: "5m" backendRef: group: "" kind: Service name: keycloak namespace: keycloak port: 8080 # Allow only the identity that the JWT belongs to authorization: action: Allow policy: matchExpressions: - "jwt.azp == '${KEYCLOAK_CLIENT}'" EOFField Description traffic.authorization.actionThe effect of the rule when it matches. When at least one Allowrule is configured, agentgateway denies every request that no allow rule matches.traffic.authorization.policy.matchExpressionsThe CEL expressions that must all evaluate to true for the rule to match. This example compares the azpclaim, which Keycloak sets to the client ID that the token was issued to.Note
Authorization runs only after authentication succeeds. A request with a missing or invalid token fails JWT authentication and returns a
401before any expression is evaluated. Authorization denials return a403.Repeat the request with the client’s token. Verify that the request still succeeds, because the
azpclaim matches the allow rule.curl -v "${INGRESS_GW_ADDRESS}:80/headers" -H "host: www.example.com" -H "Authorization: Bearer ${ACCESS_TOKEN}"Register a second client, get a token for it, and send the same request. Verify that the request fails with a
403 Forbiddenresponse code. The token is valid, so authentication succeeds, but no allow rule matches the second client.OTHER_REGISTRATION=$(curl --fail --silent --show-error \ -H "Content-Type: application/json" \ -d '{ "client_name": "jwt-auth-guide-other", "grant_types": ["client_credentials"], "token_endpoint_auth_method": "client_secret_basic" }' \ "$KEYCLOAK_URL/realms/master/clients-registrations/openid-connect") OTHER_TOKEN=$(curl -s \ -u "$(jq -r .client_id <<<"$OTHER_REGISTRATION"):$(jq -r .client_secret <<<"$OTHER_REGISTRATION")" \ -d grant_type=client_credentials \ "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \ | jq -r '.access_token') curl -v "${INGRESS_GW_ADDRESS}:80/headers" -H "host: www.example.com" -H "Authorization: Bearer ${OTHER_TOKEN}"Example output:
... < HTTP/1.1 403 Forbidden authorization failed ...
For more authorization rules, such as combining Allow with Require or restricting access by source address, see Authorization. For the claims and functions that you can use in an expression, see the CEL reference.
Other JWT auth examples
Review other common JWT auth configuration examples that you can add to your AgentgatewayPolicy.
Multiple JWT providers
You can configure multiple JWT providers to accept tokens from different identity providers. The following example uses Keycloak and the Auth0 identity providers.
traffic:
jwtAuthentication:
mode: Strict
providers:
- issuer: "${KEYCLOAK_ISSUER}"
audiences: ["my-application"]
jwks:
remote:
jwksPath: "${KEYCLOAK_JWKS_PATH}"
backendRef:
name: keycloak
namespace: keycloak
kind: Service
port: 8080
- issuer: "https://auth0.example.com/"
audiences: ["my-other-application"]
jwks:
remote:
jwksPath: "/.well-known/jwks.json"
backendRef:
name: auth0-proxy
namespace: auth-system
kind: Service
port: 443External identity provider over TLS
When your identity provider runs outside the cluster (for example, Okta, Auth0, or Microsoft Entra ID) and is served over HTTPS, reference an AgentgatewayBackend in the jwks.remote.backendRef instead of a Kubernetes Service. The AgentgatewayBackend sets the upstream host and TLS SNI together, so the JWKS fetch connects to the provider with the correct hostname and certificate.
Create an AgentgatewayBackend for the identity provider. Set
static.hostto the provider’s public hostname andpolicies.tls.snito the same hostname. Because nocaCertificateRefsare set, the provider’s certificate is verified against the system trust store.kubectl apply -f - <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayBackend metadata: name: okta-jwks namespace: agentgateway-system spec: static: host: myorg.okta.com port: 443 policies: tls: sni: myorg.okta.com EOFCreate an AgentgatewayPolicy that points
jwks.remote.backendRefat the AgentgatewayBackend that you created.kubectl apply -f - <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayPolicy metadata: name: jwt-auth-policy namespace: agentgateway-system spec: # Target the Gateway to apply JWT authentication to all routes targetRefs: - group: gateway.networking.k8s.io kind: Gateway name: agentgateway-proxy # Configure JWT authentication traffic: jwtAuthentication: mode: Strict providers: - issuer: "https://myorg.okta.com/oauth2/default" audiences: ["my-application"] jwks: remote: jwksPath: "/oauth2/default/v1/keys" cacheDuration: "5m" backendRef: group: agentgateway.dev kind: AgentgatewayBackend name: okta-jwks port: 443 EOFNote
If the AgentgatewayBackend is in a different namespace than the AgentgatewayPolicy, add the
namespacefield to thebackendRefand create aReferenceGrantthat permits the cross-namespace reference.
Inline JWKS
For testing purposes, you can use inline JWKS instead of a remote JWKS endpoint. Note that this setup is not recommended for production as it requires manual key updates.
traffic:
jwtAuthentication:
mode: Strict
providers:
- issuer: "${KEYCLOAK_ISSUER}"
audiences: ["my-application"]
jwks:
inline: '{"keys":[{"kty":"RSA","kid":"key-id-123","use":"sig","n":"0vx7agoebG...","e":"AQAB"}]}'Allow missing
By default, the JWT validation mode is set to Strict and allows connections to a backend destination only if a valid JWT was provided as part of the request.
To allow requests, even if no JWT was provided or if the JWT cannot be validated, use the Permissive or Optional modes.
Optional
The JWT is optional. If a JWT is provided during the request, the agentgateway proxy validates it. In the case that the JWT validation fails, the request is denied. However, keep in mind that if no JWT is provided during the request, the request is explicitly allowed.
traffic:
jwtAuthentication:
mode: Optional
providers:
- issuer: "${KEYCLOAK_ISSUER}"
audiences: ["my-application"]
jwks:
remote:
jwksPath: "${KEYCLOAK_JWKS_PATH}"
backendRef:
name: keycloak
namespace: keycloak
kind: Service
port: 8080Permissive
Requests are never rejected, even if no or invalid JWTs are provided during the request.
traffic:
jwtAuthentication:
mode: Permissive
providers:
- issuer: "${KEYCLOAK_ISSUER}"
audiences: ["my-application"]
jwks:
remote:
jwksPath: "${KEYCLOAK_JWKS_PATH}"
backendRef:
name: keycloak
namespace: keycloak
kind: Service
port: 8080PreRouting phase
By default, JWT authentication is enforced during routing. Use the PreRouting phase to validate JWTs before any routing decision is made. This is useful when you want to enforce authentication for all traffic at the gateway level, regardless of the route.
traffic:
phase: PreRouting
jwtAuthentication:
mode: Strict
providers:
- issuer: "${KEYCLOAK_ISSUER}"
audiences: ["my-application"]
jwks:
remote:
jwksPath: "${KEYCLOAK_JWKS_PATH}"
cacheDuration: "5m"
backendRef:
name: keycloak
namespace: keycloak
kind: Service
port: 8080Use JWT claims in transformations
After a JWT is validated, its claims are available to CEL expressions through the jwt context variable. You can use these claims in transformations to forward the authenticated user’s identity to your backends, or to route requests based on a claim. See Claim-based routing.
The jwt variable is populated only after the JWT is validated. Keep jwtAuthentication and the transformation on the same AgentgatewayPolicy and phase so that both apply to the same requests. JWT authentication always runs before transformations in the request pipeline, so the claims are available when the transformation evaluates them.
Available JWT claims
Access standard and custom claims from the jwt variable. Registered claims, such as sub, use dot notation. Custom claims whose names contain special characters, such as a URL, require bracket notation.
| CEL expression | Description |
|---|---|
jwt.sub | The subject (sub) claim, which is usually the user ID. |
jwt.iss | The issuer (iss) claim. |
jwt.aud | The audience (aud) claim. |
jwt.exp | The expiration (exp) time, as a Unix timestamp. |
jwt['custom-claim'] | Any custom claim. Use bracket notation for claim names that contain special characters, such as jwt['https://example.com/tier']. |
jwt.rawToken | The raw bearer token. Redacted by default. Use jwt.rawToken.unredacted() to access the value. |
Because the value field of a transformation is a CEL expression, jwt.sub refers to the claim value, not the literal string jwt.sub. To set a header to a fixed string instead, wrap the value in inner single quotes, such as value: "'my-value'". A claim might also be absent from a token, so wrap claim access in default() to provide a fallback and avoid errors, such as default(jwt.role, 'user'). For the full list of context variables and functions, see the CEL reference.
Forward JWT claims to a backend
In this example, you add a transformation to the JWT policy that copies claims from the validated token into request headers before the request is forwarded to the backend. This is a common way to pass the authenticated user’s identity to upstream apps without having them parse the JWT.
Update the
jwt-auth-policyto add atransformationthat sets request headers from JWT claims. Thex-user-roleheader usesdefault()to fall back touserwhen theroleclaim is absent.kubectl apply -f - <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayPolicy metadata: name: jwt-auth-policy namespace: agentgateway-system spec: targetRefs: - group: gateway.networking.k8s.io kind: Gateway name: agentgateway-proxy traffic: jwtAuthentication: mode: Strict providers: - issuer: "${KEYCLOAK_ISSUER}" jwks: remote: jwksPath: "${KEYCLOAK_JWKS_PATH}" cacheDuration: "5m" backendRef: group: "" kind: Service name: keycloak namespace: keycloak port: 8080 # Copy JWT claims into request headers for the backend transformation: request: set: - name: x-user-id value: "jwt.sub" - name: x-auth-issuer value: "jwt.iss" - name: x-user-role value: "default(jwt.role, 'user')" EOFUsing the
ACCESS_TOKENthat you retrieved in Verify JWT authentication, send an authenticated request to the httpbin app. Because httpbin echoes back the headers that it receives, you can verify that the claims were injected.curl -s "${INGRESS_GW_ADDRESS}:80/headers" -H "host: www.example.com" -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.headers'In the response, verify that the
X-User-Id,X-Auth-Issuer, andX-User-Roleheaders contain the values from your JWT claims.{ "Accept": ["*/*"], "Host": ["www.example.com"], "User-Agent": ["curl/8.7.1"], "X-Auth-Issuer": ["http://keycloak:8080/realms/master"], "X-User-Id": ["a1b2c3d4-..."], "X-User-Role": ["user"] }
Claim-based routing
You can route requests to different backends based on a JWT claim, such as sending premium and free-tier users to different services. To do this, use a PreRouting transformation to copy a claim into a request header, then match on that header in your HTTPRoute rules. The PreRouting phase runs the transformation before the gateway makes a routing decision, so the header is available for matching. See PreRouting phase.
Important
This example is illustrative. The following steps show how to derive a routing header from a claim and confirm it is set, but verifying the full premium/free split requires two things that you must configure:
- Running
premium-backendandfree-backendServices. Replace them with your own backends, then compare which one handles the request. - A token that carries the
tierclaim. The default Keycloak master-realm token has notierclaim, so every request falls back tofree. To exercise the premium path, configure a Keycloak client scope or protocol mapper that adds atierclaim, then request a token that includestier: premium.
Create or update the
jwt-auth-policyto validate the JWT and, in thePreRoutingphase, copy atierclaim into anx-user-tierheader. Reusing the same policy name replaces the policy from the previous section, so that only one JWT policy targets the Gateway.kubectl apply -f - <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayPolicy metadata: name: jwt-auth-policy namespace: agentgateway-system spec: targetRefs: - group: gateway.networking.k8s.io kind: Gateway name: agentgateway-proxy traffic: phase: PreRouting jwtAuthentication: mode: Strict providers: - issuer: "${KEYCLOAK_ISSUER}" jwks: remote: jwksPath: "${KEYCLOAK_JWKS_PATH}" cacheDuration: "5m" backendRef: group: "" kind: Service name: keycloak namespace: keycloak port: 8080 transformation: request: set: - name: x-user-tier value: "default(jwt.tier, 'free')" EOFBefore you add routing rules, confirm that the
PreRoutingtransformation derives thex-user-tierheader from the JWT claim. Send an authenticated request to the httpbin app, which echoes back the headers that it receives. Because the default Keycloak token has notierclaim,default(jwt.tier, 'free')evaluates tofree. For local testing with port-forwarding, usehttp://localhost:8080/headersinstead.curl -s "${INGRESS_GW_ADDRESS}:80/headers" -H "host: www.example.com" -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.headers'In the response, verify that the
X-User-Tierheader is set tofree.{ "Accept": ["*/*"], "Host": ["www.example.com"], "User-Agent": ["curl/8.7.1"], "X-User-Tier": ["free"] }Create an
HTTPRoutethat routes requests to different backends based on thex-user-tierheader. Requests withx-user-tier: premiumgo to the premium backend, and all other requests fall through to the default backend.kubectl apply -f - <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: tier-routing namespace: agentgateway-system spec: parentRefs: - name: agentgateway-proxy hostnames: - www.example.com rules: # Premium users, matched on the header set from the JWT claim - matches: - headers: - name: x-user-tier value: premium backendRefs: - name: premium-backend port: 8080 # All other users - backendRefs: - name: free-backend port: 8080 EOF
Cleanup
You can remove the resources that you created in this guide.kubectl delete AgentgatewayPolicy jwt-auth-policy -n agentgateway-system
kubectl delete httproute tier-routing -n agentgateway-system --ignore-not-found
kubectl delete ns keycloak