This guide describes how to configure JWT claim validation in F5 NGINX Gateway Fabric using the AuthenticationFilter custom resource definition (CRD).
JWT claim validation adds authorization after JWT authentication and OIDC authentication. Authentication checks whether a token is valid and signed correctly. Claim validation checks claims in the token payload. You can require a specific issuer, audience, or custom claim before users access your application.
By following these instructions, you will configure an AuthenticationFilter with claim validation rules and verify that only tokens containing the expected claims are allowed through.
For demonstration purposes, in this document you will deploy and configure a type: JWT AuthenticationFilter using mode: File.
JWT claim validation requires NGINX Plus.
JWT claim validation is configured through the authorization field on the AuthenticationFilter spec. It uses a two-level require model that controls how rules and claims are evaluated.
Top-level require
The top-level require field (authorization.require) controls how rules relate to each other:
- Any (default) — A request is authorized if any one of the rules are satisfied.
- All — A request is authorized only if every rule is satisfied.
Per-rule require
Each rule has its own require field (rules[].require) that controls how the claims within that rule relate to each other:
- Any (default) — The rule is satisfied if any one of its claims matches.
- All — The rule is satisfied only if every claim in the rule matches.
How the two levels work together
Consider an AuthenticationFilter with authorization.require: Any and two rules, each with require: All:
- Rule 0 requires all of:
iss=issuer-1andaud=api - Rule 1 requires all of:
iss=issuer-2andaud=admin
Since the top-level require is Any, a request is authorized if the token satisfies either rule 0 or rule 1. A token that only partially matches both rules would be rejected as it does not fully satisfy either rule.
- Install NGINX Gateway Fabric with NGINX Plus.
In this part of the document, we will set up several resources in your cluster to demonstrate the authorization field of the AuthenticationFilter CRD.
To deploy the coffee and tea applications, run the following YAML with kubectl apply:
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: coffee
spec:
replicas: 1
selector:
matchLabels:
app: coffee
template:
metadata:
labels:
app: coffee
spec:
containers:
- name: coffee
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: coffee
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: coffee
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tea
spec:
replicas: 1
selector:
matchLabels:
app: tea
template:
metadata:
labels:
app: tea
spec:
containers:
- name: tea
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: tea
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: tea
EOFTo confirm the application pods are running, run kubectl get:
kubectl get podsNAME READY STATUS RESTARTS AGE
coffee-654ddf664b-4fmcq 1/1 Running 0 13s
tea-75bc9f4b6d-8gtjl 1/1 Running 0 13sTo create your Gateway resource and provision the NGINX pod, run the following YAML with kubectl apply:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: cafe-gateway
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP
hostname: "cafe.example.com"
EOFConfirm the Gateway was assigned an IP address and reports a Programmed=True status with kubectl describe:
kubectl describe gateways.gateway.networking.k8s.io cafe-gatewayAddresses:
Type: IPAddress
Value: 192.0.2.1Set the Gateway IP address and port in shell variables:
GW_IP=<GATEWAY_IP>
GW_PORT=<GATEWAY_PORT>For testing purposes, the following example shows a simple JWKS with a single RSA key. In production, use properly generated keys from your identity provider or key management system.
Save the JWKS to a file called auth and create a Secret:
cat <<EOF > auth
{
"keys": [
{
"kty": "RSA",
"kid": "test-key",
"use": "sig",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e": "AQAB"
}
]
}
EOF
kubectl create secret generic jwks-secret --from-file=authThis example JWKS is for demonstration only. In production, use keys from your identity provider or key management system.
This example creates an AuthenticationFilter with two rules that enforce different issuer and audience combinations. The top-level require is set to Any, and each rule’s require is set to All.
This means a request is authorized if its JWT satisfies all claims in rule 0 or all claims in rule 1.
Deploy the AuthenticationFilter with claim validation rules by running the following YAML with kubectl apply:
kubectl apply -f - <<EOF
apiVersion: gateway.nginx.org/v1alpha1
kind: AuthenticationFilter
metadata:
name: jwt-claims
spec:
type: JWT
jwt:
source: File
file:
secretRef:
name: jwks-secret
realm: "nginx-gateway"
keyCache: "1h"
authorization:
require: Any # Top-level require
rules:
- require: All # Require for rules[0]
claims:
- name: "iss"
values:
- "https://issuer-1.example.com"
- name: "aud"
values:
- "api"
- require: All # Require for rules[1]
claims:
- name: "iss"
values:
- "https://issuer-2.example.com"
- name: "aud"
values:
- "admin"
EOFVerify the AuthenticationFilter is accepted with kubectl describe:
kubectl describe authenticationfilters.gateway.nginx.org jwt-claims | grep "Status:" -A10Status:
Controllers:
Conditions:
Last Transition Time: 2026-09-03T10:26:58Z
Message: The AuthenticationFilter is accepted
Observed Generation: 1
Reason: Accepted
Status: True
Type: Accepted
Controller Name: gateway.nginx.org/nginx-gateway-controller
Events: <none>authorization.require: Any— The request passes if any rule is satisfied.- Rule 0 (
require: All) — The JWT must contain bothissequal tohttps://issuer-1.example.comandaudequal toapi. - Rule 1 (
require: All) — The JWT must contain bothissequal tohttps://issuer-2.example.comandaudequal toadmin.
The following table summarizes which tokens are authorized:
| Token claims | Rule 0 | Rule 1 | Result |
|---|---|---|---|
iss=issuer-1, aud=api |
✅ All matched | ❌ | ✅ Authorized |
iss=issuer-2, aud=admin |
❌ | ✅ All matched | ✅ Authorized |
iss=issuer-1, aud=admin |
❌ Partial | ❌ Partial | ❌ Rejected |
iss=issuer-2, aud=api |
❌ Partial | ❌ Partial | ❌ Rejected |
Deploy an HTTPRoute that applies the AuthenticationFilter to the /coffee path. The /tea path has no authentication and responds normally. Run the following YAML with kubectl apply:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: cafe-routes
spec:
parentRefs:
- name: cafe-gateway
sectionName: http
hostnames:
- "cafe.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /coffee
backendRefs:
- name: coffee
port: 80
filters:
- type: ExtensionRef
extensionRef:
group: gateway.nginx.org
kind: AuthenticationFilter
name: jwt-claims
- matches:
- path:
type: PathPrefix
value: /tea
backendRefs:
- name: tea
port: 80
EOFVerify the HTTPRoute is accepted with kubectl describe:
kubectl describe httproute cafe-routes | grep "Status:" -A10Status:
Parents:
Conditions:
Last Transition Time: 2026-09-03T10:28:15Z
Message: The Route is accepted
Observed Generation: 1
Reason: Accepted
Status: True
Type: Accepted
Last Transition Time: 2026-09-03T10:28:15Z
Message: All references are resolved
Observed Generation: 1
Reason: ResolvedRefs
Status: True
Type: ResolvedRefs
Controller Name: gateway.nginx.org/nginx-gateway-controller
Parent Ref:
Group: gateway.networking.k8s.io
Kind: Gateway
Name: cafe-gateway
Namespace: default
Section Name: http
Events: <none>Your clients should be able to resolve "cafe.example.com" to the public IP of the NGINX Service.
This guide simulates that using curl’s
--resolveoption.
To test claim validation, you need JWTs signed with the private key corresponding to the public key in your JWKS. You can use jwt.io or other JWT tools to generate tokens with different claim payloads. Store each token in a shell variable.
Token matching rule 0 (iss=https://issuer-1.example.com, aud=api):
JWT_RULE0="<SIGNED_JWT_FOR_RULE_0>"Token matching rule 1 (iss=https://issuer-2.example.com, aud=admin):
JWT_RULE1="<SIGNED_JWT_FOR_RULE_1>"Token matching neither rule (iss=https://issuer-1.example.com, aud=admin):
JWT_NEITHER="<SIGNED_JWT_FOR_NO_RULE>"Access /coffee with a token matching rule 0
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "Authorization: Bearer $JWT_RULE0"Server address: 192.0.2.7:8080
Server name: coffee-654ddf664b-nhhvr
Date: 10/Mar/2026:15:20:15 +0000
URI: /coffee
Request ID: 13a925b2514b62c45ea4a79800248d5cThe request succeeds as the token satisfies all claims in rule 0.
Access /coffee with a token matching rule 1
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "Authorization: Bearer $JWT_RULE1"Server address: 192.0.2.7:8080
Server name: coffee-654ddf664b-nhhvr
Date: 02/Sep/2026:15:21:30 +0000
URI: /coffee
Request ID: 7b2e4a1c9f0d3e5a8c6b4d2f1a0e9c8bThe request succeeds as the token satisfies all claims in rule 1.
Access /coffee with a token matching neither rule
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "Authorization: Bearer $JWT_NEITHER"<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx</center>
</body>
</html>The request is rejected. Although the token has valid claims, it only partially matches each rule (iss from rule 0 and aud from rule 1). Since each rule requires all claims to match and the token does not fully satisfy either rule, authorization fails.
Access /coffee without a token
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx</center>
</body>
</html>Without a JWT, the request fails authentication before claim validation is evaluated.
Access /tea without authentication
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/teaServer address: 10.244.0.10:8080
Server name: tea-75bc9f4b6d-ms2n8
Date: 03/Sep/2026:15:36:26 +0000
URI: /tea
Request ID: c7eb0509303de1c160cb7e7d2ac1d99fThe /tea path has no AuthenticationFilter attached and responds normally.
JWT claims can be nested at multiple levels. Use the slash (/) separator to identify the required claim value.
In this example, we set up the rule to access the values of the roles claim, which is nested under realm_access
Example JSON payload
{
"realm_access": {
"roles": ["reader", "admin"]
}
}authorization:
require: Any
rules:
- require: All
claims:
- name: "realm_access/roles"
values:
- "reader"
- "admin"By default, claim values are evaluated against their exact value. This can also be set to Regex allowing for a more complex and expressive matching configuration.
This example allows an email claim that contains @example.com. This would match on foo@example.com, user@example.com etc…
authorization:
require: Any
rules:
- require: Any
claims:
- name: email
match: Regex
values:
- ".*@example\\.com"By defining the proxySetHeader for a specific claim, you can forward the value of a matched claim as a request header to the upstream application.
This example shows defining a header called X-Aud on the aud claim.
authorization:
require: Any
rules:
- require: All
claims:
- name: "aud"
proxySetHeader: X-Aud
values:
- "api"- Ensure NGINX Gateway Fabric is deployed with NGINX Plus. JWT claim validation is not supported in the open source version.
- Ensure the AuthenticationFilter is accepted by checking its status with
kubectl describe. - Ensure the HTTPRoute references the correct AuthenticationFilter name and group.
- Confirm the Secret key is named
authand contains valid JWKS JSON. The Secret must be in the same namespace as the AuthenticationFilter. - Verify your JWT includes the
kid(key ID) claim that matches one of the keys in your JWKS. - Check that the JWT is not expired by verifying the
expclaim. - Ensure the JWT signature algorithm (typically RS256) matches the key type in your JWKS.
- If claim validation rejects a token you expect to pass, decode the token at jwt.io and verify that the claim names and values exactly match what is configured in the AuthenticationFilter.