diff --git a/deployments/common/policy-definition.yaml b/deployments/common/policy-definition.yaml
index 85d63ce0ab..73237addcd 100644
--- a/deployments/common/policy-definition.yaml
+++ b/deployments/common/policy-definition.yaml
@@ -53,6 +53,16 @@ spec:
                   type: array
                   items:
                     type: string
+            jwt:
+              description: JWTAuth holds JWT authentication configuration.
+              type: object
+              properties:
+                realm:
+                  type: string
+                secret:
+                  type: string
+                token:
+                  type: string
             rateLimit:
               description: RateLimit defines a rate limit policy.
               type: object
diff --git a/deployments/helm-chart/crds/policy.yaml b/deployments/helm-chart/crds/policy.yaml
index f5fda77842..2e536e1566 100644
--- a/deployments/helm-chart/crds/policy.yaml
+++ b/deployments/helm-chart/crds/policy.yaml
@@ -55,6 +55,16 @@ spec:
                   type: array
                   items:
                     type: string
+            jwt:
+              description: JWTAuth holds JWT authentication configuration.
+              type: object
+              properties:
+                realm:
+                  type: string
+                secret:
+                  type: string
+                token:
+                  type: string
             rateLimit:
               description: RateLimit defines a rate limit policy.
               type: object
diff --git a/docs-web/configuration/policy-resource.md b/docs-web/configuration/policy-resource.md
index 30114e48d1..2ff32ea01f 100644
--- a/docs-web/configuration/policy-resource.md
+++ b/docs-web/configuration/policy-resource.md
@@ -18,6 +18,8 @@ This document is the reference documentation for the Policy resource. An example
       - [AccessControl Merging Behavior](#accesscontrol-merging-behavior)
     - [RateLimit](#ratelimit)
       - [RateLimit Merging Behavior](#ratelimit-merging-behavior)
+    - [JWT](#jwt)
+      - [JWT Merging Behavior](#jwt-merging-behavior)
   - [Using Policy](#using-policy)
     - [Validation](#validation)
       - [Structural Validation](#structural-validation)
@@ -57,6 +59,10 @@ spec:
      - The rate limit policy controls the rate of processing requests per a defined key.
      - `rateLimit <#ratelimit>`_
      - No*
+   * - ``JWT``
+     - The JWT policy configures NGINX Plus to authenticate client requests using JSON Web Tokens.
+     - `jwt <#jwt>`_
+     - No*
 ```
 
 \* A policy must include exactly one policy.
@@ -112,6 +118,7 @@ When you reference more than one access control policy, the Ingress Controller w
 
 Referencing both allow and deny policies, as shown in the example below, is not supported. If both allow and deny lists are referenced, the Ingress Controller uses just the allow list policies. 
 ```yaml
+policies:
 - name: deny-policy
 - name: allow-policy-one
 - name: allow-policy-two
@@ -189,6 +196,54 @@ policies:
 
 When you reference more than one rate limit policy, the Ingress Controller will configure NGINX to use all referenced rate limits. When you define multiple policies, each additional policy inherits the `dryRun`, `logLevel`, and `rejectCode` parameters from the first policy referenced (`rate-limit-policy-one`, in the example above).
 
+### JWT
+
+> Note: This feature is only available in NGINX Plus.
+
+The JWT policy configures NGINX Plus to authenticate client requests using JSON Web Tokens.
+
+For example, the following policy will reject all requests that do not include a valid JWT in the HTTP header `token`:
+```yaml
+jwt:
+  secret: jwk-secret
+  realm: "My API"
+  token: $http_token
+```
+
+> Note: The feature is implemented using the NGINX Plus [ngx_http_auth_jwt_module](https://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html).
+
+```eval_rst
+.. list-table::
+   :header-rows: 1
+
+   * - Field
+     - Description
+     - Type
+     - Required
+   * - ``secret``
+     - The name of the Kubernetes secret that stores the JWK. It must be in the same namespace as the Policy resource. The JWK must be stored in the secret under the key ``jwk``, otherwise the secret will be rejected as invalid.
+     - ``string``
+     - Yes
+   * - ``realm``
+     - The realm of the JWT.
+     - ``string``
+     - Yes
+   * - ``token``
+     - The token specifies a variable that contains the JSON Web Token. By default the JWT is passed in the ``Authorization`` header as a Bearer Token. JWT may be also passed as a cookie or a part of a query string, for example: ``$cookie_auth_token``. Accepted variables are ``$http_``, ``$arg_``, ``$cookie_``.
+     - ``string``
+     - No
+```
+
+#### JWT Merging Behavior
+
+A VirtualServer/VirtualServerRoute can reference multiple JWT policies. However, only one can be applied. Every subsequent reference will be ignored. For example, here we reference two policies:
+```yaml
+policies:
+- name: jwt-policy-one
+- name: jwt-policy-two
+```
+In this example the Ingress Controller will use the configuration from the first policy reference `jwt-policy-one`, and ignores `jwt-policy-two`.
+
 ## Using Policy
 
 You can use the usual `kubectl` commands to work with Policy resources, just as with built-in Kubernetes resources.
diff --git a/examples-of-custom-resources/jwt/README.md b/examples-of-custom-resources/jwt/README.md
new file mode 100644
index 0000000000..abf07146a7
--- /dev/null
+++ b/examples-of-custom-resources/jwt/README.md
@@ -0,0 +1,69 @@
+# JWT
+
+In this example, we deploy a web application, configure load balancing for it via a VirtualServer, and apply a JWT policy.
+
+## Prerequisites
+
+1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/) instructions to deploy the Ingress Controller.
+1. Save the public IP address of the Ingress Controller into a shell variable:
+    ```
+    $ IC_IP=XXX.YYY.ZZZ.III
+    ```
+1. Save the HTTP port of the Ingress Controller into a shell variable:
+    ```
+    $ IC_HTTP_PORT=<port number>
+    ```
+
+## Step 1 - Deploy a Web Application
+
+Create the application deployment and service:
+```
+$ kubectl apply -f webapp.yaml
+```
+
+## Step 2 - Deploy the JWK Secret
+
+Create a secret with the name `jwk-secret` that will be used for JWT validation:
+```
+$ kubectl apply -f jwk-secret.yaml
+```
+
+## Step 3 - Deploy the JWT Policy
+
+Create a policy with the name `jwt-policy` that references the secret from the previous step and only permits requests to our web application that contain a valid JWT:
+```
+$ kubectl apply -f jwt.yaml
+```
+
+## Step 3 - Configure Load Balancing
+
+Create a VirtualServer resource for the web application:
+```
+$ kubectl apply -f virtual-server.yaml
+```
+
+Note that the VirtualServer references the policy `jwt-policy` created in Step 3.
+
+## Step 4 - Test the Configuration
+
+If you attempt to access the application without providing a valid JWT, NGINX will reject your requests for that VirtualServer:
+```
+$ curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/
+<html>
+<head><title>401 Authorization Required</title></head>
+<body>
+<center><h1>401 Authorization Required</h1></center>
+<hr><center>nginx/1.19.1</center>
+</body>
+</html>
+```
+
+If you provide a valid JWT, your request will succeed:
+```
+$ curl --resolve webapp.example.com:$IC_HTTP_PORT:$IC_IP http://webapp.example.com:$IC_HTTP_PORT/ -H "token: `cat token.jwt`"
+Server address: 172.17.0.3:8080
+Server name: webapp-7c6d448df9-lcrx6
+Date: 10/Sep/2020:18:20:03 +0000
+URI: /
+Request ID: db2c07ce640755ccbe9f666d16f85620
+```
diff --git a/examples-of-custom-resources/jwt/jwk-secret.yaml b/examples-of-custom-resources/jwt/jwk-secret.yaml
new file mode 100644
index 0000000000..2a9ea9c191
--- /dev/null
+++ b/examples-of-custom-resources/jwt/jwk-secret.yaml
@@ -0,0 +1,6 @@
+kind: Secret
+metadata:
+  name: jwk-secret
+apiVersion: v1
+data:
+  jwk: eyJrZXlzIjoKICAgIFt7CiAgICAgICAgImsiOiJabUZ1ZEdGemRHbGphbmQwIiwKICAgICAgICAia3R5Ijoib2N0IiwKICAgICAgICAia2lkIjoiMDAwMSIKICAgIH1dCn0K
diff --git a/examples-of-custom-resources/jwt/jwt.yaml b/examples-of-custom-resources/jwt/jwt.yaml
new file mode 100644
index 0000000000..659cb7395a
--- /dev/null
+++ b/examples-of-custom-resources/jwt/jwt.yaml
@@ -0,0 +1,9 @@
+apiVersion: k8s.nginx.org/v1alpha1
+kind: Policy
+metadata:
+  name: jwt-policy
+spec:
+  jwt:
+    realm: MyProductAPI
+    secret: jwk-secret
+    token: $http_token
diff --git a/examples-of-custom-resources/jwt/token.jwt b/examples-of-custom-resources/jwt/token.jwt
new file mode 100644
index 0000000000..eacb21aa8d
--- /dev/null
+++ b/examples-of-custom-resources/jwt/token.jwt
@@ -0,0 +1 @@
+eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ.eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsImlzcyI6Ik15IEFQSSBHYXRld2F5In0.ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I
diff --git a/examples-of-custom-resources/jwt/virtual-server.yaml b/examples-of-custom-resources/jwt/virtual-server.yaml
new file mode 100644
index 0000000000..4631e4f844
--- /dev/null
+++ b/examples-of-custom-resources/jwt/virtual-server.yaml
@@ -0,0 +1,16 @@
+apiVersion: k8s.nginx.org/v1
+kind: VirtualServer
+metadata:
+  name: webapp
+spec:
+  host: webapp.example.com
+  policies:
+  - name: jwt-policy
+  upstreams:
+  - name: webapp
+    service: webapp-svc
+    port: 80
+  routes:
+  - path: /
+    action:
+      pass: webapp
diff --git a/examples-of-custom-resources/jwt/webapp.yaml b/examples-of-custom-resources/jwt/webapp.yaml
new file mode 100644
index 0000000000..31fde92a6e
--- /dev/null
+++ b/examples-of-custom-resources/jwt/webapp.yaml
@@ -0,0 +1,32 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: webapp
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: webapp
+  template:
+    metadata:
+      labels:
+        app: webapp
+    spec:
+      containers:
+      - name: webapp
+        image: nginxdemos/nginx-hello:plain-text
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: webapp-svc
+spec:
+  ports:
+  - port: 80
+    targetPort: 8080
+    protocol: TCP
+    name: http
+  selector:
+    app: webapp
diff --git a/internal/configs/configurator.go b/internal/configs/configurator.go
index 9044ef4745..ed00c79259 100644
--- a/internal/configs/configurator.go
+++ b/internal/configs/configurator.go
@@ -406,8 +406,11 @@ func (cnf *Configurator) addOrUpdateVirtualServer(virtualServerEx *VirtualServer
 	if virtualServerEx.TLSSecret != nil {
 		tlsPemFileName = cnf.addOrUpdateTLSSecret(virtualServerEx.TLSSecret)
 	}
+
+	jwtKeys := cnf.addOrUpdateJWKSecretsForVirtualServer(virtualServerEx.JWTKeys)
+
 	vsc := newVirtualServerConfigurator(cnf.cfgParams, cnf.isPlus, cnf.IsResolverConfigured(), cnf.staticCfgParams)
-	vsCfg, warnings := vsc.GenerateVirtualServerConfig(virtualServerEx, tlsPemFileName)
+	vsCfg, warnings := vsc.GenerateVirtualServerConfig(virtualServerEx, tlsPemFileName, jwtKeys)
 	name := getFileNameForVirtualServer(virtualServerEx.VirtualServer)
 	content, err := cnf.templateExecutorV2.ExecuteVirtualServerTemplate(&vsCfg)
 	if err != nil {
@@ -580,8 +583,41 @@ func (cnf *Configurator) addOrUpdateJWKSecret(secret *api_v1.Secret) string {
 	return cnf.nginxManager.CreateSecret(name, data, nginx.JWKSecretFileMode)
 }
 
-func (cnf *Configurator) AddOrUpdateJWKSecret(secret *api_v1.Secret) {
+// AddOrUpdateJWKSecret adds a JWK secret to the filesystem or updates it if it already exists.
+func (cnf *Configurator) AddOrUpdateJWKSecret(secret *api_v1.Secret, virtualServerExes []*VirtualServerEx) error {
 	cnf.addOrUpdateJWKSecret(secret)
+
+	if len(virtualServerExes) > 0 {
+		for _, vsEx := range virtualServerExes {
+			// It is safe to ignore warnings here as no new warnings should appear when adding or updating a secret
+			_, err := cnf.addOrUpdateVirtualServer(vsEx)
+			if err != nil {
+				return fmt.Errorf("Error adding or updating VirtualServer %v/%v: %v", vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name, err)
+			}
+		}
+
+		if err := cnf.nginxManager.Reload(nginx.ReloadForOtherUpdate); err != nil {
+			return fmt.Errorf("Error when reloading NGINX when updating Secret: %v", err)
+		}
+	}
+	return nil
+}
+
+// addOrUpdateJWKSecretsForVirtualServer adds JWK secrets to the filesystem or updates them if they already exist.
+// Returns map[jwkKeyName]jwtKeyFilename
+func (cnf *Configurator) addOrUpdateJWKSecretsForVirtualServer(jwtKeys map[string]*api_v1.Secret) map[string]string {
+	if !cnf.isPlus {
+		return nil
+	}
+
+	jwkSecrets := make(map[string]string)
+
+	for jwkKeyName, jwkKey := range jwtKeys {
+		filename := cnf.addOrUpdateJWKSecret(jwkKey)
+		jwkSecrets[jwkKeyName] = filename
+	}
+
+	return jwkSecrets
 }
 
 // AddOrUpdateTLSSecret adds or updates a file with the content of the TLS secret.
diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go
index 9f396b9510..4a1c6e438f 100644
--- a/internal/configs/version2/http.go
+++ b/internal/configs/version2/http.go
@@ -66,6 +66,7 @@ type Server struct {
 	Deny                      []string
 	LimitReqOptions           LimitReqOptions
 	LimitReqs                 []LimitReq
+	JWTAuth                   *JWTAuth
 	PoliciesErrorReturn       *Return
 }
 
@@ -109,9 +110,10 @@ type Location struct {
 	InternalProxyPass        string
 	Allow                    []string
 	Deny                     []string
-	PoliciesErrorReturn      *Return
 	LimitReqOptions          LimitReqOptions
 	LimitReqs                []LimitReq
+	JWTAuth                  *JWTAuth
+	PoliciesErrorReturn      *Return
 }
 
 // ReturnLocation defines a location for returning a fixed response.
@@ -266,3 +268,10 @@ type LimitReqOptions struct {
 func (rl LimitReqOptions) String() string {
 	return fmt.Sprintf("{DryRun %v, LogLevel %q, RejectCode %q}", rl.DryRun, rl.LogLevel, rl.RejectCode)
 }
+
+// JWTAuth holds JWT authentication configuration.
+type JWTAuth struct {
+	Secret string
+	Realm  string
+	Token  string
+}
diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl
index 359e6c7d46..f0afa63224 100644
--- a/internal/configs/version2/nginx-plus.virtualserver.tmpl
+++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl
@@ -131,6 +131,11 @@ server {
         {{ if $rl.Delay }} delay={{ $rl.Delay }}{{ end }}{{ if $rl.NoDelay }} nodelay{{ end }};
     {{ end }}
 
+    {{ with $s.JWTAuth }}
+    auth_jwt "{{ .Realm }}"{{ if .Token }} token={{ .Token }}{{ end }};
+    auth_jwt_key_file {{ .Secret }};
+    {{ end }}
+
     {{ range $snippet := $s.Snippets }}
     {{- $snippet }}
     {{ end }}
@@ -220,6 +225,11 @@ server {
             {{ if $rl.Delay }} delay={{ $rl.Delay }}{{ end }}{{ if $rl.NoDelay }} nodelay{{ end }};
         {{ end }}
 
+        {{ with $l.JWTAuth }}
+        auth_jwt "{{ .Realm }}"{{ if .Token }} token={{ .Token }}{{ end }};
+        auth_jwt_key_file {{ .Secret }};
+        {{ end }}
+
         {{ range $e := $l.ErrorPages }}
         error_page {{ $e.Codes }} {{ if ne 0 $e.ResponseCode }}={{ $e.ResponseCode }}{{ end }} "{{ $e.Name }}";
         {{ end }}
diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go
index abd84fb977..f9436d5519 100644
--- a/internal/configs/version2/templates_test.go
+++ b/internal/configs/version2/templates_test.go
@@ -136,6 +136,10 @@ var virtualServerCfg = VirtualServerConfig{
 			LogLevel:   "error",
 			RejectCode: 503,
 		},
+		JWTAuth: &JWTAuth{
+			Realm:  "My Api",
+			Secret: "jwk-secret",
+		},
 		Snippets: []string{"# server snippet"},
 		InternalRedirectLocations: []InternalRedirectLocation{
 			{
diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go
index d668b818b6..33d706b653 100644
--- a/internal/configs/virtualserver.go
+++ b/internal/configs/virtualserver.go
@@ -50,6 +50,7 @@ type VirtualServerEx struct {
 	VirtualServer       *conf_v1.VirtualServer
 	Endpoints           map[string][]string
 	TLSSecret           *api_v1.Secret
+	JWTKeys             map[string]*api_v1.Secret
 	VirtualServerRoutes []*conf_v1.VirtualServerRoute
 	ExternalNameSvcs    map[string]bool
 	Policies            map[string]*conf_v1alpha1.Policy
@@ -207,11 +208,11 @@ func (vsc *virtualServerConfigurator) generateEndpointsForUpstream(owner runtime
 }
 
 // GenerateVirtualServerConfig generates a full configuration for a VirtualServer
-func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualServerEx, tlsPemFileName string) (version2.VirtualServerConfig, Warnings) {
+func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualServerEx, tlsPemFileName string, jwtKeys map[string]string) (version2.VirtualServerConfig, Warnings) {
 	vsc.clearWarnings()
 
 	policiesCfg := vsc.generatePolicies(vsEx.VirtualServer, vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Namespace,
-		vsEx.VirtualServer.Name, vsEx.VirtualServer.Spec.Policies, vsEx.Policies)
+		vsEx.VirtualServer.Name, vsEx.VirtualServer.Spec.Policies, vsEx.Policies, jwtKeys)
 
 	// crUpstreams maps an UpstreamName to its conf_v1.Upstream as they are generated
 	// necessary for generateLocation to know what Upstream each Location references
@@ -317,7 +318,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS
 
 		vsLocSnippets := r.LocationSnippets
 		routePoliciesCfg := vsc.generatePolicies(vsEx.VirtualServer, vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name,
-			r.Policies, vsEx.Policies)
+			r.Policies, vsEx.Policies, jwtKeys)
 		limitReqZones = append(limitReqZones, routePoliciesCfg.LimitReqZones...)
 
 		if len(r.Matches) > 0 {
@@ -379,11 +380,11 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS
 			}
 
 			routePoliciesCfg := vsc.generatePolicies(vsr, vsr.Namespace, vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Name,
-				r.Policies, vsEx.Policies)
+				r.Policies, vsEx.Policies, jwtKeys)
 			// use the VirtualServer route policies if the route does not define any
 			if len(r.Policies) == 0 {
 				routePoliciesCfg = vsc.generatePolicies(vsEx.VirtualServer, vsEx.VirtualServer.Namespace, vsEx.VirtualServer.Namespace,
-					vsEx.VirtualServer.Name, vsrPoliciesFromVs[vsrNamespaceName], vsEx.Policies)
+					vsEx.VirtualServer.Name, vsrPoliciesFromVs[vsrNamespaceName], vsEx.Policies, jwtKeys)
 			}
 			limitReqZones = append(limitReqZones, routePoliciesCfg.LimitReqZones...)
 
@@ -457,6 +458,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS
 			Deny:                      policiesCfg.Deny,
 			LimitReqOptions:           policiesCfg.LimitReqOptions,
 			LimitReqs:                 policiesCfg.LimitReqs,
+			JWTAuth:                   policiesCfg.JWTAuth,
 			PoliciesErrorReturn:       policiesCfg.ErrorReturn,
 		},
 		SpiffeCerts: vsc.spiffeCerts,
@@ -471,16 +473,18 @@ type policiesCfg struct {
 	LimitReqOptions version2.LimitReqOptions
 	LimitReqZones   []version2.LimitReqZone
 	LimitReqs       []version2.LimitReq
+	JWTAuth         *version2.JWTAuth
 	ErrorReturn     *version2.Return
 }
 
 func (vsc *virtualServerConfigurator) generatePolicies(owner runtime.Object, ownerNamespace string, vsNamespace string,
-	vsName string, policyRefs []conf_v1.PolicyReference, policies map[string]*conf_v1alpha1.Policy) policiesCfg {
+	vsName string, policyRefs []conf_v1.PolicyReference, policies map[string]*conf_v1alpha1.Policy, jwtKeys map[string]string) policiesCfg {
 	var policyErrorReturn *version2.Return
 	var allow, deny []string
 	var limitReqOptions version2.LimitReqOptions
 	var limitReqZones []version2.LimitReqZone
 	var limitReqs []version2.LimitReq
+	var JWTAuth *version2.JWTAuth
 	var policyError bool
 
 	for _, p := range policyRefs {
@@ -496,6 +500,7 @@ func (vsc *virtualServerConfigurator) generatePolicies(owner runtime.Object, own
 				allow = append(allow, pol.Spec.AccessControl.Allow...)
 				deny = append(deny, pol.Spec.AccessControl.Deny...)
 			}
+
 			if pol.Spec.RateLimit != nil {
 				rlZoneName := fmt.Sprintf("pol_rl_%v_%v_%v_%v", polNamespace, p.Name, vsNamespace, vsName)
 				limitReqs = append(limitReqs, generateLimitReq(rlZoneName, pol.Spec.RateLimit))
@@ -505,19 +510,39 @@ func (vsc *virtualServerConfigurator) generatePolicies(owner runtime.Object, own
 				} else {
 					curOptions := generateLimitReqOptions(pol.Spec.RateLimit)
 					if curOptions.DryRun != limitReqOptions.DryRun {
-						vsc.addWarningf(owner, "RateLimit policy %v with limit request option dryRun=%v is overridden to dryRun=%v by the first policy reference in this context",
+						vsc.addWarningf(owner, "RateLimit policy %q with limit request option dryRun=%v is overridden to dryRun=%v by the first policy reference in this context",
 							key, curOptions.DryRun, limitReqOptions.DryRun)
 					}
 					if curOptions.LogLevel != limitReqOptions.LogLevel {
-						vsc.addWarningf(owner, "RateLimit policy %v with limit request option logLevel=%v is overridden to logLevel=%v by the first policy reference in this context",
+						vsc.addWarningf(owner, "RateLimit policy %q with limit request option logLevel=%v is overridden to logLevel=%v by the first policy reference in this context",
 							key, curOptions.LogLevel, limitReqOptions.LogLevel)
 					}
 					if curOptions.RejectCode != limitReqOptions.RejectCode {
-						vsc.addWarningf(owner, "RateLimit policy %v with limit request option rejectCode=%v is overridden to rejectCode=%v by the first policy reference in this context",
+						vsc.addWarningf(owner, "RateLimit policy %q with limit request option rejectCode=%v is overridden to rejectCode=%v by the first policy reference in this context",
 							key, curOptions.RejectCode, limitReqOptions.RejectCode)
 					}
 				}
 			}
+
+			if pol.Spec.JWTAuth != nil {
+				if JWTAuth != nil {
+					vsc.addWarningf(owner, "Multiple jwt policies in the same context is not valid. JWT policy %q will be ignored", key)
+					continue
+				}
+
+				jwtSecretKey := fmt.Sprintf("%v/%v", polNamespace, pol.Spec.JWTAuth.Secret)
+				if _, existsOnFilesystem := jwtKeys[jwtSecretKey]; !existsOnFilesystem {
+					vsc.addWarningf(owner, `JWT policy %q references a JWKSecret %q which does not exist`, key, jwtSecretKey)
+					policyError = true
+					break
+				}
+
+				JWTAuth = &version2.JWTAuth{
+					Secret: jwtKeys[jwtSecretKey],
+					Realm:  pol.Spec.JWTAuth.Realm,
+					Token:  pol.Spec.JWTAuth.Token,
+				}
+			}
 		} else {
 			vsc.addWarningf(owner, "Policy %s is missing or invalid", key)
 			policyError = true
@@ -538,6 +563,7 @@ func (vsc *virtualServerConfigurator) generatePolicies(owner runtime.Object, own
 		LimitReqOptions: limitReqOptions,
 		LimitReqZones:   limitReqZones,
 		LimitReqs:       limitReqs,
+		JWTAuth:         JWTAuth,
 		ErrorReturn:     policyErrorReturn,
 	}
 }
@@ -598,6 +624,7 @@ func addPoliciesCfgToLocation(cfg policiesCfg, location *version2.Location) {
 	location.Deny = cfg.Deny
 	location.LimitReqOptions = cfg.LimitReqOptions
 	location.LimitReqs = cfg.LimitReqs
+	location.JWTAuth = cfg.JWTAuth
 	location.PoliciesErrorReturn = cfg.ErrorReturn
 }
 
diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go
index 0761f9197a..3162e95d7e 100644
--- a/internal/configs/virtualserver_test.go
+++ b/internal/configs/virtualserver_test.go
@@ -597,7 +597,8 @@ func TestGenerateVirtualServerConfig(t *testing.T) {
 	isResolverConfigured := false
 	tlsPemFileName := ""
 	vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{TLSPassthrough: true})
-	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, tlsPemFileName)
+	jwtKeys := make(map[string]string)
+	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, tlsPemFileName, jwtKeys)
 	if !reflect.DeepEqual(result, expected) {
 		t.Errorf("GenerateVirtualServerConfig returned \n%+v but expected \n%+v", result, expected)
 	}
@@ -701,7 +702,8 @@ func TestGenerateVirtualServerConfigWithSpiffeCerts(t *testing.T) {
 	tlsPemFileName := ""
 	staticConfigParams := &StaticConfigParams{TLSPassthrough: true, NginxServiceMesh: true}
 	vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, staticConfigParams)
-	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, tlsPemFileName)
+	jwtKeys := make(map[string]string)
+	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, tlsPemFileName, jwtKeys)
 	if !reflect.DeepEqual(result, expected) {
 		t.Errorf("GenerateVirtualServerConfig returned \n%+v but expected \n%+v", result, expected)
 	}
@@ -969,7 +971,8 @@ func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) {
 	isResolverConfigured := false
 	tlsPemFileName := ""
 	vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{})
-	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, tlsPemFileName)
+	jwtKeys := make(map[string]string)
+	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, tlsPemFileName, jwtKeys)
 	if !reflect.DeepEqual(result, expected) {
 		t.Errorf("GenerateVirtualServerConfig returned \n%+v but expected \n%+v", result, expected)
 	}
@@ -1270,7 +1273,8 @@ func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) {
 	isResolverConfigured := false
 	tlsPemFileName := ""
 	vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{})
-	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, tlsPemFileName)
+	jwtKeys := make(map[string]string)
+	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, tlsPemFileName, jwtKeys)
 	if !reflect.DeepEqual(result, expected) {
 		t.Errorf("GenerateVirtualServerConfig returned \n%+v but expected \n%+v", result, expected)
 	}
@@ -1741,7 +1745,8 @@ func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) {
 	isResolverConfigured := false
 	tlsPemFileName := ""
 	vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{})
-	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, tlsPemFileName)
+	jwtKeys := make(map[string]string)
+	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, tlsPemFileName, jwtKeys)
 	if !reflect.DeepEqual(result, expected) {
 		t.Errorf("GenerateVirtualServerConfig returned \n%+v but expected \n%+v", result, expected)
 	}
@@ -1760,6 +1765,7 @@ func TestGeneratePolicies(t *testing.T) {
 	tests := []struct {
 		policyRefs []conf_v1.PolicyReference
 		policies   map[string]*conf_v1alpha1.Policy
+		jwtKeys    map[string]string
 		expected   policiesCfg
 		msg        string
 	}{
@@ -1779,6 +1785,7 @@ func TestGeneratePolicies(t *testing.T) {
 					},
 				},
 			},
+			jwtKeys: nil,
 			expected: policiesCfg{
 				Allow: []string{"127.0.0.1"},
 			},
@@ -1829,6 +1836,7 @@ func TestGeneratePolicies(t *testing.T) {
 					},
 				},
 			},
+			jwtKeys: nil,
 			expected: policiesCfg{
 				Allow: []string{"127.0.0.1", "127.0.0.2"},
 			},
@@ -1853,6 +1861,7 @@ func TestGeneratePolicies(t *testing.T) {
 					},
 				},
 			},
+			jwtKeys: nil,
 			expected: policiesCfg{
 				LimitReqZones: []version2.LimitReqZone{
 					{
@@ -1905,6 +1914,7 @@ func TestGeneratePolicies(t *testing.T) {
 					},
 				},
 			},
+			jwtKeys: nil,
 			expected: policiesCfg{
 				LimitReqZones: []version2.LimitReqZone{
 					{
@@ -1935,12 +1945,40 @@ func TestGeneratePolicies(t *testing.T) {
 			},
 			msg: "multi rate limit reference",
 		},
+		{
+			policyRefs: []conf_v1.PolicyReference{
+				{
+					Name:      "jwt-policy",
+					Namespace: "default",
+				},
+			},
+			policies: map[string]*conf_v1alpha1.Policy{
+				"default/jwt-policy": {
+					Spec: conf_v1alpha1.PolicySpec{
+						JWTAuth: &conf_v1alpha1.JWTAuth{
+							Realm:  "My Test API",
+							Secret: "jwt-secret",
+						},
+					},
+				},
+			},
+			jwtKeys: map[string]string{
+				"default/jwt-secret": "/etc/nginx/secrets/default-jwt-secret",
+			},
+			expected: policiesCfg{
+				JWTAuth: &version2.JWTAuth{
+					Secret: "/etc/nginx/secrets/default-jwt-secret",
+					Realm:  "My Test API",
+				},
+			},
+			msg: "jwt reference",
+		},
 	}
 
 	vsc := newVirtualServerConfigurator(&ConfigParams{}, false, false, &StaticConfigParams{})
 
 	for _, test := range tests {
-		result := vsc.generatePolicies(owner, ownerNamespace, vsNamespace, vsName, test.policyRefs, test.policies)
+		result := vsc.generatePolicies(owner, ownerNamespace, vsNamespace, vsName, test.policyRefs, test.policies, test.jwtKeys)
 		if !reflect.DeepEqual(result, test.expected) {
 			t.Errorf("generatePolicies() returned \n%+v but expected \n%+v for the case of %s", result, test.expected,
 				test.msg)
@@ -1963,6 +2001,7 @@ func TestGeneratePoliciesFails(t *testing.T) {
 	tests := []struct {
 		policyRefs       []conf_v1.PolicyReference
 		policies         map[string]*conf_v1alpha1.Policy
+		jwtKeys          map[string]string
 		expected         policiesCfg
 		expectedWarnings Warnings
 		msg              string
@@ -1975,6 +2014,7 @@ func TestGeneratePoliciesFails(t *testing.T) {
 				},
 			},
 			policies: map[string]*conf_v1alpha1.Policy{},
+			jwtKeys:  nil,
 			expected: policiesCfg{
 				ErrorReturn: &version2.Return{
 					Code: 500,
@@ -2012,6 +2052,7 @@ func TestGeneratePoliciesFails(t *testing.T) {
 					},
 				},
 			},
+			jwtKeys: nil,
 			expected: policiesCfg{
 				Allow: []string{"127.0.0.1"},
 				Deny:  []string{"127.0.0.2"},
@@ -2057,6 +2098,7 @@ func TestGeneratePoliciesFails(t *testing.T) {
 					},
 				},
 			},
+			jwtKeys: nil,
 			expected: policiesCfg{
 				LimitReqZones: []version2.LimitReqZone{
 					{
@@ -2087,19 +2129,95 @@ func TestGeneratePoliciesFails(t *testing.T) {
 			},
 			expectedWarnings: map[runtime.Object][]string{
 				nil: {
-					"RateLimit policy default/rateLimit-policy2 with limit request option dryRun=true is overridden to dryRun=false by the first policy reference in this context",
-					"RateLimit policy default/rateLimit-policy2 with limit request option logLevel=info is overridden to logLevel=error by the first policy reference in this context",
-					"RateLimit policy default/rateLimit-policy2 with limit request option rejectCode=505 is overridden to rejectCode=503 by the first policy reference in this context",
+					`RateLimit policy "default/rateLimit-policy2" with limit request option dryRun=true is overridden to dryRun=false by the first policy reference in this context`,
+					`RateLimit policy "default/rateLimit-policy2" with limit request option logLevel=info is overridden to logLevel=error by the first policy reference in this context`,
+					`RateLimit policy "default/rateLimit-policy2" with limit request option rejectCode=505 is overridden to rejectCode=503 by the first policy reference in this context`,
 				},
 			},
 			msg: "rate limit policy limit request option override",
 		},
+		{
+			policyRefs: []conf_v1.PolicyReference{
+				{
+					Name:      "jwt-policy",
+					Namespace: "default",
+				},
+			},
+			policies: map[string]*conf_v1alpha1.Policy{
+				"default/jwt-policy": {
+					Spec: conf_v1alpha1.PolicySpec{
+						JWTAuth: &conf_v1alpha1.JWTAuth{
+							Realm:  "test",
+							Secret: "jwt-secret",
+						},
+					},
+				},
+			},
+			jwtKeys: nil,
+			expected: policiesCfg{
+				ErrorReturn: &version2.Return{
+					Code: 500,
+				},
+			},
+			expectedWarnings: map[runtime.Object][]string{
+				nil: {
+					`JWT policy "default/jwt-policy" references a JWKSecret "default/jwt-secret" which does not exist`,
+				},
+			},
+			msg: "jwt reference missing secret",
+		},
+		{
+			policyRefs: []conf_v1.PolicyReference{
+				{
+					Name:      "jwt-policy",
+					Namespace: "default",
+				},
+				{
+					Name:      "jwt-policy2",
+					Namespace: "default",
+				},
+			},
+			policies: map[string]*conf_v1alpha1.Policy{
+				"default/jwt-policy": {
+					Spec: conf_v1alpha1.PolicySpec{
+						JWTAuth: &conf_v1alpha1.JWTAuth{
+							Realm:  "test",
+							Secret: "jwt-secret",
+						},
+					},
+				},
+				"default/jwt-policy2": {
+					Spec: conf_v1alpha1.PolicySpec{
+						JWTAuth: &conf_v1alpha1.JWTAuth{
+							Realm:  "test",
+							Secret: "jwt-secret2",
+						},
+					},
+				},
+			},
+			jwtKeys: map[string]string{
+				"default/jwt-secret":  "/etc/nginx/secrets/default-jwt-secret",
+				"default/jwt-secret2": "",
+			},
+			expected: policiesCfg{
+				JWTAuth: &version2.JWTAuth{
+					Secret: "/etc/nginx/secrets/default-jwt-secret",
+					Realm:  "test",
+				},
+			},
+			expectedWarnings: map[runtime.Object][]string{
+				nil: {
+					`Multiple jwt policies in the same context is not valid. JWT policy "default/jwt-policy2" will be ignored`,
+				},
+			},
+			msg: "multi jwt reference",
+		},
 	}
 
 	for _, test := range tests {
 		vsc := newVirtualServerConfigurator(&ConfigParams{}, false, false, &StaticConfigParams{})
 
-		result := vsc.generatePolicies(owner, ownerNamespace, vsNamespace, vsName, test.policyRefs, test.policies)
+		result := vsc.generatePolicies(owner, ownerNamespace, vsNamespace, vsName, test.policyRefs, test.policies, test.jwtKeys)
 		if !reflect.DeepEqual(result, test.expected) {
 			t.Errorf("generatePolicies() returned \n%+v but expected \n%+v for the case of %s", result, test.expected,
 				test.msg)
diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go
index 68ac1abc6d..5b8034c1e1 100644
--- a/internal/k8s/controller.go
+++ b/internal/k8s/controller.go
@@ -869,7 +869,7 @@ func (lbc *LoadBalancerController) syncPolicy(task task) {
 
 	if polExists {
 		pol := obj.(*conf_v1alpha1.Policy)
-		err := validation.ValidatePolicy(pol)
+		err := validation.ValidatePolicy(pol, lbc.isNginxPlus)
 		if err != nil {
 			lbc.recorder.Eventf(pol, api_v1.EventTypeWarning, "Rejected", "Policy %v is invalid and was rejected: %v", key, err)
 		} else {
@@ -1511,15 +1511,24 @@ func (lbc *LoadBalancerController) syncSecret(task task) {
 		glog.Warningf("Failed to find Ingress resources for Secret %v: %v", key, err)
 		lbc.syncQueue.RequeueAfter(task, err, 5*time.Second)
 	}
+	glog.V(2).Infof("Found %v Ingresses with Secret %v", len(ings), key)
 
 	var virtualServers []*conf_v1.VirtualServer
+
 	if lbc.areCustomResourcesEnabled {
 		virtualServers = lbc.getVirtualServersForSecret(namespace, name)
+
+		jwkSecretPols := lbc.getPoliciesForSecret(namespace, name)
+		for _, pol := range jwkSecretPols {
+			vsList := lbc.getVirtualServersForPolicy(pol.Namespace, pol.Name)
+			virtualServers = append(virtualServers, vsList...)
+		}
+
+		virtualServers = removeDuplicateVirtualServers(virtualServers)
+
 		glog.V(2).Infof("Found %v VirtualServers with Secret %v", len(virtualServers), key)
 	}
 
-	glog.V(2).Infof("Found %v Ingresses with Secret %v", len(ings), key)
-
 	if !secrExists {
 		glog.V(2).Infof("Deleting Secret: %v\n", key)
 
@@ -1544,6 +1553,20 @@ func (lbc *LoadBalancerController) syncSecret(task task) {
 	}
 }
 
+func removeDuplicateVirtualServers(virtualServers []*conf_v1.VirtualServer) []*conf_v1.VirtualServer {
+	encountered := make(map[string]bool)
+	var uniqueVirtualServers []*conf_v1.VirtualServer
+	for _, vs := range virtualServers {
+		vsKey := fmt.Sprintf("%v/%v", vs.Namespace, vs.Name)
+		if !encountered[vsKey] {
+			encountered[vsKey] = true
+			uniqueVirtualServers = append(uniqueVirtualServers, vs)
+		}
+	}
+
+	return uniqueVirtualServers
+}
+
 func (lbc *LoadBalancerController) isSpecialSecret(secretName string) bool {
 	return secretName == lbc.defaultServerSecret || secretName == lbc.wildcardTLSSecret
 }
@@ -1604,7 +1627,18 @@ func (lbc *LoadBalancerController) handleSecretUpdate(secret *api_v1.Secret, ing
 	kind, _ := GetSecretKind(secret)
 
 	if kind == JWK {
-		lbc.configurator.AddOrUpdateJWKSecret(secret)
+		virtualServerExes := lbc.virtualServersToVirtualServerExes(virtualServers)
+
+		err := lbc.configurator.AddOrUpdateJWKSecret(secret, virtualServerExes)
+		if err != nil {
+			glog.Errorf("Error when updating Secret %v: %v", secretNsName, err)
+			lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "UpdatedWithError", "%v was updated, but not applied: %v", secretNsName, err)
+
+			eventType = api_v1.EventTypeWarning
+			title = "UpdatedWithError"
+			message = fmt.Sprintf("Configuration was updated due to updated secret %v, but not applied: %v", secretNsName, err)
+			state = conf_v1.StateInvalid
+		}
 	} else {
 		regular, mergeable := lbc.createIngresses(ings)
 
@@ -2282,7 +2316,7 @@ func getIPAddressesFromEndpoints(endpoints []podEndpoint) []string {
 	return endps
 }
 
-func (lbc *LoadBalancerController) getAndValidateSecret(secretKey string) (*api_v1.Secret, error) {
+func (lbc *LoadBalancerController) getSecret(secretKey string) (*api_v1.Secret, error) {
 	secretObject, secretExists, err := lbc.secretLister.GetByKey(secretKey)
 	if err != nil {
 		return nil, fmt.Errorf("error retrieving secret %v", secretKey)
@@ -2291,6 +2325,14 @@ func (lbc *LoadBalancerController) getAndValidateSecret(secretKey string) (*api_
 		return nil, fmt.Errorf("secret %v not found", secretKey)
 	}
 	secret := secretObject.(*api_v1.Secret)
+	return secret, nil
+}
+
+func (lbc *LoadBalancerController) getAndValidateSecret(secretKey string) (*api_v1.Secret, error) {
+	secret, err := lbc.getSecret(secretKey)
+	if err != nil {
+		return nil, err
+	}
 
 	err = ValidateTLSSecret(secret)
 	if err != nil {
@@ -2299,6 +2341,19 @@ func (lbc *LoadBalancerController) getAndValidateSecret(secretKey string) (*api_
 	return secret, nil
 }
 
+func (lbc *LoadBalancerController) getAndValidateJWTSecret(secretKey string) (*api_v1.Secret, error) {
+	secret, err := lbc.getSecret(secretKey)
+	if err != nil {
+		return nil, err
+	}
+
+	err = ValidateJWKSecret(secret)
+	if err != nil {
+		return nil, fmt.Errorf("error validating secret %v", secretKey)
+	}
+	return secret, nil
+}
+
 func (lbc *LoadBalancerController) createIngress(ing *networking.Ingress) (*configs.IngressEx, error) {
 	ingEx := &configs.IngressEx{
 		Ingress: ing,
@@ -2540,6 +2595,7 @@ func newVirtualServerRouteErrorFromVSR(virtualServerRoute *conf_v1.VirtualServer
 func (lbc *LoadBalancerController) createVirtualServer(virtualServer *conf_v1.VirtualServer) (*configs.VirtualServerEx, []virtualServerRouteError) {
 	virtualServerEx := configs.VirtualServerEx{
 		VirtualServer: virtualServer,
+		JWTKeys:       make(map[string]*api_v1.Secret),
 	}
 
 	if virtualServer.Spec.TLS != nil && virtualServer.Spec.TLS.Secret != "" {
@@ -2557,6 +2613,11 @@ func (lbc *LoadBalancerController) createVirtualServer(virtualServer *conf_v1.Vi
 		glog.Warningf("Error getting policy for VirtualServer %s/%s: %v", virtualServer.Namespace, virtualServer.Name, err)
 	}
 
+	err := lbc.addJWTSecrets(policies, virtualServerEx.JWTKeys)
+	if err != nil {
+		glog.Warningf("Error getting JWT secrets for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err)
+	}
+
 	endpoints := make(map[string][]string)
 	externalNameSvcs := make(map[string]bool)
 	podsByIP := make(map[string]configs.PodInfo)
@@ -2605,6 +2666,11 @@ func (lbc *LoadBalancerController) createVirtualServer(virtualServer *conf_v1.Vi
 		}
 		policies = append(policies, vsRoutePolicies...)
 
+		err = lbc.addJWTSecrets(vsRoutePolicies, virtualServerEx.JWTKeys)
+		if err != nil {
+			glog.Warningf("Error getting JWT secrets for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err)
+		}
+
 		if r.Route == "" {
 			continue
 		}
@@ -2652,6 +2718,11 @@ func (lbc *LoadBalancerController) createVirtualServer(virtualServer *conf_v1.Vi
 				glog.Warningf("Error getting policy for VirtualServerRoute %s/%s: %v", vsr.Namespace, vsr.Name, err)
 			}
 			policies = append(policies, vsrSubroutePolicies...)
+
+			err = lbc.addJWTSecrets(vsrSubroutePolicies, virtualServerEx.JWTKeys)
+			if err != nil {
+				glog.Warningf("Error getting JWT secrets for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err)
+			}
 		}
 
 		for _, u := range vsr.Spec.Upstreams {
@@ -2707,14 +2778,32 @@ func createPolicyMap(policies []*conf_v1alpha1.Policy) map[string]*conf_v1alpha1
 	return result
 }
 
-func (lbc *LoadBalancerController) getPolicies(policies []conf_v1.PolicyReference, defaultNamespace string) ([]*conf_v1alpha1.Policy, []error) {
+func (lbc *LoadBalancerController) getAllPolicies() []*conf_v1alpha1.Policy {
+	var policies []*conf_v1alpha1.Policy
+
+	for _, obj := range lbc.policyLister.List() {
+		pol := obj.(*conf_v1alpha1.Policy)
+
+		err := validation.ValidatePolicy(pol, lbc.isNginxPlus)
+		if err != nil {
+			glog.V(3).Infof("Skipping invalid Policy %s/%s: %v", pol.Namespace, pol.Name, err)
+			continue
+		}
+
+		policies = append(policies, pol)
+	}
+
+	return policies
+}
+
+func (lbc *LoadBalancerController) getPolicies(policies []conf_v1.PolicyReference, ownerNamespace string) ([]*conf_v1alpha1.Policy, []error) {
 	var result []*conf_v1alpha1.Policy
 	var errors []error
 
 	for _, p := range policies {
 		polNamespace := p.Namespace
 		if polNamespace == "" {
-			polNamespace = defaultNamespace
+			polNamespace = ownerNamespace
 		}
 
 		policyKey := fmt.Sprintf("%s/%s", polNamespace, p.Name)
@@ -2732,7 +2821,7 @@ func (lbc *LoadBalancerController) getPolicies(policies []conf_v1.PolicyReferenc
 
 		policy := policyObj.(*conf_v1alpha1.Policy)
 
-		err = validation.ValidatePolicy(policy)
+		err = validation.ValidatePolicy(policy, lbc.isNginxPlus)
 		if err != nil {
 			errors = append(errors, fmt.Errorf("Policy %s is invalid: %v", policyKey, err))
 			continue
@@ -2744,6 +2833,42 @@ func (lbc *LoadBalancerController) getPolicies(policies []conf_v1.PolicyReferenc
 	return result, errors
 }
 
+func (lbc *LoadBalancerController) addJWTSecrets(policies []*conf_v1alpha1.Policy, jwtKeys map[string]*api_v1.Secret) error {
+	for _, pol := range policies {
+		if pol.Spec.JWTAuth == nil {
+			continue
+		}
+		secretKey := fmt.Sprintf("%v/%v", pol.Namespace, pol.Spec.JWTAuth.Secret)
+		secret, err := lbc.getAndValidateJWTSecret(secretKey)
+		if err != nil {
+			return fmt.Errorf("Error getting or validating the JWT secret %v for the policy %v/%v: %v", secretKey, pol.Namespace, pol.Name, err)
+		}
+		jwtKeys[secretKey] = secret
+	}
+
+	return nil
+}
+
+func (lbc *LoadBalancerController) getPoliciesForSecret(secretNamespace string, secretName string) []*conf_v1alpha1.Policy {
+	return findPoliciesForSecret(lbc.getAllPolicies(), secretNamespace, secretName)
+}
+
+func findPoliciesForSecret(policies []*conf_v1alpha1.Policy, secretNamespace string, secretName string) []*conf_v1alpha1.Policy {
+	var res []*conf_v1alpha1.Policy
+
+	for _, pol := range policies {
+		if pol.Spec.JWTAuth == nil {
+			continue
+		}
+
+		if pol.Spec.JWTAuth.Secret == secretName && pol.Namespace == secretNamespace {
+			res = append(res, pol)
+		}
+	}
+
+	return res
+}
+
 func (lbc *LoadBalancerController) createTransportServer(transportServer *conf_v1alpha1.TransportServer) *configs.TransportServerEx {
 	endpoints := make(map[string][]string)
 
diff --git a/internal/k8s/controller_test.go b/internal/k8s/controller_test.go
index 9aff71452b..b1ceeaac82 100644
--- a/internal/k8s/controller_test.go
+++ b/internal/k8s/controller_test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/nginxinc/kubernetes-ingress/internal/metrics/collectors"
 	"github.com/nginxinc/kubernetes-ingress/internal/nginx"
 	conf_v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1"
+	"github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1alpha1"
 	conf_v1alpha1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1alpha1"
 	v1 "k8s.io/api/core/v1"
 	networking "k8s.io/api/networking/v1beta1"
@@ -2139,6 +2140,7 @@ func TestGetPolicies(t *testing.T) {
 	}
 
 	lbc := LoadBalancerController{
+		isNginxPlus: true,
 		policyLister: &cache.FakeCustomStore{
 			GetByKeyFunc: func(key string) (item interface{}, exists bool, err error) {
 				switch key {
@@ -2176,7 +2178,7 @@ func TestGetPolicies(t *testing.T) {
 
 	expectedPolicies := []*conf_v1alpha1.Policy{validPolicy}
 	expectedErrors := []error{
-		errors.New("Policy default/invalid-policy is invalid: spec: Invalid value: \"\": must specify exactly one of: `accessControl`, `rateLimit`"),
+		errors.New("Policy default/invalid-policy is invalid: spec: Invalid value: \"\": must specify exactly one of: `accessControl`, `rateLimit`, `jwt`"),
 		errors.New("Policy nginx-ingress/valid-policy doesn't exist"),
 		errors.New("Failed to get policy nginx-ingress/some-policy: GetByKey error"),
 	}
@@ -2350,3 +2352,297 @@ func policyMapToString(policies map[string]*conf_v1alpha1.Policy) string {
 
 	return b.String()
 }
+
+func TestRemoveDuplicateVirtualServers(t *testing.T) {
+	tests := []struct {
+		virtualServers []*conf_v1.VirtualServer
+		expected       []*conf_v1.VirtualServer
+	}{
+		{
+			[]*conf_v1.VirtualServer{
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-1",
+						Namespace: "ns-1",
+					},
+				},
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-2",
+						Namespace: "ns-1",
+					},
+				},
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-2",
+						Namespace: "ns-1",
+					},
+				},
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-3",
+						Namespace: "ns-1",
+					},
+				},
+			},
+			[]*conf_v1.VirtualServer{
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-1",
+						Namespace: "ns-1",
+					},
+				},
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-2",
+						Namespace: "ns-1",
+					},
+				},
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-3",
+						Namespace: "ns-1",
+					},
+				},
+			},
+		},
+		{
+			[]*conf_v1.VirtualServer{
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-3",
+						Namespace: "ns-2",
+					},
+				},
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-3",
+						Namespace: "ns-1",
+					},
+				},
+			},
+			[]*conf_v1.VirtualServer{
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-3",
+						Namespace: "ns-2",
+					},
+				},
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "vs-3",
+						Namespace: "ns-1",
+					},
+				},
+			},
+		},
+	}
+	for _, test := range tests {
+		result := removeDuplicateVirtualServers(test.virtualServers)
+		if !reflect.DeepEqual(result, test.expected) {
+			t.Errorf("removeDuplicateVirtualServers() returned \n%v but expected \n%v", result, test.expected)
+		}
+	}
+}
+
+func TestFindPoliciesForSecret(t *testing.T) {
+	jwtPol1 := &conf_v1alpha1.Policy{
+		ObjectMeta: meta_v1.ObjectMeta{
+			Name:      "jwt-policy",
+			Namespace: "default",
+		},
+		Spec: conf_v1alpha1.PolicySpec{
+			JWTAuth: &conf_v1alpha1.JWTAuth{
+				Secret: "jwk-secret",
+			},
+		},
+	}
+
+	jwtPol2 := &conf_v1alpha1.Policy{
+		ObjectMeta: meta_v1.ObjectMeta{
+			Name:      "jwt-policy",
+			Namespace: "ns-1",
+		},
+		Spec: conf_v1alpha1.PolicySpec{
+			JWTAuth: &conf_v1alpha1.JWTAuth{
+				Secret: "jwk-secret",
+			},
+		},
+	}
+
+	tests := []struct {
+		policies        []*conf_v1alpha1.Policy
+		secretNamespace string
+		secretName      string
+		expected        []*conf_v1alpha1.Policy
+		msg             string
+	}{
+		{
+			policies:        []*conf_v1alpha1.Policy{jwtPol1},
+			secretNamespace: "default",
+			secretName:      "jwk-secret",
+			expected:        []*v1alpha1.Policy{jwtPol1},
+			msg:             "Find policy in default ns",
+		},
+		{
+			policies:        []*conf_v1alpha1.Policy{jwtPol2},
+			secretNamespace: "default",
+			secretName:      "jwk-secret",
+			expected:        nil,
+			msg:             "Ignore policies in other namespaces",
+		},
+		{
+			policies:        []*conf_v1alpha1.Policy{jwtPol1, jwtPol2},
+			secretNamespace: "default",
+			secretName:      "jwk-secret",
+			expected:        []*v1alpha1.Policy{jwtPol1},
+			msg:             "Find policy in default ns, ignore other",
+		},
+	}
+	for _, test := range tests {
+		result := findPoliciesForSecret(test.policies, test.secretNamespace, test.secretName)
+		if !reflect.DeepEqual(result, test.expected) {
+			t.Errorf("findPoliciesForSecret() returned \n%v but expected \n%v for the case of %s", result, test.expected, test.msg)
+		}
+	}
+}
+
+func TestAddJWTSecrets(t *testing.T) {
+	validSecret := &v1.Secret{
+		ObjectMeta: meta_v1.ObjectMeta{
+			Name:      "valid-jwk-secret",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{"jwk": nil},
+	}
+
+	invalidSecret := &v1.Secret{
+		ObjectMeta: meta_v1.ObjectMeta{
+			Name:      "invalid-jwk-secret",
+			Namespace: "default",
+		},
+		Data: nil,
+	}
+
+	tests := []struct {
+		policies        []*conf_v1alpha1.Policy
+		expectedJWTKeys map[string]*v1.Secret
+		wantErr         bool
+		msg             string
+	}{
+		{
+			policies: []*conf_v1alpha1.Policy{
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "jwt-policy",
+						Namespace: "default",
+					},
+					Spec: conf_v1alpha1.PolicySpec{
+						JWTAuth: &conf_v1alpha1.JWTAuth{
+							Secret: "valid-jwk-secret",
+							Realm:  "My API",
+						},
+					},
+				},
+			},
+			expectedJWTKeys: map[string]*v1.Secret{
+				"default/valid-jwk-secret": validSecret,
+			},
+			wantErr: false,
+			msg:     "test getting valid secret",
+		},
+		{
+			policies:        []*conf_v1alpha1.Policy{},
+			expectedJWTKeys: map[string]*v1.Secret{},
+			wantErr:         false,
+			msg:             "test getting valid secret with no policy",
+		},
+		{
+			policies: []*conf_v1alpha1.Policy{
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "jwt-policy",
+						Namespace: "default",
+					},
+					Spec: conf_v1alpha1.PolicySpec{
+						AccessControl: &conf_v1alpha1.AccessControl{
+							Allow: []string{"127.0.0.1"},
+						},
+					},
+				},
+			},
+			expectedJWTKeys: map[string]*v1.Secret{},
+			wantErr:         false,
+			msg:             "test getting valid secret with wrong policy",
+		},
+		{
+			policies: []*conf_v1alpha1.Policy{
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "jwt-policy",
+						Namespace: "default",
+					},
+					Spec: conf_v1alpha1.PolicySpec{
+						JWTAuth: &conf_v1alpha1.JWTAuth{
+							Secret: "non-existing-jwk-secret",
+							Realm:  "My API",
+						},
+					},
+				},
+			},
+			expectedJWTKeys: map[string]*v1.Secret{},
+			wantErr:         true,
+			msg:             "test getting secret that does not exist",
+		},
+		{
+			policies: []*conf_v1alpha1.Policy{
+				{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "jwt-policy",
+						Namespace: "default",
+					},
+					Spec: conf_v1alpha1.PolicySpec{
+						JWTAuth: &conf_v1alpha1.JWTAuth{
+							Secret: "invalid-jwk-secret",
+							Realm:  "My API",
+						},
+					},
+				},
+			},
+			expectedJWTKeys: map[string]*v1.Secret{},
+			wantErr:         true,
+			msg:             "test getting invalid secret",
+		},
+	}
+
+	for _, test := range tests {
+		lbc := LoadBalancerController{
+			secretLister: storeToSecretLister{
+				&cache.FakeCustomStore{
+					GetByKeyFunc: func(key string) (item interface{}, exists bool, err error) {
+						switch key {
+						case "default/valid-jwk-secret":
+							return validSecret, true, nil
+						case "default/invalid-jwk-secret":
+							return invalidSecret, true, errors.New("secret is missing jwk key in data")
+						default:
+							return nil, false, errors.New("GetByKey error")
+						}
+					},
+				},
+			},
+		}
+
+		jwtKeys := make(map[string]*v1.Secret)
+
+		err := lbc.addJWTSecrets(test.policies, jwtKeys)
+		if (err != nil) != test.wantErr {
+			t.Errorf("addJWTSecrets() returned %v, for the case of %v", err, test.msg)
+		}
+
+		if !reflect.DeepEqual(jwtKeys, test.expectedJWTKeys) {
+			t.Errorf("addJWTSecrets() returned \n%+v but expected \n%+v", jwtKeys, test.expectedJWTKeys)
+		}
+
+	}
+}
diff --git a/pkg/apis/configuration/v1alpha1/types.go b/pkg/apis/configuration/v1alpha1/types.go
index 1f04c6d776..9ea531830d 100644
--- a/pkg/apis/configuration/v1alpha1/types.go
+++ b/pkg/apis/configuration/v1alpha1/types.go
@@ -118,6 +118,7 @@ type Policy struct {
 type PolicySpec struct {
 	AccessControl *AccessControl `json:"accessControl"`
 	RateLimit     *RateLimit     `json:"rateLimit"`
+	JWTAuth       *JWTAuth       `json:"jwt"`
 }
 
 // AccessControl defines an access policy based on the source IP of a request.
@@ -139,6 +140,13 @@ type RateLimit struct {
 	RejectCode *int   `json:"rejectCode"`
 }
 
+// JWTAuth holds JWT authentication configuration.
+type JWTAuth struct {
+	Realm  string `json:"realm"`
+	Secret string `json:"secret"`
+	Token  string `json:"token"`
+}
+
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 
 // PolicyList is a list of the Policy resources.
diff --git a/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go
index b6a56ea675..39e1b1a72b 100644
--- a/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/configuration/v1alpha1/zz_generated.deepcopy.go
@@ -131,6 +131,22 @@ func (in *GlobalConfigurationSpec) DeepCopy() *GlobalConfigurationSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *JWTAuth) DeepCopyInto(out *JWTAuth) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuth.
+func (in *JWTAuth) DeepCopy() *JWTAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(JWTAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Listener) DeepCopyInto(out *Listener) {
 	*out = *in
@@ -220,6 +236,11 @@ func (in *PolicySpec) DeepCopyInto(out *PolicySpec) {
 		*out = new(RateLimit)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.JWTAuth != nil {
+		in, out := &in.JWTAuth, &out.JWTAuth
+		*out = new(JWTAuth)
+		**out = **in
+	}
 	return
 }
 
diff --git a/pkg/apis/configuration/validation/common.go b/pkg/apis/configuration/validation/common.go
index e38a54a024..a614177785 100644
--- a/pkg/apis/configuration/validation/common.go
+++ b/pkg/apis/configuration/validation/common.go
@@ -117,6 +117,22 @@ func validateSize(size string, fieldPath *field.Path) field.ErrorList {
 	return allErrs
 }
 
+// validateSecretName checks if a secret name is valid.
+// It performs the same validation as ValidateSecretName from k8s.io/kubernetes/pkg/apis/core/validation/validation.go.
+func validateSecretName(name string, fieldPath *field.Path) field.ErrorList {
+	allErrs := field.ErrorList{}
+
+	if name == "" {
+		return allErrs
+	}
+
+	for _, msg := range validation.IsDNS1123Subdomain(name) {
+		allErrs = append(allErrs, field.Invalid(fieldPath, name, msg))
+	}
+
+	return allErrs
+}
+
 func mapToPrettyString(m map[string]bool) string {
 	var out []string
 
diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go
index af18ac3185..d55f182de9 100644
--- a/pkg/apis/configuration/validation/policy.go
+++ b/pkg/apis/configuration/validation/policy.go
@@ -13,12 +13,12 @@ import (
 )
 
 // ValidatePolicy validates a Policy.
-func ValidatePolicy(policy *v1alpha1.Policy) error {
-	allErrs := validatePolicySpec(&policy.Spec, field.NewPath("spec"))
+func ValidatePolicy(policy *v1alpha1.Policy, isPlus bool) error {
+	allErrs := validatePolicySpec(&policy.Spec, field.NewPath("spec"), isPlus)
 	return allErrs.ToAggregate()
 }
 
-func validatePolicySpec(spec *v1alpha1.PolicySpec, fieldPath *field.Path) field.ErrorList {
+func validatePolicySpec(spec *v1alpha1.PolicySpec, fieldPath *field.Path, isPlus bool) field.ErrorList {
 	allErrs := field.ErrorList{}
 
 	fieldCount := 0
@@ -33,8 +33,22 @@ func validatePolicySpec(spec *v1alpha1.PolicySpec, fieldPath *field.Path) field.
 		fieldCount++
 	}
 
+	if spec.JWTAuth != nil {
+		if !isPlus {
+			return append(allErrs, field.Forbidden(fieldPath.Child("jwt"), "jwt secrets are only supported in NGINX Plus"))
+		}
+
+		allErrs = append(allErrs, validateJWT(spec.JWTAuth, fieldPath.Child("jwt"))...)
+		fieldCount++
+	}
+
 	if fieldCount != 1 {
-		allErrs = append(allErrs, field.Invalid(fieldPath, "", "must specify exactly one of: `accessControl`, `rateLimit`"))
+		msg := "must specify exactly one of: `accessControl`, `rateLimit`"
+		if isPlus {
+			msg = fmt.Sprint(msg, ", `jwt`")
+		}
+
+		allErrs = append(allErrs, field.Invalid(fieldPath, "", msg))
 	}
 
 	return allErrs
@@ -95,6 +109,21 @@ func validateRateLimit(rateLimit *v1alpha1.RateLimit, fieldPath *field.Path) fie
 	return allErrs
 }
 
+func validateJWT(jwt *v1alpha1.JWTAuth, fieldPath *field.Path) field.ErrorList {
+	allErrs := field.ErrorList{}
+
+	allErrs = append(allErrs, validateJWTRealm(jwt.Realm, fieldPath.Child("realm"))...)
+
+	if jwt.Secret == "" {
+		return append(allErrs, field.Required(fieldPath.Child("secret"), ""))
+	}
+	allErrs = append(allErrs, validateSecretName(jwt.Secret, fieldPath.Child("secret"))...)
+
+	allErrs = append(allErrs, validateJWTToken(jwt.Token, fieldPath.Child("token"))...)
+
+	return allErrs
+}
+
 const rateFmt = `[1-9]\d*r/[sSmM]`
 const rateErrMsg = "must consist of numeric characters followed by a valid rate suffix. 'r/s|r/m"
 
@@ -138,7 +167,7 @@ func validateRateLimitZoneSize(zoneSize string, fieldPath *field.Path) field.Err
 
 var rateLimitKeySpecialVariables = []string{"arg_", "http_", "cookie_"}
 
-// rateLimitVariables includes NGINX variables allowed to be used in a rateLimit policy key.
+// rateLimitKeyVariables includes NGINX variables allowed to be used in a rateLimit policy key.
 var rateLimitKeyVariables = map[string]bool{
 	"binary_remote_addr": true,
 	"request_uri":        true,
@@ -163,6 +192,38 @@ func validateRateLimitKey(key string, fieldPath *field.Path) field.ErrorList {
 	return allErrs
 }
 
+var jwtTokenSpecialVariables = []string{"arg_", "http_", "cookie_"}
+
+func validateJWTToken(token string, fieldPath *field.Path) field.ErrorList {
+	allErrs := field.ErrorList{}
+
+	if token == "" {
+		return allErrs
+	}
+
+	nginxVars := strings.Split(token, "$")
+	if len(nginxVars) != 2 {
+		return append(allErrs, field.Invalid(fieldPath, token, "must have 1 var"))
+	}
+	nVar := token[1:]
+
+	special := false
+	for _, specialVar := range jwtTokenSpecialVariables {
+		if strings.HasPrefix(nVar, specialVar) {
+			special = true
+			break
+		}
+	}
+
+	if special {
+		allErrs = append(allErrs, validateSpecialVariable(nVar, fieldPath)...)
+	} else {
+		return append(allErrs, field.Invalid(fieldPath, token, "must only have special vars"))
+	}
+
+	return allErrs
+}
+
 var validLogLevels = map[string]bool{
 	"info":   true,
 	"notice": true,
@@ -181,6 +242,26 @@ func validateRateLimitLogLevel(logLevel string, fieldPath *field.Path) field.Err
 	return allErrs
 }
 
+const jwtRealmFmt = `([^"$\\]|\\[^$])*`
+const jwtRealmFmtErrMsg string = `a valid realm must have all '"' escaped and must not contain any '$' or end with an unescaped '\'`
+
+var jwtRealmFmtRegexp = regexp.MustCompile("^" + jwtRealmFmt + "$")
+
+func validateJWTRealm(realm string, fieldPath *field.Path) field.ErrorList {
+	allErrs := field.ErrorList{}
+
+	if realm == "" {
+		return append(allErrs, field.Required(fieldPath, ""))
+	}
+
+	if !jwtRealmFmtRegexp.MatchString(realm) {
+		msg := validation.RegexError(jwtRealmFmtErrMsg, jwtRealmFmt, "MyAPI", "My Product API")
+		allErrs = append(allErrs, field.Invalid(fieldPath, realm, msg))
+	}
+
+	return allErrs
+}
+
 func validateIPorCIDR(ipOrCIDR string, fieldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
 
diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go
index f0addee9d9..8b1f5fb313 100644
--- a/pkg/apis/configuration/validation/policy_test.go
+++ b/pkg/apis/configuration/validation/policy_test.go
@@ -15,8 +15,9 @@ func TestValidatePolicy(t *testing.T) {
 			},
 		},
 	}
+	isPlus := false
 
-	err := ValidatePolicy(policy)
+	err := ValidatePolicy(policy, isPlus)
 	if err != nil {
 		t.Errorf("ValidatePolicy() returned error %v for valid input", err)
 	}
@@ -26,8 +27,9 @@ func TestValidatePolicyFails(t *testing.T) {
 	policy := &v1alpha1.Policy{
 		Spec: v1alpha1.PolicySpec{},
 	}
+	isPlus := false
 
-	err := ValidatePolicy(policy)
+	err := ValidatePolicy(policy, isPlus)
 	if err == nil {
 		t.Errorf("ValidatePolicy() returned no error for invalid input")
 	}
@@ -45,7 +47,7 @@ func TestValidatePolicyFails(t *testing.T) {
 		},
 	}
 
-	err = ValidatePolicy(multiPolicy)
+	err = ValidatePolicy(multiPolicy, isPlus)
 	if err == nil {
 		t.Errorf("ValidatePolicy() returned no error for invalid input")
 	}
@@ -221,6 +223,97 @@ func TestValidateRateLimitFails(t *testing.T) {
 	}
 }
 
+func TestValidateJWT(t *testing.T) {
+	tests := []struct {
+		jwt *v1alpha1.JWTAuth
+		msg string
+	}{
+		{
+			jwt: &v1alpha1.JWTAuth{
+				Realm:  "My Product API",
+				Secret: "my-jwk",
+			},
+			msg: "basic",
+		},
+		{
+			jwt: &v1alpha1.JWTAuth{
+				Realm:  "My Product API",
+				Secret: "my-jwk",
+				Token:  "$cookie_auth_token",
+			},
+			msg: "jwt with token",
+		},
+	}
+	for _, test := range tests {
+		allErrs := validateJWT(test.jwt, field.NewPath("jwt"))
+		if len(allErrs) != 0 {
+			t.Errorf("validateJWT() returned errors %v for valid input for the case of %v", allErrs, test.msg)
+		}
+	}
+}
+
+func TestValidateJWTFails(t *testing.T) {
+	tests := []struct {
+		msg string
+		jwt *v1alpha1.JWTAuth
+	}{
+		{
+			jwt: &v1alpha1.JWTAuth{
+				Realm: "My Product API",
+			},
+			msg: "missing secret",
+		},
+		{
+			jwt: &v1alpha1.JWTAuth{
+				Secret: "my-jwk",
+			},
+			msg: "missing realm",
+		},
+		{
+			jwt: &v1alpha1.JWTAuth{
+				Realm:  "My Product API",
+				Secret: "my-jwk",
+				Token:  "$uri",
+			},
+			msg: "invalid variable use in token",
+		},
+		{
+			jwt: &v1alpha1.JWTAuth{
+				Realm:  "My Product API",
+				Secret: "my-\"jwk",
+			},
+			msg: "invalid secret name",
+		},
+		{
+			jwt: &v1alpha1.JWTAuth{
+				Realm:  "My \"Product API",
+				Secret: "my-jwk",
+			},
+			msg: "invalid realm due to escaped string",
+		},
+		{
+			jwt: &v1alpha1.JWTAuth{
+				Realm:  "My Product ${api}",
+				Secret: "my-jwk",
+			},
+			msg: "invalid variable use in realm with curly braces",
+		},
+		{
+			jwt: &v1alpha1.JWTAuth{
+				Realm:  "My Product $api",
+				Secret: "my-jwk",
+			},
+			msg: "invalid variable use in realm without curly braces",
+		},
+	}
+	for _, test := range tests {
+		allErrs := validateJWT(test.jwt, field.NewPath("jwt"))
+		if len(allErrs) == 0 {
+			t.Errorf("validateJWT() returned no errors for invalid input for the case of %v", test.msg)
+		}
+	}
+}
+
 func TestValidateIPorCIDR(t *testing.T) {
 	validInput := []string{
 		"192.168.1.1",
@@ -339,3 +432,65 @@ func TestValidateRateLimitLogLevel(t *testing.T) {
 		}
 	}
 }
+
+func TestValidateJWTToken(t *testing.T) {
+	validTests := []struct {
+		token string
+		msg   string
+	}{
+		{
+			token: "",
+			msg:   "no token set",
+		},
+		{
+			token: "$http_token",
+			msg:   "http special variable usage",
+		},
+		{
+			token: "$arg_token",
+			msg:   "arg special variable usage",
+		},
+		{
+			token: "$cookie_token",
+			msg:   "cookie special variable usage",
+		},
+	}
+	for _, test := range validTests {
+		allErrs := validateJWTToken(test.token, field.NewPath("token"))
+		if len(allErrs) != 0 {
+			t.Errorf("validateJWTToken(%v) returned an error for valid input for the case of %v", test.token, test.msg)
+		}
+	}
+
+	invalidTests := []struct {
+		token string
+		msg   string
+	}{
+		{
+			token: "http_token",
+			msg:   "missing $ prefix",
+		},
+		{
+			token: "${http_token}",
+			msg:   "usage of $ and curly braces",
+		},
+		{
+			token: "$http_token$http_token",
+			msg:   "multi variable usage",
+		},
+		{
+			token: "something$http_token",
+			msg:   "non variable usage",
+		},
+		{
+			token: "$uri",
+			msg:   "non special variable usage",
+		},
+	}
+	for _, test := range invalidTests {
+		allErrs := validateJWTToken(test.token, field.NewPath("token"))
+		if len(allErrs) == 0 {
+			t.Errorf("validateJWTToken(%v) didn't return error for invalid input for the case of %v", test.token, test.msg)
+		}
+	}
+}
diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go
index 507317f8a4..d8c8a7749b 100644
--- a/pkg/apis/configuration/validation/virtualserver.go
+++ b/pkg/apis/configuration/validation/virtualserver.go
@@ -417,22 +417,6 @@ func isValidHeaderValue(s string) []string {
 	return nil
 }
 
-// validateSecretName checks if a secret name is valid.
-// It performs the same validation as ValidateSecretName from k8s.io/kubernetes/pkg/apis/core/validation/validation.go.
-func validateSecretName(name string, fieldPath *field.Path) field.ErrorList {
-	allErrs := field.ErrorList{}
-
-	if name == "" {
-		return allErrs
-	}
-
-	for _, msg := range validation.IsDNS1123Subdomain(name) {
-		allErrs = append(allErrs, field.Invalid(fieldPath, name, msg))
-	}
-
-	return allErrs
-}
-
 func validateUpstreams(upstreams []v1.Upstream, fieldPath *field.Path, isPlus bool) (allErrs field.ErrorList, upstreamNames sets.String) {
 	allErrs = field.ErrorList{}
 	upstreamNames = sets.String{}