Repository: envoyproxy/gateway
Pull request #4102 (2024-08-26 02:47:32)

impl for setting OIDC token cookie domain
This PR introduces the ability to set a domain for OAuth2 filter token cookies. By configuring the domain, these token cookies can be shared across multiple subdomains, enabling users to remain logged in while navigating between mulitple subdomains without the need for repeated logins.

```yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: oidc-example
  namespace: default
spec:
  oidc:
    clientID: 758469837151-2tchi5dn2sh7o5kcc3m974nsfdvjlfpr.apps.googleusercontent.com
    clientSecret:
      group: ""
      kind: Secret
      name: my-app-client-secret
    cookieDomain: example.com
    logoutPath: /myapp/logout
    provider:
      issuer: https://accounts.google.com
    redirectURL: https://www.example.com:8443/myapp/oauth2/callback
  targetRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: eg
```

Related issue: #3919
Envoy PRs: https://github.com/envoyproxy/envoy/pull/35491 https://github.com/envoyproxy/envoy/pull/35845

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/4102?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
Attention: Patch coverage is `33.33333%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 67.97%. Comparing base [(`8ae9b09`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/8ae9b09398c692fb9af36f61e1289f2c81f80dbc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`3c89a57`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/3c89a57f7d48103cc3b56cd3b26d5521338fd7c1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

| [Files](https://app.codecov.io/gh/envoyproxy/gateway/pull/4102?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) | Patch % | Lines |
|---|---|---|
| [internal/xds/translator/oidc.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/4102?src=pr&el=tree&filepath=internal%2Fxds%2Ftranslator%2Foidc.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3Ivb2lkYy5nbw==) | 0.00% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/4102?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #4102      +/-   ##
==========================================
- Coverage   67.97%   67.97%   -0.01%     
==========================================
  Files         187      187              
  Lines       23032    23035       +3     
==========================================
+ Hits        15656    15657       +1     
- Misses       6261     6262       +1     
- Partials     1115     1116       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/4102?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).


Patch:
diff --git a/api/v1alpha1/authorization_types.go b/api/v1alpha1/authorization_types.go
index 3a589daef..2851cecf6 100644
--- a/api/v1alpha1/authorization_types.go
+++ b/api/v1alpha1/authorization_types.go
@@ -36,6 +36,9 @@ type AuthorizationRule struct {
 	Action AuthorizationAction `json:"action"`
 
 	// Principal specifies the client identity of a request.
+	// If there are multiple principal types, all principals must match for the rule to match.
+	// For example, if there are two principals: one for client IP and one for JWT claim,
+	// the rule will match only if both the client IP and the JWT claim match.
 	Principal Principal `json:"principal"`
 }
 
@@ -43,21 +46,85 @@ type AuthorizationRule struct {
 // A client identity can be a client IP, a JWT claim, username from the Authorization header,
 // or any other identity that can be extracted from a custom header.
 // Currently, only the client IP is supported.
+//
+// +kubebuilder:validation:XValidation:rule="(has(self.clientCIDRs) || has(self.jwt))",message="at least one of clientCIDRs or jwt must be specified"
 type Principal struct {
 	// ClientCIDRs are the IP CIDR ranges of the client.
 	// Valid examples are "192.168.1.0/24" or "2001:db8::/64"
 	//
+	// If multiple CIDR ranges are specified, one of the CIDR ranges must match
+	// the client IP for the rule to match.
+	//
 	// The client IP is inferred from the X-Forwarded-For header, a custom header,
 	// or the proxy protocol.
 	// You can use the `ClientIPDetection` or the `EnableProxyProtocol` field in
 	// the `ClientTrafficPolicy` to configure how the client IP is detected.
-	// +kubebuilder:validation:MinItems=1
+	// +optional
 	ClientCIDRs []CIDR `json:"clientCIDRs"`
 
-	// TODO: Zhaohuabing the MinItems=1 validation can be relaxed to allow empty list
-	// after other principal types are supported. However, at least one principal is required
+	// JWT authorize the request based on the JWT claims and scopes.
+	// Note: in order to use JWT claims for authorization, you must configure the
+	// JWT authentication in the same `SecurityPolicy`.
+	// +optional
+	// +notImplementedHide
+	JWT *JWTPrincipal `json:"jwt,omitempty"`
+}
+
+// JWTPrincipal specifies the client identity of a request based on the JWT claims and scopes.
+// At least one of the claims or scopes must be specified.
+// Claims and scopes are And-ed together if both are specified.
+//
+// +kubebuilder:validation:XValidation:rule="(has(self.claims) || has(self.scopes))",message="at least one of claims or scopes must be specified"
+type JWTPrincipal struct {
+	// Claims are the claims in a JWT token.
+	//
+	// If multiple claims are specified, all claims must match for the rule to match.
+	// For example, if there are two claims: one for the audience and one for the issuer,
+	// the rule will match only if both the audience and the issuer match.
+	// +optional
+	Claims []JWTClaim `json:"claims,omitempty"`
+
+	// Scopes are a special type of claim in a JWT token that represents the permissions of the client.
+	//
+	// The value of the scopes field should be a space delimited string that is expected in the scope parameter,
+	// as defined in RFC 6749: https://datatracker.ietf.org/doc/html/rfc6749#page-23.
+	//
+	// If multiple scopes are specified, all scopes must match for the rule to match.
+	// +optional
+	Scopes []string `json:"scopes,omitempty"`
+}
+
+// JWTClaim specifies a claim in a JWT token.
+type JWTClaim struct {
+	// Name is the name of the claim.
+	// If it is a nested claim, use a dot (.) separated string as the name to
+	// represent the full path to the claim.
+	// For example, if the claim is in the "department" field in the "organization" field,
+	// the name should be "organization.department".
+	Name string `json:"name"`
+
+	// ValueType is the type of the claim value.
+	// Only String and StringArray types are supported for now.
+	// +kubebuilder:validation:Enum=String;StringArray
+	// +kubebuilder:default=String
+	// +unionDiscriminator
+	// +optional
+	ValueType *JWTClaimValueType `json:"valueType,omitempty"`
+
+	// Values are the values that the claim must match.
+	// If the claim is a string type, the specified value must match exactly.
+	// If the claim is a string array type, the specified value must match one of the values in the array.
+	// If multiple values are specified, one of the values must match for the rule to match.
+	Values []string `json:"values"`
 }
 
+type JWTClaimValueType string
+
+const (
+	JWTClaimValueTypeString      JWTClaimValueType = "String"
+	JWTClaimValueTypeStringArray JWTClaimValueType = "StringArray"
+)
+
 // AuthorizationAction defines the action to be taken if a rule matches.
 // +kubebuilder:validation:Enum=Allow;Deny
 type AuthorizationAction string
diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go
index 1720d0561..542971ec1 100644
--- a/api/v1alpha1/securitypolicy_types.go
+++ b/api/v1alpha1/securitypolicy_types.go
@@ -41,6 +41,7 @@ type SecurityPolicy struct {
 // +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.group == 'gateway.networking.k8s.io') : true ", message="this policy can only have a targetRefs[*].group of gateway.networking.k8s.io"
 // +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute']) : true ", message="this policy can only have a targetRefs[*].kind of Gateway/HTTPRoute/GRPCRoute"
 // +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName)) : true",message="this policy does not yet support the sectionName field"
+// +kubebuilder:validation:XValidation:rule="(has(self.authorization) && has(self.authorization.rules) && self.authorization.rules.exists(r, has(r.principal.jwt))) ? has(self.jwt) : true", message="if authorization.rules.principal.jwt is used, jwt must be defined"
 //
 // SecurityPolicySpec defines the desired state of SecurityPolicy.
 type SecurityPolicySpec struct {
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 9afe197fc..cde4e3b90 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -2888,6 +2888,31 @@ func (in *JWT) DeepCopy() *JWT {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *JWTClaim) DeepCopyInto(out *JWTClaim) {
+	*out = *in
+	if in.ValueType != nil {
+		in, out := &in.ValueType, &out.ValueType
+		*out = new(JWTClaimValueType)
+		**out = **in
+	}
+	if in.Values != nil {
+		in, out := &in.Values, &out.Values
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTClaim.
+func (in *JWTClaim) DeepCopy() *JWTClaim {
+	if in == nil {
+		return nil
+	}
+	out := new(JWTClaim)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *JWTExtractor) DeepCopyInto(out *JWTExtractor) {
 	*out = *in
@@ -2940,6 +2965,33 @@ func (in *JWTHeaderExtractor) DeepCopy() *JWTHeaderExtractor {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *JWTPrincipal) DeepCopyInto(out *JWTPrincipal) {
+	*out = *in
+	if in.Claims != nil {
+		in, out := &in.Claims, &out.Claims
+		*out = make([]JWTClaim, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.Scopes != nil {
+		in, out := &in.Scopes, &out.Scopes
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTPrincipal.
+func (in *JWTPrincipal) DeepCopy() *JWTPrincipal {
+	if in == nil {
+		return nil
+	}
+	out := new(JWTPrincipal)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *JWTProvider) DeepCopyInto(out *JWTProvider) {
 	*out = *in
@@ -3748,6 +3800,11 @@ func (in *Principal) DeepCopyInto(out *Principal) {
 		*out = make([]CIDR, len(*in))
 		copy(*out, *in)
 	}
+	if in.JWT != nil {
+		in, out := &in.JWT, &out.JWT
+		*out = new(JWTPrincipal)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Principal.
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
index 7631ed68f..f0fc4379a 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -85,14 +85,20 @@ spec:
                             If not specified, Envoy Gateway will generate a unique name for the rule.n
                           type: string
                         principal:
-                          description: Principal specifies the client identity of
-                            a request.
+                          description: |-
+                            Principal specifies the client identity of a request.
+                            If there are multiple principal types, all principals must match for the rule to match.
+                            For example, if there are two principals: one for client IP and one for JWT claim,
+                            the rule will match only if both the client IP and the JWT claim match.
                           properties:
                             clientCIDRs:
                               description: |-
                                 ClientCIDRs are the IP CIDR ranges of the client.
                                 Valid examples are "192.168.1.0/24" or "2001:db8::/64"
 
+                                If multiple CIDR ranges are specified, one of the CIDR ranges must match
+                                the client IP for the rule to match.
+
                                 The client IP is inferred from the X-Forwarded-For header, a custom header,
                                 or the proxy protocol.
                                 You can use the `ClientIPDetection` or the `EnableProxyProtocol` field in
@@ -103,11 +109,75 @@ spec:
                                   A CIDR can be an IPv4 address range such as "192.168.1.0/24" or an IPv6 address range such as "2001:0db8:11a3:09d7::/64".
                                 pattern: ((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]+))|((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/([0-9]+))
                                 type: string
-                              minItems: 1
                               type: array
-                          required:
-                          - clientCIDRs
+                            jwt:
+                              description: |-
+                                JWT authorize the request based on the JWT claims and scopes.
+                                Note: in order to use JWT claims for authorization, you must configure the
+                                JWT authentication in the same `SecurityPolicy`.
+                              properties:
+                                claims:
+                                  description: |-
+                                    Claims are the claims in a JWT token.
+
+                                    If multiple claims are specified, all claims must match for the rule to match.
+                                    For example, if there are two claims: one for the audience and one for the issuer,
+                                    the rule will match only if both the audience and the issuer match.
+                                  items:
+                                    description: JWTClaim specifies a claim in a JWT
+                                      token.
+                                    properties:
+                                      name:
+                                        description: |-
+                                          Name is the name of the claim.
+                                          If it is a nested claim, use a dot (.) separated string as the name to
+                                          represent the full path to the claim.
+                                          For example, if the claim is in the "department" field in the "organization" field,
+                                          the name should be "organization.department".
+                                        type: string
+                                      valueType:
+                                        default: String
+                                        description: |-
+                                          ValueType is the type of the claim value.
+                                          Only String and StringArray types are supported for now.
+                                        enum:
+                                        - String
+                                        - StringArray
+                                        type: string
+                                      values:
+                                        description: |-
+                                          Values are the values that the claim must match.
+                                          If the claim is a string type, the specified value must match exactly.
+                                          If the claim is a string array type, the specified value must match one of the values in the array.
+                                          If multiple values are specified, one of the values must match for the rule to match.
+                                        items:
+                                          type: string
+                                        type: array
+                                    required:
+                                    - name
+                                    - values
+                                    type: object
+                                  type: array
+                                scopes:
+                                  description: |-
+                                    Scopes are a special type of claim in a JWT token that represents the permissions of the client.
+
+                                    The value of the scopes field should be a space delimited string that is expected in the scope parameter,
+                                    as defined in RFC 6749: https://datatracker.ietf.org/doc/html/rfc6749#page-23.
+
+                                    If multiple scopes are specified, all scopes must match for the rule to match.
+                                  items:
+                                    type: string
+                                  type: array
+                              type: object
+                              x-kubernetes-validations:
+                              - message: at least one of claims or scopes must be
+                                  specified
+                                rule: (has(self.claims) || has(self.scopes))
                           type: object
+                          x-kubernetes-validations:
+                          - message: at least one of clientCIDRs or jwt must be specified
+                            rule: (has(self.clientCIDRs) || has(self.jwt))
                       required:
                       - action
                       - principal
@@ -2321,6 +2391,10 @@ spec:
             - message: this policy does not yet support the sectionName field
               rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, !has(ref.sectionName))
                 : true'
+            - message: if authorization.rules.principal.jwt is used, jwt must be defined
+              rule: '(has(self.authorization) && has(self.authorization.rules) &&
+                self.authorization.rules.exists(r, has(r.principal.jwt))) ? has(self.jwt)
+                : true'
           status:
             description: Status defines the current status of SecurityPolicy.
             properties:
diff --git a/examples/extension-server/go.mod b/examples/extension-server/go.mod
index 6a9f7f98d..53f4ad7c0 100644
--- a/examples/extension-server/go.mod
+++ b/examples/extension-server/go.mod
@@ -4,7 +4,7 @@ go 1.22.5
 
 require (
 	github.com/envoyproxy/gateway v1.0.2
-	github.com/envoyproxy/go-control-plane v0.13.0
+	github.com/envoyproxy/go-control-plane v0.13.1-0.20240903155423-c0847bf34c89
 	github.com/urfave/cli/v2 v2.27.2
 	google.golang.org/grpc v1.66.0
 	google.golang.org/protobuf v1.34.2
@@ -18,7 +18,7 @@ require (
 	github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
 	github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
-	github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
+	github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
diff --git a/examples/extension-server/go.sum b/examples/extension-server/go.sum
index 425c27ef6..de267cce4 100644
--- a/examples/extension-server/go.sum
+++ b/examples/extension-server/go.sum
@@ -11,10 +11,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les=
-github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8=
-github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
-github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
+github.com/envoyproxy/go-control-plane v0.13.1-0.20240903155423-c0847bf34c89 h1:ZY5uB8jkNV3rhhzDI0+B9g5RISAOwazjjGqkP1sQ2HI=
+github.com/envoyproxy/go-control-plane v0.13.1-0.20240903155423-c0847bf34c89/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
+github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
+github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
diff --git a/go.mod b/go.mod
index 18b53fa51..038fc9073 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 	github.com/docker/cli v27.2.0+incompatible
 	github.com/dominikbraun/graph v0.23.0
-	github.com/envoyproxy/go-control-plane v0.13.0
+	github.com/envoyproxy/go-control-plane v0.13.1-0.20240903155423-c0847bf34c89
 	github.com/envoyproxy/ratelimit v1.4.1-0.20230427142404-e2a87f41d3a7
 	github.com/evanphx/json-patch/v5 v5.9.0
 	github.com/fatih/color v1.17.0
@@ -231,7 +231,7 @@ require (
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/chai2010/gettext-go v1.0.3 // indirect
 	github.com/emicklei/go-restful/v3 v3.12.1 // indirect
-	github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
+	github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
 	github.com/evanphx/json-patch v5.9.0+incompatible
 	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
diff --git a/go.sum b/go.sum
index 951786354..943742dcf 100644
--- a/go.sum
+++ b/go.sum
@@ -423,11 +423,11 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
-github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les=
-github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8=
+github.com/envoyproxy/go-control-plane v0.13.1-0.20240903155423-c0847bf34c89 h1:ZY5uB8jkNV3rhhzDI0+B9g5RISAOwazjjGqkP1sQ2HI=
+github.com/envoyproxy/go-control-plane v0.13.1-0.20240903155423-c0847bf34c89/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
-github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
+github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
+github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
 github.com/envoyproxy/ratelimit v1.4.1-0.20230427142404-e2a87f41d3a7 h1:yz9/p/8QVPuEjPqRfZDXJmRaURKpKkxCZXUhl22i+cU=
 github.com/envoyproxy/ratelimit v1.4.1-0.20230427142404-e2a87f41d3a7/go.mod h1:NmJBO+gDMvSQWvcSWq8wmlgkDmHHAkx1SCxEGva5hKU=
 github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index 276a63c8b..6590311d1 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -662,6 +662,7 @@ func (t *Translator) buildOIDC(
 		DefaultRefreshTokenTTL: oidc.DefaultRefreshTokenTTL,
 		CookieSuffix:           suffix,
 		CookieNameOverrides:    policy.Spec.OIDC.CookieNames,
+		CookieDomain:           policy.Spec.OIDC.CookieDomain,
 		HMACSecret:             hmacData,
 	}, nil
 }
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 9a3af5efe..5d90b6816 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -819,6 +819,9 @@ type OIDC struct {
 
 	// CookieNameOverrides can optionally override the generated name of the cookies set by the oauth filter.
 	CookieNameOverrides *egv1a1.OIDCCookieNames `json:"cookieNameOverrides,omitempty"`
+
+	// CookieDomain sets the domain of the cookies set by the oauth filter.
+	CookieDomain *string `json:"cookieDomain,omitempty"`
 }
 
 type OIDCProvider struct {
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 285077222..c16028999 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -1900,6 +1900,11 @@ func (in *OIDC) DeepCopyInto(out *OIDC) {
 		*out = new(v1alpha1.OIDCCookieNames)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.CookieDomain != nil {
+		in, out := &in.CookieDomain, &out.CookieDomain
+		*out = new(string)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDC.
diff --git a/internal/xds/extensions/extensions.gen.go b/internal/xds/extensions/extensions.gen.go
index 09713ea43..18a0eeb1c 100644
--- a/internal/xds/extensions/extensions.gen.go
+++ b/internal/xds/extensions/extensions.gen.go
@@ -131,7 +131,7 @@ import (
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/oauth2/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/on_demand/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/original_src/v3"
-	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/proto_message_logging/v3"
+	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/proto_message_extraction/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rate_limit_quota/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ratelimit/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
@@ -292,6 +292,7 @@ import (
 	_ "github.com/envoyproxy/go-control-plane/envoy/service/metrics/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/service/rate_limit_quota/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3"
+	_ "github.com/envoyproxy/go-control-plane/envoy/service/redis_auth/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/service/route/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/service/runtime/v3"
 	_ "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3"
diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go
index e04b601a2..210285195 100644
--- a/internal/xds/translator/oidc.go
+++ b/internal/xds/translator/oidc.go
@@ -205,6 +205,10 @@ func oauth2Config(oidc *ir.OIDC) (*oauth2v3.OAuth2, error) {
 		oauth2.Config.Credentials.CookieNames.IdToken = *oidc.CookieNameOverrides.IDToken
 	}
 
+	if oidc.CookieDomain != nil {
+		oauth2.Config.Credentials.CookieDomain = *oidc.CookieDomain
+	}
+
 	return oauth2, nil
 }
 


Test patch:
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml
index d97cf0469..b5031e6aa 100644
--- a/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml
@@ -132,3 +132,4 @@ securityPolicies:
       defaultTokenTTL: 1h
       refreshToken: true
       defaultRefreshTokenTTL: 48h
+      cookieDomain: "example.com"
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml
index 9d27209db..5f327af87 100644
--- a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml
@@ -147,6 +147,7 @@ securityPolicies:
         group: null
         kind: null
         name: client2-secret
+      cookieDomain: example.com
       defaultRefreshTokenTTL: 48h0m0s
       defaultTokenTTL: 1h0m0s
       forwardAccessToken: true
@@ -273,6 +274,7 @@ xdsIR:
           oidc:
             clientID: client2.oauth.foo.com
             clientSecret: Y2xpZW50MTpzZWNyZXQK
+            cookieDomain: example.com
             cookieSuffix: 5f93c2e4
             defaultRefreshTokenTTL: 48h0m0s
             defaultTokenTTL: 1h0m0s
diff --git a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml
index 4775fa967..828da7c2f 100644
--- a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml
+++ b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml
@@ -73,3 +73,4 @@ http:
         cookieNameOverrides:
           idToken: "CustomIdTokenOverride"
           accessToken: "CustomAccessTokenOverride"
+        cookieDomain: "example.com"
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml
index bc4956b28..f1defaf5d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml
@@ -73,6 +73,7 @@
               authorizationEndpoint: https://oauth.bar.com/oauth2/v2/auth
               credentials:
                 clientId: client.oauth.bar.com
+                cookieDomain: example.com
                 cookieNames:
                   bearerToken: CustomAccessTokenOverride
                   idToken: CustomIdTokenOverride
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 94fad9c15..802efac58 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -240,7 +240,7 @@ _Appears in:_
 | ---   | ---  | ---      | ---         |
 | `name` | _string_ |  false  | Name is a user-friendly name for the rule.<br />If not specified, Envoy Gateway will generate a unique name for the rule.n |
 | `action` | _[AuthorizationAction](#authorizationaction)_ |  true  | Action defines the action to be taken if the rule matches. |
-| `principal` | _[Principal](#principal)_ |  true  | Principal specifies the client identity of a request. |
+| `principal` | _[Principal](#principal)_ |  true  | Principal specifies the client identity of a request.<br />If there are multiple principal types, all principals must match for the rule to match.<br />For example, if there are two principals: one for client IP and one for JWT claim,<br />the rule will match only if both the client IP and the JWT claim match. |
 
 
 #### BackOffPolicy
@@ -2120,6 +2120,37 @@ _Appears in:_
 | `providers` | _[JWTProvider](#jwtprovider) array_ |  true  | Providers defines the JSON Web Token (JWT) authentication provider type.<br />When multiple JWT providers are specified, the JWT is considered valid if<br />any of the providers successfully validate the JWT. For additional details,<br />see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter.html. |
 
 
+#### JWTClaim
+
+
+
+JWTClaim specifies a claim in a JWT token.
+
+_Appears in:_
+- [JWTPrincipal](#jwtprincipal)
+
+| Field | Type | Required | Description |
+| ---   | ---  | ---      | ---         |
+| `name` | _string_ |  true  | Name is the name of the claim.<br />If it is a nested claim, use a dot (.) separated string as the name to<br />represent the full path to the claim.<br />For example, if the claim is in the "department" field in the "organization" field,<br />the name should be "organization.department". |
+| `valueType` | _[JWTClaimValueType](#jwtclaimvaluetype)_ |  false  | ValueType is the type of the claim value.<br />Only String and StringArray types are supported for now. |
+| `values` | _string array_ |  true  | Values are the values that the claim must match.<br />If the claim is a string type, the specified value must match exactly.<br />If the claim is a string array type, the specified value must match one of the values in the array.<br />If multiple values are specified, one of the values must match for the rule to match. |
+
+
+#### JWTClaimValueType
+
+_Underlying type:_ _string_
+
+
+
+_Appears in:_
+- [JWTClaim](#jwtclaim)
+
+| Value | Description |
+| ----- | ----------- |
+| `String` |  | 
+| `StringArray` |  | 
+
+
 #### JWTExtractor
 
 
@@ -2153,6 +2184,23 @@ _Appears in:_
 | `valuePrefix` | _string_ |  false  | ValuePrefix is the prefix that should be stripped before extracting the token.<br />The format would be used by Envoy like "\{ValuePrefix\}<TOKEN>".<br />For example, "Authorization: Bearer <TOKEN>", then the ValuePrefix="Bearer " with a space at the end. |
 
 
+#### JWTPrincipal
+
+
+
+JWTPrincipal specifies the client identity of a request based on the JWT claims and scopes.
+At least one of the claims or scopes must be specified.
+Claims and scopes are And-ed together if both are specified.
+
+_Appears in:_
+- [Principal](#principal)
+
+| Field | Type | Required | Description |
+| ---   | ---  | ---      | ---         |
+| `claims` | _[JWTClaim](#jwtclaim) array_ |  false  | Claims are the claims in a JWT token.<br /><br />If multiple claims are specified, all claims must match for the rule to match.<br />For example, if there are two claims: one for the audience and one for the issuer,<br />the rule will match only if both the audience and the issuer match. |
+| `scopes` | _string array_ |  false  | Scopes are a special type of claim in a JWT token that represents the permissions of the client.<br /><br />The value of the scopes field should be a space delimited string that is expected in the scope parameter,<br />as defined in RFC 6749: https://datatracker.ietf.org/doc/html/rfc6749#page-23.<br /><br />If multiple scopes are specified, all scopes must match for the rule to match. |
+
+
 #### JWTProvider
 
 
@@ -2668,7 +2716,7 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `clientCIDRs` | _[CIDR](#cidr) array_ |  true  | ClientCIDRs are the IP CIDR ranges of the client.<br />Valid examples are "192.168.1.0/24" or "2001:db8::/64"<br /><br />The client IP is inferred from the X-Forwarded-For header, a custom header,<br />or the proxy protocol.<br />You can use the `ClientIPDetection` or the `EnableProxyProtocol` field in<br />the `ClientTrafficPolicy` to configure how the client IP is detected. |
+| `clientCIDRs` | _[CIDR](#cidr) array_ |  false  | ClientCIDRs are the IP CIDR ranges of the client.<br />Valid examples are "192.168.1.0/24" or "2001:db8::/64"<br /><br />If multiple CIDR ranges are specified, one of the CIDR ranges must match<br />the client IP for the rule to match.<br /><br />The client IP is inferred from the X-Forwarded-For header, a custom header,<br />or the proxy protocol.<br />You can use the `ClientIPDetection` or the `EnableProxyProtocol` field in<br />the `ClientTrafficPolicy` to configure how the client IP is detected. |
 
 
 #### ProcessingModeOptions
diff --git a/site/content/en/latest/tasks/security/oidc.md b/site/content/en/latest/tasks/security/oidc.md
index 8add22bfb..45adc554a 100644
--- a/site/content/en/latest/tasks/security/oidc.md
+++ b/site/content/en/latest/tasks/security/oidc.md
@@ -97,7 +97,7 @@ providers, including Auth0, Azure AD, Keycloak, Okta, OneLogin, Salesforce, UAA,
 
 Follow the steps in the [Google OIDC documentation][google-oidc] to register an OIDC application. Please make sure the
 redirect URL is set to the one you configured in the SecurityPolicy that you will create in the step below. In this example,
-the redirect URL is `http://www.example.com:8443/myapp/oauth2/callback`.
+the redirect URL is `https://www.example.com:8443/myapp/oauth2/callback`.
 
 After registering the application, you should have the following information:
 * Client ID: The client ID of the OIDC application.
@@ -221,12 +221,78 @@ If you haven't registered an OIDC application, follow the steps in the previous
 
 If you haven't created a kubernetes secret, follow the steps in the previous section to create a kubernetes secret.
 
+### Create an HTTPRoute with a different subdomain
+
+Let's create another HTTPRoute in the same Gateway, but with a different subdomain.
+
+{{< tabpane text=true >}}
+{{% tab header="Apply from stdin" %}}
+
+```shell
+cat <<EOF | kubectl apply -f -
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: foo
+spec:
+  parentRefs:
+  - name: eg
+  hostnames: ["foo.example.com"]
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /
+    backendRefs:
+    - name: backend
+      port: 3000
+EOF
+```
+
+{{% /tab %}}
+{{% tab header="Apply from file" %}}
+Save and apply the following resource to your cluster:
+
+```yaml
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: foo
+spec:
+  parentRefs:
+  - name: eg
+  hostnames: ["foo.example.com"]
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /foo
+    backendRefs:
+    - name: backend
+      port: 3000
+```
+
+{{% /tab %}}
+{{< /tabpane >}}
+
+Verify the HTTPRoute status:
+
+```shell
+kubectl get httproute/foo -o yaml
+```
+
 ### Create a SecurityPolicy
 
 Create or update the SecurityPolicy to target the Gateway instead of the HTTPRoute. **Please notice that the `redirectURL` 
 and `logoutPath` must match one of the HTTPRoutes associated with the Gateway.** In this example, the target Gateway has 
-two HTTPRoutes associated with it, one with the host `www.example.com` and the path `/myapp`, and the other with the host
-`www.example.com` and the path `/`. Either one of the HTTPRoutes can be used to match the `redirectURL` and `logoutPath`.
+three HTTPRoutes associated with it, one with the host `www.example.com` and the path `/myapp`, one with the host 
+`www.example.com` and the path `/`, and one with the host `foo.example.com` and the path `/`. Any of these HTTPRoutes 
+can be used to match the `redirectURL` and `logoutPath`.
+
+By default, the access token and ID token cookies are set to the host of the request, excluding subdomains. To allow the
+token cookies to be shared across subdomains and prevent users from having to log in again when switching between subdomains, 
+the `cookieDomain` field needs to be set to the root domain. In this example, the root domain is `example.com`.
 
 {{< tabpane text=true >}}
 {{% tab header="Apply from stdin" %}}
@@ -250,6 +316,7 @@ spec:
       name: "my-app-client-secret"
     redirectURL: "https://www.example.com:8443/myapp/oauth2/callback"
     logoutPath: "/myapp/logout"
+    cookieDomain: "example.com"
 EOF
 ```
 
@@ -276,6 +343,7 @@ spec:
       name: "my-app-client-secret"
     redirectURL: "https://www.example.com:8443/myapp/oauth2/callback"
     logoutPath: "/myapp/logout"
+    cookieDomain: "example.com"
 ```
 
 {{% /tab %}}
@@ -287,16 +355,40 @@ Verify the SecurityPolicy configuration:
 kubectl get securitypolicy/oidc-example -o yaml
 ```
 
+### Update the Listener TLS certificate to support multiple subdomains
+
+Create a multi-domain wildcard certificate for `*.example.com`.
+
+```shell
+openssl req -out wildcard.csr -newkey rsa:2048 -nodes -keyout wildcard.key -subj "/CN=*.example.com/O=example organization"
+openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in wildcard.csr -out wildcard.crt
+```
+
+Replace the TLS certificate of the Gateway with the wildcard certificate.
+
+```shell
+kubectl delete secret example-cert
+kubectl create secret tls example-cert --key=wildcard.key --cert=wildcard.crt
+```
+
 ### Testing
 
 If you haven't done so, follow the steps in the previous section to port forward gateway port to localhost and put
 www.example.com in the /etc/hosts file in your test machine.
 
-Open a browser and navigate to the `https://www.example.com:8443/foo` address. You should be redirected to the Google
+Also, put foo.example.com in the /etc/hosts file in your test machine.
+
+```shell
+...
+127.0.0.1 foo.example.com
+```
+
+Open a browser and navigate to the `https://www.example.com:8443/myapp` address. You should be redirected to the Google
 login page. After you successfully login, you should see the response from the backend service.
 
-You can also try to access `https://www.example.com:8443/myapp` address. You should be able to see this page since the
-path `/myapp` is protected by the same OIDC policy.
+You can also try to access `https://foo.example.com:8443` and `https://www.example.com:8443/bar` addresses. You should
+be able to see the response from the backend service since these HTTPRoutes are also protected by the same OIDC config,
+and the cookies are shared across subdomains.
 
 ## Clean-Up
 
@@ -308,6 +400,7 @@ Delete the SecurityPolicy, the secret and the HTTPRoute:
 kubectl delete securitypolicy/oidc-example
 kubectl delete secret/my-app-client-secret
 kubectl delete httproute/myapp
+kubectl delete httproute/foo
 ```
 
 ## Next Steps
diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md
index 94fad9c15..802efac58 100644
--- a/site/content/zh/latest/api/extension_types.md
+++ b/site/content/zh/latest/api/extension_types.md
@@ -240,7 +240,7 @@ _Appears in:_
 | ---   | ---  | ---      | ---         |
 | `name` | _string_ |  false  | Name is a user-friendly name for the rule.<br />If not specified, Envoy Gateway will generate a unique name for the rule.n |
 | `action` | _[AuthorizationAction](#authorizationaction)_ |  true  | Action defines the action to be taken if the rule matches. |
-| `principal` | _[Principal](#principal)_ |  true  | Principal specifies the client identity of a request. |
+| `principal` | _[Principal](#principal)_ |  true  | Principal specifies the client identity of a request.<br />If there are multiple principal types, all principals must match for the rule to match.<br />For example, if there are two principals: one for client IP and one for JWT claim,<br />the rule will match only if both the client IP and the JWT claim match. |
 
 
 #### BackOffPolicy
@@ -2120,6 +2120,37 @@ _Appears in:_
 | `providers` | _[JWTProvider](#jwtprovider) array_ |  true  | Providers defines the JSON Web Token (JWT) authentication provider type.<br />When multiple JWT providers are specified, the JWT is considered valid if<br />any of the providers successfully validate the JWT. For additional details,<br />see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter.html. |
 
 
+#### JWTClaim
+
+
+
+JWTClaim specifies a claim in a JWT token.
+
+_Appears in:_
+- [JWTPrincipal](#jwtprincipal)
+
+| Field | Type | Required | Description |
+| ---   | ---  | ---      | ---         |
+| `name` | _string_ |  true  | Name is the name of the claim.<br />If it is a nested claim, use a dot (.) separated string as the name to<br />represent the full path to the claim.<br />For example, if the claim is in the "department" field in the "organization" field,<br />the name should be "organization.department". |
+| `valueType` | _[JWTClaimValueType](#jwtclaimvaluetype)_ |  false  | ValueType is the type of the claim value.<br />Only String and StringArray types are supported for now. |
+| `values` | _string array_ |  true  | Values are the values that the claim must match.<br />If the claim is a string type, the specified value must match exactly.<br />If the claim is a string array type, the specified value must match one of the values in the array.<br />If multiple values are specified, one of the values must match for the rule to match. |
+
+
+#### JWTClaimValueType
+
+_Underlying type:_ _string_
+
+
+
+_Appears in:_
+- [JWTClaim](#jwtclaim)
+
+| Value | Description |
+| ----- | ----------- |
+| `String` |  | 
+| `StringArray` |  | 
+
+
 #### JWTExtractor
 
 
@@ -2153,6 +2184,23 @@ _Appears in:_
 | `valuePrefix` | _string_ |  false  | ValuePrefix is the prefix that should be stripped before extracting the token.<br />The format would be used by Envoy like "\{ValuePrefix\}<TOKEN>".<br />For example, "Authorization: Bearer <TOKEN>", then the ValuePrefix="Bearer " with a space at the end. |
 
 
+#### JWTPrincipal
+
+
+
+JWTPrincipal specifies the client identity of a request based on the JWT claims and scopes.
+At least one of the claims or scopes must be specified.
+Claims and scopes are And-ed together if both are specified.
+
+_Appears in:_
+- [Principal](#principal)
+
+| Field | Type | Required | Description |
+| ---   | ---  | ---      | ---         |
+| `claims` | _[JWTClaim](#jwtclaim) array_ |  false  | Claims are the claims in a JWT token.<br /><br />If multiple claims are specified, all claims must match for the rule to match.<br />For example, if there are two claims: one for the audience and one for the issuer,<br />the rule will match only if both the audience and the issuer match. |
+| `scopes` | _string array_ |  false  | Scopes are a special type of claim in a JWT token that represents the permissions of the client.<br /><br />The value of the scopes field should be a space delimited string that is expected in the scope parameter,<br />as defined in RFC 6749: https://datatracker.ietf.org/doc/html/rfc6749#page-23.<br /><br />If multiple scopes are specified, all scopes must match for the rule to match. |
+
+
 #### JWTProvider
 
 
@@ -2668,7 +2716,7 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `clientCIDRs` | _[CIDR](#cidr) array_ |  true  | ClientCIDRs are the IP CIDR ranges of the client.<br />Valid examples are "192.168.1.0/24" or "2001:db8::/64"<br /><br />The client IP is inferred from the X-Forwarded-For header, a custom header,<br />or the proxy protocol.<br />You can use the `ClientIPDetection` or the `EnableProxyProtocol` field in<br />the `ClientTrafficPolicy` to configure how the client IP is detected. |
+| `clientCIDRs` | _[CIDR](#cidr) array_ |  false  | ClientCIDRs are the IP CIDR ranges of the client.<br />Valid examples are "192.168.1.0/24" or "2001:db8::/64"<br /><br />If multiple CIDR ranges are specified, one of the CIDR ranges must match<br />the client IP for the rule to match.<br /><br />The client IP is inferred from the X-Forwarded-For header, a custom header,<br />or the proxy protocol.<br />You can use the `ClientIPDetection` or the `EnableProxyProtocol` field in<br />the `ClientTrafficPolicy` to configure how the client IP is detected. |
 
 
 #### ProcessingModeOptions
diff --git a/test/cel-validation/securitypolicy_test.go b/test/cel-validation/securitypolicy_test.go
index cbc0145aa..92d179fec 100644
--- a/test/cel-validation/securitypolicy_test.go
+++ b/test/cel-validation/securitypolicy_test.go
@@ -1030,6 +1030,98 @@ func TestSecurityPolicyTarget(t *testing.T) {
 			},
 			wantErrors: []string{},
 		},
+		{
+			desc: "authorization-missing principal",
+			mutate: func(sp *egv1a1.SecurityPolicy) {
+				sp.Spec = egv1a1.SecurityPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetSelectors: []egv1a1.TargetSelector{
+							{
+								Group: ptr.To(gwapiv1a2.Group("gateway.networking.k8s.io")),
+								Kind:  "HTTPRoute",
+								MatchLabels: map[string]string{
+									"eg/namespace": "reference-apps",
+								},
+							},
+						},
+					},
+					Authorization: &egv1a1.Authorization{
+						Rules: []egv1a1.AuthorizationRule{
+							{
+								Action:    egv1a1.AuthorizationActionAllow,
+								Principal: egv1a1.Principal{},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"at least one of clientCIDRs or jwt must be specified"},
+		},
+		{
+			desc: "authorization-jwt-claims-without-jwt-authn",
+			mutate: func(sp *egv1a1.SecurityPolicy) {
+				sp.Spec = egv1a1.SecurityPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetSelectors: []egv1a1.TargetSelector{
+							{
+								Group: ptr.To(gwapiv1a2.Group("gateway.networking.k8s.io")),
+								Kind:  "HTTPRoute",
+								MatchLabels: map[string]string{
+									"eg/namespace": "reference-apps",
+								},
+							},
+						},
+					},
+					Authorization: &egv1a1.Authorization{
+						Rules: []egv1a1.AuthorizationRule{
+							{
+								Action: egv1a1.AuthorizationActionAllow,
+								Principal: egv1a1.Principal{
+									JWT: &egv1a1.JWTPrincipal{
+										Claims: []egv1a1.JWTClaim{
+											{
+												Name:   "iss",
+												Values: []string{"https://example.com"},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"if authorization.rules.principal.jwt is used, jwt must be defined"},
+		},
+		{
+			desc: "authorization-jwt-empty-principal",
+			mutate: func(sp *egv1a1.SecurityPolicy) {
+				sp.Spec = egv1a1.SecurityPolicySpec{
+					PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+						TargetSelectors: []egv1a1.TargetSelector{
+							{
+								Group: ptr.To(gwapiv1a2.Group("gateway.networking.k8s.io")),
+								Kind:  "HTTPRoute",
+								MatchLabels: map[string]string{
+									"eg/namespace": "reference-apps",
+								},
+							},
+						},
+					},
+					Authorization: &egv1a1.Authorization{
+						Rules: []egv1a1.AuthorizationRule{
+							{
+								Action: egv1a1.AuthorizationActionAllow,
+								Principal: egv1a1.Principal{
+									JWT: &egv1a1.JWTPrincipal{},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"at least one of claims or scopes must be specified"},
+		},
 	}
 
 	for _, tc := range cases {