# Configure JSON Web Token (JWT) authentication Type of document: How-to guide Product: NGINX Gateway Fabric --- This guide describes how to configure JSON Web Token (JWT) authentication in NGINX Gateway Fabric using the AuthenticationFilter custom resource definition (CRD). JWT authentication secures applications and APIs by validating JSON Web Tokens in incoming requests. Only requests with valid JWTs are allowed access. By following these instructions, you will create two sample application endpoints: one with JWT authentication and one without, so you can see how each behaves. **Note:** JWT authentication requires NGINX Plus. ## Overview JWT authentication in NGINX Gateway Fabric validates JSON Web Tokens using JSON Web Key Sets (JWKS). The JWKS contains the public keys used to verify JWT signatures. When a request arrives with a JWT in the `Authorization` header, NGINX Plus validates the token against the configured JWKS before forwarding the request to your application. NGINX Gateway Fabric supports two JWKS source types, set using the `source` field on the `AuthenticationFilter`: - **File** — JWKS is stored locally in a Kubernetes Secret. Use this when you manage your own keys or want to avoid external dependencies. - **Remote** — NGINX Plus fetches JWKS from an HTTPS endpoint at runtime. Use this when your identity provider (for example, Keycloak or Auth0) exposes a JWKS URI. ## Before you begin - [Install](/ngf/install/) NGINX Gateway Fabric with NGINX Plus. ## Common setup The following steps are required for both file-based and remote JWT authentication. ### Deploy sample applications To deploy the `coffee` and `tea` applications, run the following YAML with `kubectl apply`: ```yaml kubectl apply -f - < ``` --- ## File-based JWT authentication Use file-based JWT authentication when your JWKS is stored in a Kubernetes Secret. NGINX Plus loads the key material directly from the Secret at startup and after each reload. ### Generate a JWKS 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. ```json { "keys": [ { "kty": "RSA", "kid": "test-key", "use": "sig", "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", "e": "AQAB" } ] } ``` **Note:** This example JWKS is for demonstration only. In production, use keys from your identity provider or key management system. ### Create a JWKS Secret and AuthenticationFilter Deploy a Secret containing your JWKS and the AuthenticationFilter by running these `kubectl` commands: ``` kubectl create secret generic jwks-secret --from-file=auth=jwks.json ``` ```yaml kubectl apply -f - < ``` ### Deploy an HTTPRoute referencing the AuthenticationFilter Deploy an HTTPRoute that references the AuthenticationFilter using the `ExtensionRef` filter type. In this example, the filter is applied to the `/coffee` path only. Run the following YAML with `kubectl apply`: ```yaml kubectl apply -f - < ``` ### Verify file-based JWT authentication **Note:** 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 `--resolve` option. To test the authentication, you need a JWT signed with the private key that corresponds to the public key in your JWKS. You can use [jwt.io](https://jwt.io) or other JWT tools to generate one. Store it in a shell variable: ```shell JWT_TOKEN="" ``` Access `/coffee` with a valid JWT: ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "Authorization: Bearer $JWT_TOKEN" ``` ```text Server address: 10.244.0.7:8080 Server name: coffee-654ddf664b-nhhvr Date: 10/Mar/2026:15:20:15 +0000 URI: /coffee Request ID: 13a925b2514b62c45ea4a79800248d5c ``` Access `/coffee` without a JWT: ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee ``` ```text 401 Authorization Required

401 Authorization Required


nginx
``` Access `/coffee` with an invalid JWT: ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "Authorization: Bearer invalid.jwt.token" ``` ```text 401 Authorization Required

401 Authorization Required


nginx
``` Access `/tea`, which has no AuthenticationFilter and responds normally: ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea ``` ```text Server address: 10.244.0.10:8080 Server name: tea-75bc9f4b6d-ms2n8 Date: 10/Mar/2026:15:36:26 +0000 URI: /tea Request ID: c7eb0509303de1c160cb7e7d2ac1d99f ``` --- ## Remote JWT authentication Use remote JWT authentication when your identity provider (IdP) exposes a JWKS endpoint. NGINX Plus fetches the JWKS from the URI at runtime using an internal subrequest, so keys are always up to date without requiring a Secret or NGINX reload. ### Install cert-manager Install cert-manager onto the cluster using Helm with Gateway API features enabled. - Add the Helm repository. ```shell helm repo add jetstack https://charts.jetstack.io helm repo update ``` - Install cert-manager, and enable the GatewayAPI feature gate: ```shell helm install \ cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set config.apiVersion="controller.config.cert-manager.io/v1alpha1" \ --set config.kind="ControllerConfiguration" \ --set config.enableGatewayAPI=true \ --set crds.enabled=true ``` ### Generate certificates The following steps use `cert-manager` to issue a local Certificate Authority (CA) and sign certificates for both Keycloak and NGINX. `cert-manager` creates the required Kubernetes Secrets directly so no manual secret creation is needed for TLS. Create a self-signed `ClusterIssuer` to bootstrap the CA, then issue the CA certificate and create a second `ClusterIssuer` backed by it: ```yaml kubectl apply -f - < ``` ### Deploy an HTTPRoute referencing the remote AuthenticationFilter Deploy an HTTPRoute that applies the remote AuthenticationFilter to `/coffee`. Run the following YAML with `kubectl apply`: ```yaml kubectl apply -f - < ``` ### Obtain a JWT Expose Keycloak with port-forward: ```shell kubectl port-forward svc/keycloak 8443:8443 ``` Set your `JWT_TOKEN` environment variable by calling the `tokens` endpoint in keycloak: ```shell export JWT_TOKEN=$(curl -s -k -X POST https://localhost:8443/realms/nginx-gateway/protocol/openid-connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=testuser" \ -d "password=testpassword" \ -d "grant_type=password" \ -d "client_id=cafe-app" | jq -r '.access_token') ``` ### Verify remote JWT authentication **Note:** 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 `--resolve` option. Access `/coffee` with a valid JWT: ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "Authorization: Bearer $JWT_TOKEN" ``` ```text Server address: 10.244.0.7:8080 Server name: coffee-654ddf664b-nhhvr Date: 10/Mar/2026:15:20:15 +0000 URI: /coffee Request ID: 13a925b2514b62c45ea4a79800248d5c ``` Access `/coffee` without a JWT: ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee ``` ```text 401 Authorization Required

401 Authorization Required


nginx
``` Access `/tea`, which has no AuthenticationFilter and responds normally: ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea ``` ```text Server address: 10.244.0.10:8080 Server name: tea-75bc9f4b6d-ms2n8 Date: 10/Mar/2026:15:36:26 +0000 URI: /tea Request ID: c7eb0509303de1c160cb7e7d2ac1d99f ``` --- ## Troubleshooting - Ensure NGINX Gateway Fabric is deployed with NGINX Plus. JWT authentication is not supported in the open source version. - Ensure the HTTPRoute is accepted and references the correct AuthenticationFilter name and group. - For file-based JWT: confirm the Secret key is named `auth` and contains valid JWKS JSON. The Secret must be in the same namespace as the AuthenticationFilter. - For remote JWT: confirm the `uri` uses the `https://` scheme and the endpoint is reachable from the NGINX Plus pod. - For remote JWT with a custom CA: confirm the Secret key is named `ca.crt` and contains a valid PEM certificate. 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 `exp` claim. - Ensure the JWT signature algorithm (typically RS256) matches the key type in your JWKS. ## Further reading - [AuthenticationFilter API reference](/ngf/reference/api.md#gateway.nginx.org/v1alpha1.AuthenticationFilter) - [NGINX JWT Authentication Module](https://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html) - [JWT.io - JWT debugger and generator](https://jwt.io) - [RFC 7519 - JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) - [RFC 7517 - JSON Web Key (JWK)](https://datatracker.ietf.org/doc/html/rfc7517)