JWT auth
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
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 two user credentials as follows.
- Username:
user1, password:password, email:[email protected] - Username:
user2, password:password, email:[email protected]
Install and configure Keycloak:
-
Create a namespace for your Keycloak deployment.
kubectl create namespace keycloak -
Create the Keycloak deployment.
kubectl -n keycloak apply -f https://raw.githubusercontent.com/solo-io/gloo-mesh-use-cases/main/policy-demo/oidc/keycloak.yaml -
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_URL -
Set 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 -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) echo $KEYCLOAK_TOKEN -
Use the admin token to configure Keycloak with the two users for testing purposes. If you get a
401 Unauthorizederror, run the previous command and try again.# Create initial token to register the client read -r client token <<<$(curl -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -X POST -H "Content-Type: application/json" -d '{"expiration": 0, "count": 1}' $KEYCLOAK_URL/admin/realms/master/clients-initial-access | jq -r '[.id, .token] | @tsv') export KEYCLOAK_CLIENT=${client} echo $KEYCLOAK_CLIENT # Register the client read -r id secret <<<$(curl -k -X POST -d "{ \"clientId\": \"${KEYCLOAK_CLIENT}\" }" -H "Content-Type:application/json" -H "Authorization: bearer ${token}" ${KEYCLOAK_URL}/realms/master/clients-registrations/default| jq -r '[.id, .secret] | @tsv') export KEYCLOAK_SECRET=${secret} echo $KEYCLOAK_SECRET # Add allowed redirect URIs curl -k -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -X PUT -H "Content-Type: application/json" -d '{"serviceAccountsEnabled": true, "directAccessGrantsEnabled": true, "authorizationServicesEnabled": true, "redirectUris": ["*"]}' $KEYCLOAK_URL/admin/realms/master/clients/${id} # Add the group attribute in the JWT token returned by Keycloak curl -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -X POST -H "Content-Type: application/json" -d '{"name": "group", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "config": {"claim.name": "group", "jsonType.label": "String", "user.attribute": "group", "id.token.claim": "true", "access.token.claim": "true"}}' $KEYCLOAK_URL/admin/realms/master/clients/${id}/protocol-mappers/models # Create first user curl -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -X POST -H "Content-Type: application/json" -d '{"username": "user1", "email": "[email protected]", "firstName": "Alice", "lastName": "Doe", "enabled": true, "attributes": {"group": "users"}, "credentials": [{"type": "password", "value": "password", "temporary": false}]}' $KEYCLOAK_URL/admin/realms/master/users # Create second user curl -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -X POST -H "Content-Type: application/json" -d '{"username": "user2", "email": "[email protected]", "firstName": "Bob", "lastName": "Doe", "enabled": true, "attributes": {"group": "users"}, "credentials": [{"type": "password", "value": "password", "temporary": false}]}' $KEYCLOAK_URL/admin/realms/master/users # Remove the trusted-hosts client registration policies (testing-purpose only) trusted_hosts=$(curl -v -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "${KEYCLOAK_URL}/admin/realms/master/components?type=org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" \ | jq -r ' if type=="array" then .[] | select(.providerId=="trusted-hosts") | .id else empty end ') curl -X DELETE \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "${KEYCLOAK_URL}/admin/realms/master/components/${trusted_hosts}" # Remove the allowed-client-templates client registration policies (testing-purpose only) allowed_client_templates=$(curl -v \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "${KEYCLOAK_URL}/admin/realms/master/components?type=org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" \ | jq -r ' .[] | select(.providerId=="allowed-client-templates" and .subType=="anonymous") | .id ') curl -X DELETE \ -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \ "${KEYCLOAK_URL}/admin/realms/master/components/${allowed_client_templates}" -
Open the Keycloak frontend.
open $KEYCLOAK_URL -
Log 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 created earlier are displayed. You might need to click on View all users to see them.
-
In the Keycloak admin console, go to Clients, and verify that you can see a client ID that equals the output of
$KEYCLOAK_CLIENT.
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_URL -
Get 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.
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 Example 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.StrictissuerThe issuer URL that must match the issclaim in JWT tokens exactly. Agentgateway rejects tokens from other issuers.http://keycloak:8080/realms/masteraudiencesList of allowed audience values. The JWT’s audclaim must contain at least one of these values. If not specified, any audience is accepted.["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. /realms/master/protocol/openid-connect/certsjwks.remote.cacheDurationHow long to cache the JWKS keys locally. This reduces load on the identity provider and improves performance. Keys are automatically refreshed when the cache expires. 5m(5 minutes)jwks.remote.backendRefReference to the Kubernetes service that hosts the identity provider. Agentgateway uses this to fetch the JWKS from the identity provider. Keycloak service -
View 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"curl -i localhost:8080/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% -
Get an access token from Keycloak by using the password grant type.
ACCESS_TOKEN=$(curl -s -X POST "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=password" \ -d "client_id=${KEYCLOAK_CLIENT}" \ -d "client_secret=${KEYCLOAK_SECRET}" \ -d "username=user1" \ -d "password=password" \ | jq -r '.access_token') echo $ACCESS_TOKEN -
Repeat 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}"curl -v "http://localhost:8080/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" ] } }
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: 443Inline 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: 8080Cleanup
You can remove the resources that you created in this guide.kubectl delete AgentgatewayPolicy jwt-auth-policy -n agentgateway-system
kubectl delete ns keycloak