Repository: envoyproxy/gateway
Pull request #4227 (2024-09-12 15:52:20)

Use BackendCluster to represent OIDCProvider
Using BackendCluster to represent OIDCProvider allows users to customize backend connection settings for OIDC providers, such as retry policy and tls setting.

Fix: https://github.com/envoyproxy/gateway/issues/3178

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/4227?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 `86.66667%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 66.49%. Comparing base [(`eae2870`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/eae287070dcb49914ae7d7d873dd3933f3e7e0d5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`3342b33`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/3342b33a5b642c2a908c12cb5225c30faeea7fb8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).
> Report is 3 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/envoyproxy/gateway/pull/4227?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/gatewayapi/securitypolicy.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/4227?src=pr&el=tree&filepath=internal%2Fgatewayapi%2Fsecuritypolicy.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvZ2F0ZXdheWFwaS9zZWN1cml0eXBvbGljeS5nbw==) | 86.66% | [2 Missing and 2 partials :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/4227?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    #4227      +/-   ##
==========================================
+ Coverage   66.44%   66.49%   +0.04%     
==========================================
  Files         195      195              
  Lines       23734    23754      +20     
==========================================
+ Hits        15770    15795      +25     
+ Misses       6841     6836       -5     
  Partials     1123     1123              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/4227?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).

arkodg: thanks @zhaohuabing ! 

Patch:
diff --git a/api/v1alpha1/httproutefilter_types.go b/api/v1alpha1/httproutefilter_types.go
index 7f56ca07d..9ae8be598 100644
--- a/api/v1alpha1/httproutefilter_types.go
+++ b/api/v1alpha1/httproutefilter_types.go
@@ -33,6 +33,8 @@ type HTTPRouteFilter struct {
 type HTTPRouteFilterSpec struct {
 	// +optional
 	URLRewrite *HTTPURLRewriteFilter `json:"urlRewrite,omitempty"`
+	// +optional
+	DirectResponse *HTTPDirectResponseFilter `json:"directResponse,omitempty"`
 }
 
 // HTTPURLRewriteFilter define rewrites of HTTP URL components such as path and host
@@ -49,6 +51,24 @@ type HTTPURLRewriteFilter struct {
 	Path *HTTPPathModifier `json:"path,omitempty"`
 }
 
+// HTTPDirectResponseFilter defines the configuration to return a fixed response.
+type HTTPDirectResponseFilter struct {
+	// Content Type of the response. This will be set in the Content-Type header.
+	//
+	// +optional
+	ContentType *string `json:"contentType,omitempty"`
+
+	// Body of the Response
+	//
+	// +optional
+	Body *CustomResponseBody `json:"body,omitempty"`
+
+	// Status Code of the HTTP response
+	// If unset, defaults to 200.
+	// +optional
+	StatusCode *int `json:"statusCode,omitempty"`
+}
+
 // HTTPPathModifierType defines the type of path redirect or rewrite.
 type HTTPPathModifierType string
 
diff --git a/api/v1alpha1/oidc_types.go b/api/v1alpha1/oidc_types.go
index 73e1dea8c..78c32287c 100644
--- a/api/v1alpha1/oidc_types.go
+++ b/api/v1alpha1/oidc_types.go
@@ -107,7 +107,8 @@ type OIDC struct {
 
 // OIDCProvider defines the OIDC Provider configuration.
 // +kubebuilder:validation:XValidation:rule="!has(self.backendRef)",message="BackendRefs must be used, backendRef is not supported."
-// +kubebuilder:validation:XValidation:rule="has(self.backendRefs)? self.backendRefs.size() > 1 : true",message="Only one backendRefs is allowed."
+// +kubebuilder:validation:XValidation:rule="has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)? !has(self.backendSettings.retry.perRetry.timeout):true):true):true",message="Retry timeout is not supported."
+// +kubebuilder:validation:XValidation:rule="has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)? !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true",message="HTTPStatusCodes is not supported."
 type OIDCProvider struct {
 	// BackendRefs is used to specify the address of the OIDC Provider.
 	// If the BackendRefs is not specified, The host and port of the OIDC Provider's token endpoint
diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go
index 8e98b904e..fe795c833 100644
--- a/api/v1alpha1/shared_types.go
+++ b/api/v1alpha1/shared_types.go
@@ -669,7 +669,9 @@ type CustomResponse struct {
 	ContentType *string `json:"contentType,omitempty"`
 
 	// Body of the Custom Response
-	Body CustomResponseBody `json:"body"`
+	//
+	// +optional
+	Body *CustomResponseBody `json:"body,omitempty"`
 }
 
 // ResponseValueType defines the types of values for the response body supported by Envoy Gateway.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index d6a4f9daf..696c99259 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -1148,7 +1148,11 @@ func (in *CustomResponse) DeepCopyInto(out *CustomResponse) {
 		*out = new(string)
 		**out = **in
 	}
-	in.Body.DeepCopyInto(&out.Body)
+	if in.Body != nil {
+		in, out := &in.Body, &out.Body
+		*out = new(CustomResponseBody)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResponse.
@@ -2688,6 +2692,36 @@ func (in *HTTPClientTimeout) DeepCopy() *HTTPClientTimeout {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HTTPDirectResponseFilter) DeepCopyInto(out *HTTPDirectResponseFilter) {
+	*out = *in
+	if in.ContentType != nil {
+		in, out := &in.ContentType, &out.ContentType
+		*out = new(string)
+		**out = **in
+	}
+	if in.Body != nil {
+		in, out := &in.Body, &out.Body
+		*out = new(CustomResponseBody)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.StatusCode != nil {
+		in, out := &in.StatusCode, &out.StatusCode
+		*out = new(int)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPDirectResponseFilter.
+func (in *HTTPDirectResponseFilter) DeepCopy() *HTTPDirectResponseFilter {
+	if in == nil {
+		return nil
+	}
+	out := new(HTTPDirectResponseFilter)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *HTTPExtAuthService) DeepCopyInto(out *HTTPExtAuthService) {
 	*out = *in
@@ -2820,6 +2854,11 @@ func (in *HTTPRouteFilterSpec) DeepCopyInto(out *HTTPRouteFilterSpec) {
 		*out = new(HTTPURLRewriteFilter)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.DirectResponse != nil {
+		in, out := &in.DirectResponse, &out.DirectResponse
+		*out = new(HTTPDirectResponseFilter)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteFilterSpec.
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
index 521ee8bcf..7b2e93731 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -1035,8 +1035,6 @@ spec:
                           description: Content Type of the response. This will be
                             set in the Content-Type header.
                           type: string
-                      required:
-                      - body
                       type: object
                   required:
                   - match
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml
index 7a55ec887..8a75fec42 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml
@@ -49,6 +49,66 @@ spec:
           spec:
             description: Spec defines the desired state of HTTPRouteFilter.
             properties:
+              directResponse:
+                description: HTTPDirectResponseFilter defines the configuration to
+                  return a fixed response.
+                properties:
+                  body:
+                    description: Body of the Response
+                    properties:
+                      inline:
+                        description: Inline contains the value as an inline string.
+                        type: string
+                      type:
+                        description: Type is the type of method to use to read the
+                          body value.
+                        enum:
+                        - Inline
+                        - ValueRef
+                        type: string
+                      valueRef:
+                        description: |-
+                          ValueRef contains the contents of the body
+                          specified as a local object reference.
+                          Only a reference to ConfigMap is supported.
+                        properties:
+                          group:
+                            description: |-
+                              Group is the group of the referent. For example, "gateway.networking.k8s.io".
+                              When unspecified or empty string, core API group is inferred.
+                            maxLength: 253
+                            pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          kind:
+                            description: Kind is kind of the referent. For example
+                              "HTTPRoute" or "Service".
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                            type: string
+                          name:
+                            description: Name is the name of the referent.
+                            maxLength: 253
+                            minLength: 1
+                            type: string
+                        required:
+                        - group
+                        - kind
+                        - name
+                        type: object
+                    required:
+                    - type
+                    type: object
+                  contentType:
+                    description: Content Type of the response. This will be set in
+                      the Content-Type header.
+                    type: string
+                  statusCode:
+                    description: |-
+                      Status Code of the HTTP response
+                      If unset, defaults to 200.
+                    type: integer
+                type: object
               urlRewrite:
                 description: HTTPURLRewriteFilter define rewrites of HTTP URL components
                   such as path and host
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 ad2c81818..b6a040f8c 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -3261,9 +3261,12 @@ spec:
                     x-kubernetes-validations:
                     - message: BackendRefs must be used, backendRef is not supported.
                       rule: '!has(self.backendRef)'
-                    - message: Only one backendRefs is allowed.
-                      rule: 'has(self.backendRefs)? self.backendRefs.size() > 1 :
-                        true'
+                    - message: Retry timeout is not supported.
+                      rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)?
+                        !has(self.backendSettings.retry.perRetry.timeout):true):true):true
+                    - message: HTTPStatusCodes is not supported.
+                      rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)?
+                        !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true
                   redirectURL:
                     description: |-
                       The redirect URL to be used in the OIDC
diff --git a/go.mod b/go.mod
index ee7928f0a..0b7c29eb5 100644
--- a/go.mod
+++ b/go.mod
@@ -159,7 +159,7 @@ require (
 	github.com/moby/patternmatcher v0.6.0 // indirect
 	github.com/moby/sys/mountinfo v0.7.2 // indirect
 	github.com/moby/sys/sequential v0.5.0 // indirect
-	github.com/moby/sys/user v0.2.0 // indirect
+	github.com/moby/sys/user v0.3.0 // indirect
 	github.com/moby/sys/userns v0.1.0 // indirect
 	github.com/onsi/ginkgo v1.16.5 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 93ade68d7..5aa804824 100644
--- a/go.sum
+++ b/go.sum
@@ -601,8 +601,8 @@ github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9Kou
 github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
 github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
 github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
-github.com/moby/sys/user v0.2.0 h1:OnpapJsRp25vkhw8TFG6OLJODNh/3rEwRWtJ3kakwRM=
-github.com/moby/sys/user v0.2.0/go.mod h1:RYstrcWOJpVh+6qzUqp2bU3eaRpdiQeKGlKitaH0PM8=
+github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
+github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
 github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
 github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
 github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go
index 12453ea18..b8f289a9d 100644
--- a/internal/gatewayapi/backendtrafficpolicy.go
+++ b/internal/gatewayapi/backendtrafficpolicy.go
@@ -16,7 +16,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/sets"
-	"k8s.io/utils/ptr"
 	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
@@ -324,7 +323,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen
 		errs = errors.Join(errs, err)
 	}
 	if policy.Spec.Retry != nil {
-		rt = t.buildRetry(policy)
+		rt = buildRetry(policy.Spec.Retry)
 	}
 	if to, err = buildClusterSettingsTimeout(policy.Spec.ClusterSettings, nil); err != nil {
 		err = perr.WithMessage(err, "Timeout")
@@ -460,7 +459,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back
 		errs = errors.Join(errs, err)
 	}
 	if policy.Spec.Retry != nil {
-		rt = t.buildRetry(policy)
+		rt = buildRetry(policy.Spec.Retry)
 	}
 	if ct, err = buildClusterSettingsTimeout(policy.Spec.ClusterSettings, nil); err != nil {
 		err = perr.WithMessage(err, "Timeout")
@@ -812,67 +811,6 @@ func (t *Translator) buildFaultInjection(policy *egv1a1.BackendTrafficPolicy) *i
 	return fi
 }
 
-func (t *Translator) buildRetry(policy *egv1a1.BackendTrafficPolicy) *ir.Retry {
-	var rt *ir.Retry
-	if policy.Spec.Retry != nil {
-		prt := policy.Spec.Retry
-		rt = &ir.Retry{}
-
-		if prt.NumRetries != nil {
-			rt.NumRetries = ptr.To(uint32(*prt.NumRetries))
-		}
-
-		if prt.RetryOn != nil {
-			ro := &ir.RetryOn{}
-			bro := false
-			if prt.RetryOn.HTTPStatusCodes != nil {
-				ro.HTTPStatusCodes = makeIrStatusSet(prt.RetryOn.HTTPStatusCodes)
-				bro = true
-			}
-
-			if prt.RetryOn.Triggers != nil {
-				ro.Triggers = makeIrTriggerSet(prt.RetryOn.Triggers)
-				bro = true
-			}
-
-			if bro {
-				rt.RetryOn = ro
-			}
-		}
-
-		if prt.PerRetry != nil {
-			pr := &ir.PerRetryPolicy{}
-			bpr := false
-
-			if prt.PerRetry.Timeout != nil {
-				pr.Timeout = prt.PerRetry.Timeout
-				bpr = true
-			}
-
-			if prt.PerRetry.BackOff != nil {
-				if prt.PerRetry.BackOff.MaxInterval != nil || prt.PerRetry.BackOff.BaseInterval != nil {
-					bop := &ir.BackOffPolicy{}
-					if prt.PerRetry.BackOff.MaxInterval != nil {
-						bop.MaxInterval = prt.PerRetry.BackOff.MaxInterval
-					}
-
-					if prt.PerRetry.BackOff.BaseInterval != nil {
-						bop.BaseInterval = prt.PerRetry.BackOff.BaseInterval
-					}
-					pr.BackOff = bop
-					bpr = true
-				}
-			}
-
-			if bpr {
-				rt.PerRetry = pr
-			}
-		}
-	}
-
-	return rt
-}
-
 func makeIrStatusSet(in []egv1a1.HTTPStatus) []ir.HTTPStatus {
 	statusSet := sets.NewInt()
 	for _, r := range in {
diff --git a/internal/gatewayapi/clustersettings.go b/internal/gatewayapi/clustersettings.go
index 742b02624..260c1db84 100644
--- a/internal/gatewayapi/clustersettings.go
+++ b/internal/gatewayapi/clustersettings.go
@@ -71,6 +71,8 @@ func translateTrafficFeatures(policy *egv1a1.ClusterSettings) (*ir.TrafficFeatur
 		ret.HTTP2 = h2
 	}
 
+	ret.Retry = buildRetry(policy.Retry)
+
 	// If nothing was set in any of the above calls, return nil instead of an empty
 	// container
 	var empty ir.TrafficFeatures
@@ -507,3 +509,64 @@ func translateDNS(policy egv1a1.ClusterSettings) *ir.DNS {
 		DNSRefreshRate: policy.DNS.DNSRefreshRate,
 	}
 }
+
+func buildRetry(r *egv1a1.Retry) *ir.Retry {
+	if r == nil {
+		return nil
+	}
+
+	rt := &ir.Retry{}
+
+	if r.NumRetries != nil {
+		rt.NumRetries = ptr.To(uint32(*r.NumRetries))
+	}
+
+	if r.RetryOn != nil {
+		ro := &ir.RetryOn{}
+		bro := false
+		if r.RetryOn.HTTPStatusCodes != nil {
+			ro.HTTPStatusCodes = makeIrStatusSet(r.RetryOn.HTTPStatusCodes)
+			bro = true
+		}
+
+		if r.RetryOn.Triggers != nil {
+			ro.Triggers = makeIrTriggerSet(r.RetryOn.Triggers)
+			bro = true
+		}
+
+		if bro {
+			rt.RetryOn = ro
+		}
+	}
+
+	if r.PerRetry != nil {
+		pr := &ir.PerRetryPolicy{}
+		bpr := false
+
+		if r.PerRetry.Timeout != nil {
+			pr.Timeout = r.PerRetry.Timeout
+			bpr = true
+		}
+
+		if r.PerRetry.BackOff != nil {
+			if r.PerRetry.BackOff.MaxInterval != nil || r.PerRetry.BackOff.BaseInterval != nil {
+				bop := &ir.BackOffPolicy{}
+				if r.PerRetry.BackOff.MaxInterval != nil {
+					bop.MaxInterval = r.PerRetry.BackOff.MaxInterval
+				}
+
+				if r.PerRetry.BackOff.BaseInterval != nil {
+					bop.BaseInterval = r.PerRetry.BackOff.BaseInterval
+				}
+				pr.BackOff = bop
+				bpr = true
+			}
+		}
+
+		if bpr {
+			rt.PerRetry = pr
+		}
+	}
+
+	return rt
+}
diff --git a/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go
index 473945875..5e61f2eb3 100644
--- a/internal/gatewayapi/envoyextensionpolicy.go
+++ b/internal/gatewayapi/envoyextensionpolicy.go
@@ -411,7 +411,7 @@ func (t *Translator) buildExtProcs(policy *egv1a1.EnvoyExtensionPolicy, resource
 
 	for idx, ep := range policy.Spec.ExtProc {
 		name := irConfigNameForExtProc(policy, idx)
-		extProcIR, err := t.buildExtProc(name, utils.NamespacedName(policy), ep, idx, resources, envoyProxy)
+		extProcIR, err := t.buildExtProc(name, policy, ep, idx, resources, envoyProxy)
 		if err != nil {
 			return nil, err
 		}
@@ -422,59 +422,33 @@ func (t *Translator) buildExtProcs(policy *egv1a1.EnvoyExtensionPolicy, resource
 
 func (t *Translator) buildExtProc(
 	name string,
-	policyNamespacedName types.NamespacedName,
+	policy *egv1a1.EnvoyExtensionPolicy,
 	extProc egv1a1.ExtProc,
 	extProcIdx int,
 	resources *resource.Resources,
 	envoyProxy *egv1a1.EnvoyProxy,
 ) (*ir.ExtProc, error) {
 	var (
-		ds        *ir.DestinationSetting
+		rd        *ir.RouteDestination
 		authority string
 		err       error
 	)
 
-	var dsl []*ir.DestinationSetting
-	for i := range extProc.BackendRefs {
-		if err = t.validateExtServiceBackendReference(
-			&extProc.BackendRefs[i].BackendObjectReference,
-			policyNamespacedName.Namespace,
-			egv1a1.KindEnvoyExtensionPolicy,
-			resources); err != nil {
-			return nil, err
-		}
-
-		ds, err = t.processExtServiceDestination(
-			&extProc.BackendRefs[i],
-			policyNamespacedName,
-			egv1a1.KindEnvoyExtensionPolicy,
-			ir.GRPC,
-			resources,
-			envoyProxy,
-		)
-		if err != nil {
-			return nil, err
-		}
-
-		dsl = append(dsl, ds)
-	}
-
-	rd := ir.RouteDestination{
-		Name:     irIndexedExtServiceDestinationName(policyNamespacedName, egv1a1.KindEnvoyExtensionPolicy, extProcIdx),
-		Settings: dsl,
+	if rd, err = t.translateExtServiceBackendRefs(policy, extProc.BackendRefs, ir.GRPC, resources, envoyProxy, extProcIdx); err != nil {
+		return nil, err
 	}
 
 	if extProc.BackendRefs[0].Port != nil {
 		authority = fmt.Sprintf(
 			"%s.%s:%d",
 			extProc.BackendRefs[0].Name,
-			NamespaceDerefOr(extProc.BackendRefs[0].Namespace, policyNamespacedName.Namespace),
+			NamespaceDerefOr(extProc.BackendRefs[0].Namespace, policy.Namespace),
 			*extProc.BackendRefs[0].Port)
 	} else {
 		authority = fmt.Sprintf(
 			"%s.%s",
 			extProc.BackendRefs[0].Name,
-			NamespaceDerefOr(extProc.BackendRefs[0].Namespace, policyNamespacedName.Namespace))
+			NamespaceDerefOr(extProc.BackendRefs[0].Namespace, policy.Namespace))
 	}
 
 	traffic, err := translateTrafficFeatures(extProc.BackendCluster.BackendSettings)
@@ -484,7 +458,7 @@ func (t *Translator) buildExtProc(
 
 	extProcIR := &ir.ExtProc{
 		Name:        name,
-		Destination: rd,
+		Destination: *rd,
 		Traffic:     traffic,
 		Authority:   authority,
 	}
diff --git a/internal/gatewayapi/ext_service.go b/internal/gatewayapi/ext_service.go
index 7e13b1010..e7ab19036 100644
--- a/internal/gatewayapi/ext_service.go
+++ b/internal/gatewayapi/ext_service.go
@@ -12,15 +12,66 @@ import (
 
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/utils/ptr"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
 	"github.com/envoyproxy/gateway/internal/ir"
+	"github.com/envoyproxy/gateway/internal/utils"
 )
 
-// TODO: zhaohuabing combine this function with the one in the route translator
+// translateExtServiceBackendRefs translates external service backend references to route destinations.
+func (t *Translator) translateExtServiceBackendRefs(
+	policy client.Object,
+	backendRefs []egv1a1.BackendRef,
+	protocol ir.AppProtocol,
+	resources *resource.Resources,
+	envoyProxy *egv1a1.EnvoyProxy,
+	index int, // index is used to differentiate between multiple external services in the same policy
+) (*ir.RouteDestination, error) {
+	var (
+		rs  *ir.RouteDestination
+		ds  []*ir.DestinationSetting
+		err error
+	)
+
+	if len(backendRefs) == 0 {
+		return nil, errors.New("no backendRefs found for external service")
+	}
+
+	pnn := utils.NamespacedName(policy)
+	for _, backendRef := range backendRefs {
+		if err = t.validateExtServiceBackendReference(
+			&backendRef.BackendObjectReference,
+			policy.GetNamespace(),
+			policy.GetObjectKind().GroupVersionKind().Kind,
+			resources); err != nil {
+			return nil, err
+		}
+
+		var extServiceDest *ir.DestinationSetting
+		if extServiceDest, err = t.processExtServiceDestination(
+			&backendRef,
+			pnn,
+			policy.GetObjectKind().GroupVersionKind().Kind,
+			protocol,
+			resources,
+			envoyProxy,
+		); err != nil {
+			return nil, err
+		}
+		ds = append(ds, extServiceDest)
+	}
+
+	rs = &ir.RouteDestination{
+		Name:     irIndexedExtServiceDestinationName(pnn, policy.GetObjectKind().GroupVersionKind().Kind, index),
+		Settings: ds,
+	}
+	return rs, nil
+}
+
 func (t *Translator) processExtServiceDestination(
 	backendRef *egv1a1.BackendRef,
 	policyNamespacedName types.NamespacedName,
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index 2ab9670d5..c9289bbcf 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -329,7 +329,6 @@ func (t *Translator) translateSecurityPolicyForRoute(
 	var (
 		cors          *ir.CORS
 		jwt           *ir.JWT
-		oidc          *ir.OIDC
 		basicAuth     *ir.BasicAuth
 		authorization *ir.Authorization
 		err, errs     error
@@ -343,15 +342,6 @@ func (t *Translator) translateSecurityPolicyForRoute(
 		jwt = t.buildJWT(policy.Spec.JWT)
 	}
 
-	if policy.Spec.OIDC != nil {
-		if oidc, err = t.buildOIDC(
-			policy,
-			resources); err != nil {
-			err = perr.WithMessage(err, "OIDC")
-			errs = errors.Join(errs, err)
-		}
-	}
-
 	if policy.Spec.BasicAuth != nil {
 		if basicAuth, err = t.buildBasicAuth(
 			policy,
@@ -388,6 +378,18 @@ func (t *Translator) translateSecurityPolicyForRoute(
 				errs = errors.Join(errs, err)
 			}
 		}
+
+		var oidc *ir.OIDC
+		if policy.Spec.OIDC != nil {
+			if oidc, err = t.buildOIDC(
+				policy,
+				resources,
+				gtwCtx.envoyProxy); err != nil {
+				err = perr.WithMessage(err, "OIDC")
+				errs = errors.Join(errs, err)
+			}
+		}
+
 		irKey := t.getIRKey(gtwCtx.Gateway)
 		for _, listener := range parentRefCtx.listeners {
 			irListener := xdsIR[irKey].GetHTTPListener(irListenerName(listener))
@@ -445,7 +447,8 @@ func (t *Translator) translateSecurityPolicyForGateway(
 	if policy.Spec.OIDC != nil {
 		if oidc, err = t.buildOIDC(
 			policy,
-			resources); err != nil {
+			resources,
+			gateway.envoyProxy); err != nil {
 			err = perr.WithMessage(err, "OIDC")
 			errs = errors.Join(errs, err)
 		}
@@ -566,19 +569,30 @@ func (t *Translator) buildJWT(jwt *egv1a1.JWT) *ir.JWT {
 func (t *Translator) buildOIDC(
 	policy *egv1a1.SecurityPolicy,
 	resources *resource.Resources,
+	envoyProxy *egv1a1.EnvoyProxy,
 ) (*ir.OIDC, error) {
 	var (
-		oidc         = policy.Spec.OIDC
-		clientSecret *corev1.Secret
-		provider     *ir.OIDCProvider
-		err          error
+		oidc               = policy.Spec.OIDC
+		provider           *ir.OIDCProvider
+		clientSecret       *corev1.Secret
+		redirectURL        = defaultRedirectURL
+		redirectPath       = defaultRedirectPath
+		logoutPath         = defaultLogoutPath
+		forwardAccessToken = defaultForwardAccessToken
+		refreshToken       = defaultRefreshToken
+		err                error
 	)
 
+	if provider, err = t.buildOIDCProvider(policy, resources, envoyProxy); err != nil {
+		return nil, err
+	}
+
 	from := crossNamespaceFrom{
 		group:     egv1a1.GroupName,
 		kind:      resource.KindSecurityPolicy,
 		namespace: policy.Namespace,
 	}
+
 	if clientSecret, err = t.validateSecretRef(
 		false, from, oidc.ClientSecret, resources); err != nil {
 		return nil, err
@@ -591,25 +605,8 @@ func (t *Translator) buildOIDC(
 			clientSecret.Namespace, clientSecret.Name)
 	}
 
-	// Discover the token and authorization endpoints from the issuer's
-	// well-known url if not explicitly specified
-	if provider, err = discoverEndpointsFromIssuer(&oidc.Provider); err != nil {
-		return nil, err
-	}
-
-	if err = validateTokenEndpoint(provider.TokenEndpoint); err != nil {
-		return nil, err
-	}
 	scopes := appendOpenidScopeIfNotExist(oidc.Scopes)
 
-	var (
-		redirectURL        = defaultRedirectURL
-		redirectPath       = defaultRedirectPath
-		logoutPath         = defaultLogoutPath
-		forwardAccessToken = defaultForwardAccessToken
-		refreshToken       = defaultRefreshToken
-	)
-
 	if oidc.RedirectURL != nil {
 		path, err := extractRedirectPath(*oidc.RedirectURL)
 		if err != nil {
@@ -668,6 +665,62 @@ func (t *Translator) buildOIDC(
 	}, nil
 }
 
+func (t *Translator) buildOIDCProvider(policy *egv1a1.SecurityPolicy, resources *resource.Resources, envoyProxy *egv1a1.EnvoyProxy) (*ir.OIDCProvider, error) {
+	var (
+		provider              = policy.Spec.OIDC.Provider
+		tokenEndpoint         string
+		authorizationEndpoint string
+		protocol              ir.AppProtocol
+		rd                    *ir.RouteDestination
+		traffic               *ir.TrafficFeatures
+		err                   error
+	)
+
+	// Discover the token and authorization endpoints from the issuer's
+	// well-known url if not explicitly specified
+	if provider.TokenEndpoint == nil || provider.AuthorizationEndpoint == nil {
+		tokenEndpoint, authorizationEndpoint, err = fetchEndpointsFromIssuer(provider.Issuer)
+		if err != nil {
+			return nil, fmt.Errorf("error fetching endpoints from issuer: %w", err)
+		}
+	} else {
+		tokenEndpoint = *provider.TokenEndpoint
+		authorizationEndpoint = *provider.AuthorizationEndpoint
+	}
+
+	if err = validateTokenEndpoint(tokenEndpoint); err != nil {
+		return nil, err
+	}
+
+	u, err := url.Parse(tokenEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	if u.Scheme == "https" {
+		protocol = ir.HTTPS
+	} else {
+		protocol = ir.HTTP
+	}
+
+	if len(provider.BackendRefs) > 0 {
+		if rd, err = t.translateExtServiceBackendRefs(policy, provider.BackendRefs, protocol, resources, envoyProxy, 0); err != nil {
+			return nil, err
+		}
+	}
+
+	if traffic, err = translateTrafficFeatures(provider.BackendSettings); err != nil {
+		return nil, err
+	}
+
+	return &ir.OIDCProvider{
+		Destination:           rd,
+		Traffic:               traffic,
+		AuthorizationEndpoint: authorizationEndpoint,
+		TokenEndpoint:         tokenEndpoint,
+	}, nil
+}
+
 func extractRedirectPath(redirectURL string) (string, error) {
 	schemeDelimiter := strings.Index(redirectURL, "://")
 	if schemeDelimiter <= 0 {
@@ -712,26 +765,6 @@ type OpenIDConfig struct {
 	AuthorizationEndpoint string `json:"authorization_endpoint"`
 }
 
-// discoverEndpointsFromIssuer discovers the token and authorization endpoints from the issuer's well-known url
-// return error if failed to fetch the well-known configuration
-func discoverEndpointsFromIssuer(provider *egv1a1.OIDCProvider) (*ir.OIDCProvider, error) {
-	if provider.TokenEndpoint == nil || provider.AuthorizationEndpoint == nil {
-		tokenEndpoint, authorizationEndpoint, err := fetchEndpointsFromIssuer(provider.Issuer)
-		if err != nil {
-			return nil, fmt.Errorf("error fetching endpoints from issuer: %w", err)
-		}
-		return &ir.OIDCProvider{
-			TokenEndpoint:         tokenEndpoint,
-			AuthorizationEndpoint: authorizationEndpoint,
-		}, nil
-	}
-
-	return &ir.OIDCProvider{
-		TokenEndpoint:         *provider.TokenEndpoint,
-		AuthorizationEndpoint: *provider.AuthorizationEndpoint,
-	}, nil
-}
-
 func fetchEndpointsFromIssuer(issuerURL string) (string, string, error) {
 	// Fetch the OpenID configuration from the issuer URL
 	resp, err := http.Get(fmt.Sprintf("%s/.well-known/openid-configuration", issuerURL))
@@ -811,7 +844,7 @@ func (t *Translator) buildExtAuth(policy *egv1a1.SecurityPolicy, resources *reso
 		grpc      = policy.Spec.ExtAuth.GRPC
 		backends  *egv1a1.BackendCluster
 		protocol  ir.AppProtocol
-		ds        []*ir.DestinationSetting
+		rd        *ir.RouteDestination
 		authority string
 		err       error
 		traffic   *ir.TrafficFeatures
@@ -833,12 +866,12 @@ func (t *Translator) buildExtAuth(policy *egv1a1.SecurityPolicy, resources *reso
 		backends = &grpc.BackendCluster
 		protocol = ir.GRPC
 	}
-	pnn := utils.NamespacedName(policy)
-	for _, backendRef := range backends.BackendRefs {
-		if err = t.validateExtServiceBackendReference(&backendRef.BackendObjectReference, policy.Namespace, policy.Kind, resources); err != nil {
-			return nil, err
-		}
 
+	if rd, err = t.translateExtServiceBackendRefs(policy, backends.BackendRefs, protocol, resources, envoyProxy, 0); err != nil {
+		return nil, err
+	}
+
+	for _, backendRef := range backends.BackendRefs {
 		// Authority is the calculated hostname that will be used as the Authority header.
 		// If there are multiple backend referenced, simply use the first one - there are no good answers here.
 		// When translated to XDS, the authority is used on the filter level not on the cluster level.
@@ -846,23 +879,6 @@ func (t *Translator) buildExtAuth(policy *egv1a1.SecurityPolicy, resources *reso
 		if authority == "" {
 			authority = backendRefAuthority(resources, &backendRef.BackendObjectReference, policy)
 		}
-
-		extServiceDest, err := t.processExtServiceDestination(
-			&backendRef,
-			pnn,
-			resource.KindSecurityPolicy,
-			protocol,
-			resources,
-			envoyProxy,
-		)
-		if err != nil {
-			return nil, err
-		}
-		ds = append(ds, extServiceDest)
-	}
-	rd := ir.RouteDestination{
-		Name:     irIndexedExtServiceDestinationName(pnn, resource.KindSecurityPolicy, 0),
-		Settings: ds,
 	}
 
 	if traffic, err = translateTrafficFeatures(backends.BackendSettings); err != nil {
@@ -878,14 +894,14 @@ func (t *Translator) buildExtAuth(policy *egv1a1.SecurityPolicy, resources *reso
 
 	if http != nil {
 		extAuth.HTTP = &ir.HTTPExtAuthService{
-			Destination:      rd,
+			Destination:      *rd,
 			Authority:        authority,
 			Path:             ptr.Deref(http.Path, ""),
 			HeadersToBackend: http.HeadersToBackend,
 		}
 	} else {
 		extAuth.GRPC = &ir.GRPCExtAuthService{
-			Destination: rd,
+			Destination: *rd,
 			Authority:   authority,
 		}
 	}
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 49d6fdbf0..fdcace324 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -848,7 +848,16 @@ type OIDC struct {
 	CookieDomain *string `json:"cookieDomain,omitempty"`
 }
 
+// OIDCProvider defines the schema for the OIDC Provider.
+//
+// +k8s:deepcopy-gen=true
 type OIDCProvider struct {
+	// Destination defines the destination for the OIDC Provider.
+	Destination *RouteDestination `json:"destination,omitempty"`
+
+	// Traffic contains configuration for traffic features for the OIDC Provider
+	Traffic *TrafficFeatures `json:"traffic,omitempty"`
+
 	// The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint).
 	AuthorizationEndpoint string `json:"authorizationEndpoint,omitempty"`
 
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 1a0185bbb..3c0c1135f 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -1900,7 +1900,7 @@ func (in *Metrics) DeepCopy() *Metrics {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *OIDC) DeepCopyInto(out *OIDC) {
 	*out = *in
-	out.Provider = in.Provider
+	in.Provider.DeepCopyInto(&out.Provider)
 	if in.ClientSecret != nil {
 		in, out := &in.ClientSecret, &out.ClientSecret
 		*out = make([]byte, len(*in))
@@ -1953,6 +1953,31 @@ func (in *OIDC) DeepCopy() *OIDC {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OIDCProvider) DeepCopyInto(out *OIDCProvider) {
+	*out = *in
+	if in.Destination != nil {
+		in, out := &in.Destination, &out.Destination
+		*out = new(RouteDestination)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Traffic != nil {
+		in, out := &in.Traffic, &out.Traffic
+		*out = new(TrafficFeatures)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCProvider.
+func (in *OIDCProvider) DeepCopy() *OIDCProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(OIDCProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *OpenTelemetryAccessLog) DeepCopyInto(out *OpenTelemetryAccessLog) {
 	*out = *in
diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go
index 41228a1d2..963b7c804 100644
--- a/internal/xds/translator/oidc.go
+++ b/internal/xds/translator/oidc.go
@@ -102,14 +102,24 @@ func oauth2FilterName(oidc *ir.OIDC) string {
 }
 
 func oauth2Config(oidc *ir.OIDC) (*oauth2v3.OAuth2, error) {
-	cluster, err := url2Cluster(oidc.Provider.TokenEndpoint)
-	if err != nil {
-		return nil, err
-	}
-	if cluster.endpointType == EndpointTypeStatic {
-		return nil, fmt.Errorf(
-			"static IP cluster is not allowed: %s",
-			oidc.Provider.TokenEndpoint)
+	var (
+		tokenEndpointCluster string
+		err                  error
+	)
+
+	if oidc.Provider.Destination != nil && len(oidc.Provider.Destination.Settings) > 0 {
+		tokenEndpointCluster = oidc.Provider.Destination.Name
+	} else {
+		var cluster *urlCluster
+		if cluster, err = url2Cluster(oidc.Provider.TokenEndpoint); err != nil {
+			return nil, err
+		}
+		if cluster.endpointType == EndpointTypeStatic {
+			return nil, fmt.Errorf(
+				"static IP cluster is not allowed: %s",
+				oidc.Provider.TokenEndpoint)
+		}
+		tokenEndpointCluster = cluster.name
 	}
 
 	// Envoy OAuth2 filter deletes the HTTP authorization header by default, which surprises users.
@@ -126,7 +136,7 @@ func oauth2Config(oidc *ir.OIDC) (*oauth2v3.OAuth2, error) {
 			TokenEndpoint: &corev3.HttpUri{
 				Uri: oidc.Provider.TokenEndpoint,
 				HttpUpstreamType: &corev3.HttpUri_Cluster{
-					Cluster: cluster.name,
+					Cluster: tokenEndpointCluster,
 				},
 				Timeout: &durationpb.Duration{
 					Seconds: defaultExtServiceRequestTimeout,
@@ -210,9 +220,55 @@ func oauth2Config(oidc *ir.OIDC) (*oauth2v3.OAuth2, error) {
 		oauth2.Config.Credentials.CookieDomain = *oidc.CookieDomain
 	}
 
+	// Set the retry policy if it exists.
+	if oidc.Provider.Traffic != nil && oidc.Provider.Traffic.Retry != nil {
+		var rp *corev3.RetryPolicy
+		if rp, err = buildNonRouteRetryPolicy(oidc.Provider.Traffic.Retry); err != nil {
+			return nil, err
+		}
+		oauth2.Config.RetryPolicy = rp
+	}
 	return oauth2, nil
 }
 
+func buildNonRouteRetryPolicy(rr *ir.Retry) (*corev3.RetryPolicy, error) {
+	rp := &corev3.RetryPolicy{
+		RetryOn: retryDefaultRetryOn,
+	}
+
+	// These two fields in the RetryPolicy are just for route-level retries, they are not used for non-route retries.
+	// retry.PerRetry.Timeout
+	// retry.RetryOn.HTTPStatusCodes
+
+	if rr.PerRetry != nil && rr.PerRetry.BackOff != nil {
+		rp.RetryBackOff = &corev3.BackoffStrategy{
+			BaseInterval: &durationpb.Duration{
+				Seconds: int64(rr.PerRetry.BackOff.BaseInterval.Seconds()),
+			},
+			MaxInterval: &durationpb.Duration{
+				Seconds: int64(rr.PerRetry.BackOff.MaxInterval.Seconds()),
+			},
+		}
+	}
+
+	if rr.NumRetries != nil {
+		rp.NumRetries = &wrappers.UInt32Value{
+			Value: *rr.NumRetries,
+		}
+	}
+
+	if rr.RetryOn != nil {
+		if len(rr.RetryOn.Triggers) > 0 {
+			if ro, err := buildRetryOn(rr.RetryOn.Triggers); err == nil {
+				rp.RetryOn = ro
+			} else {
+				return nil, err
+			}
+		}
+	}
+	return rp, nil
+}
+
 // routeContainsOIDC returns true if OIDC exists for the provided route.
 func routeContainsOIDC(irRoute *ir.HTTPRoute) bool {
 	if irRoute != nil &&
@@ -226,7 +282,7 @@ func routeContainsOIDC(irRoute *ir.HTTPRoute) bool {
 func (*oidc) patchResources(tCtx *types.ResourceVersionTable,
 	routes []*ir.HTTPRoute,
 ) error {
-	if err := createOAuth2TokenEndpointClusters(tCtx, routes); err != nil {
+	if err := createOAuthServerClusters(tCtx, routes); err != nil {
 		return err
 	}
 	if err := createOAuth2Secrets(tCtx, routes); err != nil {
@@ -235,9 +291,8 @@ func (*oidc) patchResources(tCtx *types.ResourceVersionTable,
 	return nil
 }
 
-// createOAuth2TokenEndpointClusters creates token endpoint clusters from the
-// provided routes, if needed.
-func createOAuth2TokenEndpointClusters(tCtx *types.ResourceVersionTable,
+// createOAuthServerClusters creates clusters for the OAuth2 server.
+func createOAuthServerClusters(tCtx *types.ResourceVersionTable,
 	routes []*ir.HTTPRoute,
 ) error {
 	if tCtx == nil || tCtx.XdsResources == nil {
@@ -250,59 +305,78 @@ func createOAuth2TokenEndpointClusters(tCtx *types.ResourceVersionTable,
 			continue
 		}
 
-		var (
-			cluster *urlCluster
-			ds      *ir.DestinationSetting
-			tSocket *corev3.TransportSocket
-			err     error
-		)
+		oidc := route.Security.OIDC
 
-		cluster, err = url2Cluster(route.Security.OIDC.Provider.TokenEndpoint)
-		if err != nil {
-			errs = errors.Join(errs, err)
-			continue
+		// If the OIDC provider has a destination, use it.
+		if oidc.Provider.Destination != nil && len(oidc.Provider.Destination.Settings) > 0 {
+			if err := createExtServiceXDSCluster(
+				oidc.Provider.Destination, oidc.Provider.Traffic, tCtx); err != nil && !errors.Is(
+				err, ErrXdsClusterExists) {
+				errs = errors.Join(errs, err)
+			}
+		} else {
+			// Create a cluster with the token endpoint url.
+			if err := createOAuth2TokenEndpointCluster(tCtx, oidc.Provider.TokenEndpoint); err != nil {
+				errs = errors.Join(errs, err)
+			}
 		}
+	}
 
-		// EG does not support static IP clusters for token endpoint clusters.
-		// This validation could be removed since it's already validated in the
-		// Gateway API translator.
-		if cluster.endpointType == EndpointTypeStatic {
-			errs = errors.Join(errs, fmt.Errorf(
-				"static IP cluster is not allowed: %s",
-				route.Security.OIDC.Provider.TokenEndpoint))
-			continue
-		}
+	return errs
+}
 
-		ds = &ir.DestinationSetting{
-			Weight: ptr.To[uint32](1),
-			Endpoints: []*ir.DestinationEndpoint{
-				ir.NewDestEndpoint(
-					cluster.hostname,
-					cluster.port),
-			},
-		}
+// createOAuth2TokenEndpointClusters creates token endpoint clusters from the
+// provided routes, if needed.
+func createOAuth2TokenEndpointCluster(tCtx *types.ResourceVersionTable,
+	tokenEndpoint string,
+) error {
+	var (
+		cluster *urlCluster
+		ds      *ir.DestinationSetting
+		tSocket *corev3.TransportSocket
+		err     error
+	)
+
+	if cluster, err = url2Cluster(tokenEndpoint); err != nil {
+		return err
+	}
 
-		clusterArgs := &xdsClusterArgs{
-			name:         cluster.name,
-			settings:     []*ir.DestinationSetting{ds},
-			tSocket:      tSocket,
-			endpointType: cluster.endpointType,
-		}
-		if cluster.tls {
-			tSocket, err = buildXdsUpstreamTLSSocket(cluster.hostname)
-			if err != nil {
-				errs = errors.Join(errs, err)
-				continue
-			}
-			clusterArgs.tSocket = tSocket
-		}
+	// EG does not support static IP clusters for token endpoint clusters.
+	// This validation could be removed since it's already validated in the
+	// Gateway API translator.
+	if cluster.endpointType == EndpointTypeStatic {
+		return fmt.Errorf(
+			"static IP cluster is not allowed: %s",
+			tokenEndpoint)
+	}
 
-		if err = addXdsCluster(tCtx, clusterArgs); err != nil && !errors.Is(err, ErrXdsClusterExists) {
-			errs = errors.Join(errs, err)
+	ds = &ir.DestinationSetting{
+		Weight: ptr.To[uint32](1),
+		Endpoints: []*ir.DestinationEndpoint{
+			ir.NewDestEndpoint(
+				cluster.hostname,
+				cluster.port),
+		},
+	}
+
+	clusterArgs := &xdsClusterArgs{
+		name:         cluster.name,
+		settings:     []*ir.DestinationSetting{ds},
+		tSocket:      tSocket,
+		endpointType: cluster.endpointType,
+	}
+	if cluster.tls {
+		if tSocket, err = buildXdsUpstreamTLSSocket(cluster.hostname); err != nil {
+			return err
 		}
+		clusterArgs.tSocket = tSocket
 	}
 
-	return errs
+	if err = addXdsCluster(tCtx, clusterArgs); err != nil && !errors.Is(err, ErrXdsClusterExists) {
+		return err
+	}
+
+	return err
 }
 
 // createOAuth2Secrets creates OAuth2 client and HMAC secrets from the provided


Test patch:
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-backendcluster.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-backendcluster.in.yaml
new file mode 100644
index 000000000..67b051e4b
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-backendcluster.in.yaml
@@ -0,0 +1,101 @@
+secrets:
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: envoy-gateway
+    name: client1-secret
+  data:
+    client-secret: Y2xpZW50MTpzZWNyZXQK
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: envoy-gateway-system
+    name: envoy-oidc-hmac
+  data:
+    hmac-secret: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY=
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - www.example.com
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/foo"
+      backendRefs:
+      - name: service-1
+        port: 8080
+backends:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: Backend
+  metadata:
+    name: backend-fqdn
+    namespace: envoy-gateway
+  spec:
+    endpoints:
+    - fqdn:
+        hostname: 'oauth.foo.com'
+        port: 443
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: envoy-gateway
+    name: policy-for-gateway
+    uid: b8284d0f-de82-4c65-b204-96a0d3f258a1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+    oidc:
+      provider:
+        backendRefs:
+        - group: gateway.envoyproxy.io
+          kind: Backend
+          name: backend-fqdn
+          port: 443
+        backendSettings:
+          retry:
+            numRetries: 3
+            perRetry:
+              backOff:
+                baseInterval: 1s
+                maxInterval: 5s
+            retryOn:
+              triggers: ["5xx", "gateway-error", "reset"]
+        issuer: "https://oauth.foo.com"
+        authorizationEndpoint: "https://oauth.foo.com/oauth2/v2/auth"
+        tokenEndpoint: "https://oauth.foo.com/token"
+      clientID: "client1.apps.googleusercontent.com"
+      clientSecret:
+        name: "client1-secret"
+      redirectURL: "https://www.example.com/bar/oauth2/callback"
+      logoutPath: "/bar/logout"
+      forwardAccessToken: true
+      defaultTokenTTL: 30m
+      refreshToken: true
+      defaultRefreshTokenTTL: 24h
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-backendcluster.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-backendcluster.out.yaml
new file mode 100644
index 000000000..d6f0c4dbc
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-backendcluster.out.yaml
@@ -0,0 +1,256 @@
+backends:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: Backend
+  metadata:
+    creationTimestamp: null
+    name: backend-fqdn
+    namespace: envoy-gateway
+  spec:
+    endpoints:
+    - fqdn:
+        hostname: oauth.foo.com
+        port: 443
+  status:
+    conditions:
+    - lastTransitionTime: null
+      message: The Backend was accepted
+      reason: Accepted
+      status: "True"
+      type: Accepted
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - www.example.com
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /foo
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-1/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-gateway
+    namespace: envoy-gateway
+    uid: b8284d0f-de82-4c65-b204-96a0d3f258a1
+  spec:
+    oidc:
+      clientID: client1.apps.googleusercontent.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client1-secret
+      defaultRefreshTokenTTL: 24h0m0s
+      defaultTokenTTL: 30m0s
+      forwardAccessToken: true
+      logoutPath: /bar/logout
+      provider:
+        authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+        backendRefs:
+        - group: gateway.envoyproxy.io
+          kind: Backend
+          name: backend-fqdn
+          port: 443
+        backendSettings:
+          retry:
+            numRetries: 3
+            perRetry:
+              backOff:
+                baseInterval: 1s
+                maxInterval: 5s
+            retryOn:
+              triggers:
+              - 5xx
+              - gateway-error
+              - reset
+        issuer: https://oauth.foo.com
+        tokenEndpoint: https://oauth.foo.com/token
+      redirectURL: https://www.example.com/bar/oauth2/callback
+      refreshToken: true
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: HTTP
+            weight: 1
+        hostname: www.example.com
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/www_example_com
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /foo
+        security:
+          oidc:
+            clientID: client1.apps.googleusercontent.com
+            clientSecret: Y2xpZW50MTpzZWNyZXQK
+            cookieSuffix: b0a1b740
+            defaultRefreshTokenTTL: 24h0m0s
+            defaultTokenTTL: 30m0s
+            forwardAccessToken: true
+            hmacSecret: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY=
+            logoutPath: /bar/logout
+            name: securitypolicy/envoy-gateway/policy-for-gateway
+            provider:
+              authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+              destination:
+                name: securitypolicy/envoy-gateway/policy-for-gateway/0
+                settings:
+                - addressType: FQDN
+                  endpoints:
+                  - host: oauth.foo.com
+                    port: 443
+                  protocol: HTTPS
+                  weight: 1
+              tokenEndpoint: https://oauth.foo.com/token
+              traffic:
+                retry:
+                  numRetries: 3
+                  perRetry:
+                    backOff:
+                      baseInterval: 1s
+                      maxInterval: 5s
+                  retryOn:
+                    triggers:
+                    - 5xx
+                    - gateway-error
+                    - reset
+            redirectPath: /bar/oauth2/callback
+            redirectURL: https://www.example.com/bar/oauth2/callback
+            refreshToken: true
+            scopes:
+            - openid
diff --git a/internal/xds/translator/testdata/in/xds-ir/oidc-backend-cluster-provider.yaml b/internal/xds/translator/testdata/in/xds-ir/oidc-backend-cluster-provider.yaml
new file mode 100644
index 000000000..6fc9a045b
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/oidc-backend-cluster-provider.yaml
@@ -0,0 +1,60 @@
+http:
+- name: "first-listener"
+  address: "0.0.0.0"
+  port: 10080
+  hostnames:
+  - "*"
+  path:
+    mergeSlashes: true
+    escapedSlashesAction: UnescapeAndRedirect
+  routes:
+  - name: "first-route"
+    hostname: "*"
+    pathMatch:
+      exact: "baz"
+    destination:
+      name: "third-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.2.3.4"
+          port: 50000
+    security:
+      oidc:
+        name: securitypolicy/envoy-gateway/policy-for-gateway
+        clientID: client1.apps.googleusercontent.com
+        clientSecret: Y2xpZW50MTpzZWNyZXQK
+        cookieSuffix: b0a1b740
+        defaultRefreshTokenTTL: 24h0m0s
+        defaultTokenTTL: 30m0s
+        forwardAccessToken: true
+        hmacSecret: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY=
+        logoutPath: /bar/logout
+        provider:
+          authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+          destination:
+            name: securitypolicy/envoy-gateway/policy-for-gateway/0
+            settings:
+            - addressType: FQDN
+              endpoints:
+              - host: oauth.foo.com
+                port: 443
+              protocol: HTTPS
+              weight: 1
+          tokenEndpoint: https://oauth.foo.com/token
+          traffic:
+            retry:
+              numRetries: 3
+              perRetry:
+                backOff:
+                  baseInterval: 1s
+                  maxInterval: 5s
+              retryOn:
+                triggers:
+                - "5xx"
+                - gateway-error
+                - reset
+        redirectPath: /bar/oauth2/callback
+        redirectURL: https://www.example.com/bar/oauth2/callback
+        refreshToken: true
+        scopes:
+        - openid
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.clusters.yaml
new file mode 100644
index 000000000..863e761bf
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.clusters.yaml
@@ -0,0 +1,44 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_ONLY
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: third-route-dest
+  lbPolicy: LEAST_REQUEST
+  name: third-route-dest
+  outlierDetection: {}
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_ONLY
+  dnsRefreshRate: 30s
+  lbPolicy: LEAST_REQUEST
+  loadAssignment:
+    clusterName: securitypolicy/envoy-gateway/policy-for-gateway/0
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: oauth.foo.com
+              portValue: 443
+        loadBalancingWeight: 1
+      loadBalancingWeight: 1
+      locality:
+        region: securitypolicy/envoy-gateway/policy-for-gateway/0/backend/0
+  name: securitypolicy/envoy-gateway/policy-for-gateway/0
+  outlierDetection: {}
+  perConnectionBufferLimitBytes: 32768
+  respectDnsTtl: true
+  type: STRICT_DNS
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.endpoints.yaml
new file mode 100644
index 000000000..6c69841c2
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.endpoints.yaml
@@ -0,0 +1,12 @@
+- clusterName: third-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.2.3.4
+            portValue: 50000
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: third-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.listeners.yaml
new file mode 100644
index 000000000..3addb2944
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.listeners.yaml
@@ -0,0 +1,83 @@
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - disabled: true
+          name: envoy.filters.http.oauth2/securitypolicy/envoy-gateway/policy-for-gateway
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2
+            config:
+              authScopes:
+              - openid
+              authType: BASIC_AUTH
+              authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+              credentials:
+                clientId: client1.apps.googleusercontent.com
+                cookieNames:
+                  bearerToken: AccessToken-b0a1b740
+                  idToken: IdToken-b0a1b740
+                  oauthExpires: OauthExpires-b0a1b740
+                  oauthHmac: OauthHMAC-b0a1b740
+                  oauthNonce: OauthNonce-b0a1b740
+                  refreshToken: RefreshToken-b0a1b740
+                hmacSecret:
+                  name: oauth2/hmac_secret/securitypolicy/envoy-gateway/policy-for-gateway
+                  sdsConfig:
+                    ads: {}
+                    resourceApiVersion: V3
+                tokenSecret:
+                  name: oauth2/client_secret/securitypolicy/envoy-gateway/policy-for-gateway
+                  sdsConfig:
+                    ads: {}
+                    resourceApiVersion: V3
+              defaultExpiresIn: 1800s
+              defaultRefreshTokenExpiresIn: 86400s
+              forwardBearerToken: true
+              redirectPathMatcher:
+                path:
+                  exact: /bar/oauth2/callback
+              redirectUri: https://www.example.com/bar/oauth2/callback
+              retryPolicy:
+                numRetries: 3
+                retryBackOff:
+                  baseInterval: 1s
+                  maxInterval: 5s
+                retryOn: 5xx,gateway-error,reset
+              signoutPath:
+                path:
+                  exact: /bar/logout
+              tokenEndpoint:
+                cluster: securitypolicy/envoy-gateway/policy-for-gateway/0
+                timeout: 10s
+                uri: https://oauth.foo.com/token
+              useRefreshToken: true
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: first-listener
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: first-listener
+  name: first-listener
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.routes.yaml
new file mode 100644
index 000000000..b17df8647
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.routes.yaml
@@ -0,0 +1,18 @@
+- ignorePortInHostMatching: true
+  name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener/*
+    routes:
+    - match:
+        path: baz
+      name: first-route
+      route:
+        cluster: third-route-dest
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.oauth2/securitypolicy/envoy-gateway/policy-for-gateway:
+          '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+          config: {}
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.secrets.yaml
new file mode 100644
index 000000000..398ab6cef
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backencluster-provider.secrets.yaml
@@ -0,0 +1,8 @@
+- genericSecret:
+    secret:
+      inlineBytes: Y2xpZW50MTpzZWNyZXQK
+  name: oauth2/client_secret/securitypolicy/envoy-gateway/policy-for-gateway
+- genericSecret:
+    secret:
+      inlineBytes: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY=
+  name: oauth2/hmac_secret/securitypolicy/envoy-gateway/policy-for-gateway
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.clusters.yaml
new file mode 100644
index 000000000..863e761bf
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.clusters.yaml
@@ -0,0 +1,44 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_ONLY
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: third-route-dest
+  lbPolicy: LEAST_REQUEST
+  name: third-route-dest
+  outlierDetection: {}
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_ONLY
+  dnsRefreshRate: 30s
+  lbPolicy: LEAST_REQUEST
+  loadAssignment:
+    clusterName: securitypolicy/envoy-gateway/policy-for-gateway/0
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: oauth.foo.com
+              portValue: 443
+        loadBalancingWeight: 1
+      loadBalancingWeight: 1
+      locality:
+        region: securitypolicy/envoy-gateway/policy-for-gateway/0/backend/0
+  name: securitypolicy/envoy-gateway/policy-for-gateway/0
+  outlierDetection: {}
+  perConnectionBufferLimitBytes: 32768
+  respectDnsTtl: true
+  type: STRICT_DNS
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.endpoints.yaml
new file mode 100644
index 000000000..6c69841c2
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.endpoints.yaml
@@ -0,0 +1,12 @@
+- clusterName: third-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.2.3.4
+            portValue: 50000
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: third-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.listeners.yaml
new file mode 100644
index 000000000..3addb2944
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.listeners.yaml
@@ -0,0 +1,83 @@
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - disabled: true
+          name: envoy.filters.http.oauth2/securitypolicy/envoy-gateway/policy-for-gateway
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2
+            config:
+              authScopes:
+              - openid
+              authType: BASIC_AUTH
+              authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+              credentials:
+                clientId: client1.apps.googleusercontent.com
+                cookieNames:
+                  bearerToken: AccessToken-b0a1b740
+                  idToken: IdToken-b0a1b740
+                  oauthExpires: OauthExpires-b0a1b740
+                  oauthHmac: OauthHMAC-b0a1b740
+                  oauthNonce: OauthNonce-b0a1b740
+                  refreshToken: RefreshToken-b0a1b740
+                hmacSecret:
+                  name: oauth2/hmac_secret/securitypolicy/envoy-gateway/policy-for-gateway
+                  sdsConfig:
+                    ads: {}
+                    resourceApiVersion: V3
+                tokenSecret:
+                  name: oauth2/client_secret/securitypolicy/envoy-gateway/policy-for-gateway
+                  sdsConfig:
+                    ads: {}
+                    resourceApiVersion: V3
+              defaultExpiresIn: 1800s
+              defaultRefreshTokenExpiresIn: 86400s
+              forwardBearerToken: true
+              redirectPathMatcher:
+                path:
+                  exact: /bar/oauth2/callback
+              redirectUri: https://www.example.com/bar/oauth2/callback
+              retryPolicy:
+                numRetries: 3
+                retryBackOff:
+                  baseInterval: 1s
+                  maxInterval: 5s
+                retryOn: 5xx,gateway-error,reset
+              signoutPath:
+                path:
+                  exact: /bar/logout
+              tokenEndpoint:
+                cluster: securitypolicy/envoy-gateway/policy-for-gateway/0
+                timeout: 10s
+                uri: https://oauth.foo.com/token
+              useRefreshToken: true
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        mergeSlashes: true
+        normalizePath: true
+        pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: first-listener
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http-10080
+        useRemoteAddress: true
+    name: first-listener
+  name: first-listener
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.routes.yaml
new file mode 100644
index 000000000..b17df8647
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.routes.yaml
@@ -0,0 +1,18 @@
+- ignorePortInHostMatching: true
+  name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener/*
+    routes:
+    - match:
+        path: baz
+      name: first-route
+      route:
+        cluster: third-route-dest
+        upgradeConfigs:
+        - upgradeType: websocket
+      typedPerFilterConfig:
+        envoy.filters.http.oauth2/securitypolicy/envoy-gateway/policy-for-gateway:
+          '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+          config: {}
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.secrets.yaml
new file mode 100644
index 000000000..398ab6cef
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.secrets.yaml
@@ -0,0 +1,8 @@
+- genericSecret:
+    secret:
+      inlineBytes: Y2xpZW50MTpzZWNyZXQK
+  name: oauth2/client_secret/securitypolicy/envoy-gateway/policy-for-gateway
+- genericSecret:
+    secret:
+      inlineBytes: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY=
+  name: oauth2/hmac_secret/securitypolicy/envoy-gateway/policy-for-gateway
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 8a54fb2d6..fe361099a 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -866,7 +866,7 @@ _Appears in:_
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
 | `contentType` | _string_ |  false  | Content Type of the response. This will be set in the Content-Type header. |
-| `body` | _[CustomResponseBody](#customresponsebody)_ |  true  | Body of the Custom Response |
+| `body` | _[CustomResponseBody](#customresponsebody)_ |  false  | Body of the Custom Response |
 
 
 #### CustomResponseBody
@@ -877,6 +877,7 @@ CustomResponseBody
 
 _Appears in:_
 - [CustomResponse](#customresponse)
+- [HTTPDirectResponseFilter](#httpdirectresponsefilter)
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
@@ -1922,6 +1923,22 @@ _Appears in:_
 | `idleTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ |  false  | IdleTimeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection.<br />Default: 1 hour. |
 
 
+#### HTTPDirectResponseFilter
+
+
+
+HTTPDirectResponseFilter defines the configuration to return a fixed response.
+
+_Appears in:_
+- [HTTPRouteFilterSpec](#httproutefilterspec)
+
+| Field | Type | Required | Description |
+| ---   | ---  | ---      | ---         |
+| `contentType` | _string_ |  false  | Content Type of the response. This will be set in the Content-Type header. |
+| `body` | _[CustomResponseBody](#customresponsebody)_ |  false  | Body of the Response |
+| `statusCode` | _integer_ |  false  | Status Code of the HTTP response<br />If unset, defaults to 200. |
+
+
 #### HTTPExtAuthService
 
 
@@ -2045,6 +2062,7 @@ _Appears in:_
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
 | `urlRewrite` | _[HTTPURLRewriteFilter](#httpurlrewritefilter)_ |  false  |  |
+| `directResponse` | _[HTTPDirectResponseFilter](#httpdirectresponsefilter)_ |  false  |  |
 
 
 #### HTTPStatus
diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md
index 8a54fb2d6..fe361099a 100644
--- a/site/content/zh/latest/api/extension_types.md
+++ b/site/content/zh/latest/api/extension_types.md
@@ -866,7 +866,7 @@ _Appears in:_
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
 | `contentType` | _string_ |  false  | Content Type of the response. This will be set in the Content-Type header. |
-| `body` | _[CustomResponseBody](#customresponsebody)_ |  true  | Body of the Custom Response |
+| `body` | _[CustomResponseBody](#customresponsebody)_ |  false  | Body of the Custom Response |
 
 
 #### CustomResponseBody
@@ -877,6 +877,7 @@ CustomResponseBody
 
 _Appears in:_
 - [CustomResponse](#customresponse)
+- [HTTPDirectResponseFilter](#httpdirectresponsefilter)
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
@@ -1922,6 +1923,22 @@ _Appears in:_
 | `idleTimeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.Duration)_ |  false  | IdleTimeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection.<br />Default: 1 hour. |
 
 
+#### HTTPDirectResponseFilter
+
+
+
+HTTPDirectResponseFilter defines the configuration to return a fixed response.
+
+_Appears in:_
+- [HTTPRouteFilterSpec](#httproutefilterspec)
+
+| Field | Type | Required | Description |
+| ---   | ---  | ---      | ---         |
+| `contentType` | _string_ |  false  | Content Type of the response. This will be set in the Content-Type header. |
+| `body` | _[CustomResponseBody](#customresponsebody)_ |  false  | Body of the Response |
+| `statusCode` | _integer_ |  false  | Status Code of the HTTP response<br />If unset, defaults to 200. |
+
+
 #### HTTPExtAuthService
 
 
@@ -2045,6 +2062,7 @@ _Appears in:_
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
 | `urlRewrite` | _[HTTPURLRewriteFilter](#httpurlrewritefilter)_ |  false  |  |
+| `directResponse` | _[HTTPDirectResponseFilter](#httpdirectresponsefilter)_ |  false  |  |
 
 
 #### HTTPStatus
diff --git a/test/cel-validation/securitypolicy_test.go b/test/cel-validation/securitypolicy_test.go
index c4efa812d..f00ee8426 100644
--- a/test/cel-validation/securitypolicy_test.go
+++ b/test/cel-validation/securitypolicy_test.go
@@ -18,6 +18,7 @@ import (
 	"k8s.io/utils/ptr"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 )
@@ -1121,6 +1122,103 @@ func TestSecurityPolicyTarget(t *testing.T) {
 			},
 			wantErrors: []string{"at least one of claims or scopes must be specified"},
 		},
+		{
+			desc: "oidc-retry",
+			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",
+								},
+							},
+						},
+					},
+					OIDC: &egv1a1.OIDC{
+						Provider: egv1a1.OIDCProvider{
+							BackendCluster: egv1a1.BackendCluster{
+								BackendSettings: &egv1a1.ClusterSettings{
+									Retry: &egv1a1.Retry{
+										NumRetries: ptr.To(int32(3)),
+										PerRetry: &egv1a1.PerRetryPolicy{
+											BackOff: &egv1a1.BackOffPolicy{
+												BaseInterval: &metav1.Duration{
+													Duration: time.Second * 1,
+												},
+												MaxInterval: &metav1.Duration{
+													Duration: time.Second * 10,
+												},
+											},
+										},
+										RetryOn: &egv1a1.RetryOn{
+											Triggers: []egv1a1.TriggerEnum{
+												egv1a1.Error5XX, egv1a1.GatewayError, egv1a1.Reset,
+											},
+										},
+									},
+								},
+							},
+							Issuer:                "https://accounts.google.com",
+							AuthorizationEndpoint: ptr.To("https://accounts.google.com/o/oauth2/v2/auth"),
+							TokenEndpoint:         ptr.To("https://oauth2.googleapis.com/token"),
+						},
+						ClientID: "client-id",
+						ClientSecret: gwapiv1b1.SecretObjectReference{
+							Name: "secret",
+						},
+					},
+				}
+			},
+			wantErrors: []string{},
+		},
+		{
+			desc: "oidc-retry-unsupported-parameters",
+			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",
+								},
+							},
+						},
+					},
+					OIDC: &egv1a1.OIDC{
+						Provider: egv1a1.OIDCProvider{
+							BackendCluster: egv1a1.BackendCluster{
+								BackendSettings: &egv1a1.ClusterSettings{
+									Retry: &egv1a1.Retry{
+										NumRetries: ptr.To(int32(3)),
+										PerRetry: &egv1a1.PerRetryPolicy{
+											Timeout: &metav1.Duration{
+												Duration: time.Second * 10,
+											},
+										},
+										RetryOn: &egv1a1.RetryOn{
+											HTTPStatusCodes: []egv1a1.HTTPStatus{500},
+										},
+									},
+								},
+							},
+							Issuer:                "https://accounts.google.com",
+							AuthorizationEndpoint: ptr.To("https://accounts.google.com/o/oauth2/v2/auth"),
+							TokenEndpoint:         ptr.To("https://oauth2.googleapis.com/token"),
+						},
+						ClientID: "client-id",
+						ClientSecret: gwapiv1b1.SecretObjectReference{
+							Name: "secret",
+						},
+					},
+				}
+			},
+			wantErrors: []string{"Retry timeout is not supported", "HTTPStatusCodes is not supported"},
+		},
 	}
 
 	for _, tc := range cases {
diff --git a/test/e2e/testdata/oidc-securitypolicy-backendcluster.yaml b/test/e2e/testdata/oidc-securitypolicy-backendcluster.yaml
new file mode 100644
index 000000000..2e1a86791
--- /dev/null
+++ b/test/e2e/testdata/oidc-securitypolicy-backendcluster.yaml
@@ -0,0 +1,86 @@
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-with-oidc
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+  - name: same-namespace
+  hostnames: ["www.example.com"]
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /myapp              # This is the path that will be protected by OIDC
+    backendRefs:
+    - name: infra-backend-v1
+      port: 8080
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: http-keycloak
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+  - name: same-namespace
+  hostnames: ["keycloak.gateway-conformance-infra"]
+  rules:
+  - backendRefs:
+    - name: keycloak
+      port: 80
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  namespace: gateway-conformance-infra
+  name: oidctest-secret
+data:
+  client-secret: b2lkY3Rlc3QtY2xpZW50LXNlY3JldA==   # base64 encoding of "oidctest-client-secret"
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: SecurityPolicy
+metadata:
+  name: oidc-test       # Test OIDC Provider represented by a backend cluster
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: http-with-oidc
+  oidc:
+    provider:
+      backendRefs:
+      - group: gateway.envoyproxy.io
+        kind: Backend
+        name: backend-keycloak
+        port: 80
+      backendSettings:
+        retry:
+          numRetries: 3
+          perRetry:
+            backOff:
+              baseInterval: 1s
+              maxInterval: 5s
+          retryOn:
+            triggers: ["5xx", "gateway-error", "reset"]
+      issuer: "http://keycloak.gateway-conformance-infra/realms/master"
+      authorizationEndpoint: "http://keycloak.gateway-conformance-infra/realms/master/protocol/openid-connect/auth"
+      tokenEndpoint: "http://keycloak.gateway-conformance-infra/realms/master/protocol/openid-connect/token"
+    clientID: "oidctest"
+    clientSecret:
+      name: "oidctest-secret"
+    redirectURL: "http://www.example.com/myapp/oauth2/callback"
+    logoutPath: "/myapp/logout"
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: Backend
+metadata:
+  name: backend-keycloak
+  namespace: gateway-conformance-infra
+spec:
+  endpoints:
+  - fqdn:
+      hostname: 'keycloak.gateway-conformance-infra'
+      port: 80
diff --git a/test/e2e/tests/oidc-backendcluster.go b/test/e2e/tests/oidc-backendcluster.go
new file mode 100644
index 000000000..b2bcc93ce
--- /dev/null
+++ b/test/e2e/tests/oidc-backendcluster.go
@@ -0,0 +1,37 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+// +build e2e
+
+package tests
+
+import (
+	"testing"
+
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, OIDCBackendClusterTest)
+}
+
+// OIDCTest tests OIDC authentication for an http route with OIDC configured.
+// The http route points to an application to verify that OIDC authentication works on application/http path level.
+var OIDCBackendClusterTest = suite.ConformanceTest{
+	ShortName:   "OIDC with BackendCluster",
+	Description: "Test OIDC authentication",
+	Manifests:   []string{"testdata/oidc-keycloak.yaml", "testdata/oidc-securitypolicy-backendcluster.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		t.Run("oidc provider represented by a BackendCluster", func(t *testing.T) {
+			// Add a function to dump current cluster status
+			t.Cleanup(func() {
+				CollectAndDump(t, suite.RestConfig)
+			})
+
+			testOIDC(t, suite)
+		})
+	},
+}
diff --git a/test/e2e/tests/oidc.go b/test/e2e/tests/oidc.go
index 27fb79be9..f03512c1e 100644
--- a/test/e2e/tests/oidc.go
+++ b/test/e2e/tests/oidc.go
@@ -31,8 +31,6 @@ import (
 )
 
 const (
-	testURL             = "http://www.example.com/myapp"
-	logoutURL           = "http://www.example.com/myapp/logout"
 	keyCloakLoginFormID = "kc-form-login"
 	username            = "oidcuser"
 	password            = "oidcpassword"
@@ -49,100 +47,13 @@ var OIDCTest = suite.ConformanceTest{
 	Description: "Test OIDC authentication",
 	Manifests:   []string{"testdata/oidc-keycloak.yaml", "testdata/oidc-securitypolicy.yaml"},
 	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
-		t.Run("http route with oidc authentication", func(t *testing.T) {
+		t.Run("oidc provider represented by a URL", func(t *testing.T) {
 			// Add a function to dump current cluster status
 			t.Cleanup(func() {
 				CollectAndDump(t, suite.RestConfig)
 			})
-			ns := "gateway-conformance-infra"
-			routeNN := types.NamespacedName{Name: "http-with-oidc", Namespace: ns}
-			gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
-			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
-
-			ancestorRef := gwapiv1a2.ParentReference{
-				Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
-				Kind:      gatewayapi.KindPtr(resource.KindGateway),
-				Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
-				Name:      gwapiv1.ObjectName(gwNN.Name),
-			}
-			SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "oidc-test", Namespace: ns}, suite.ControllerName, ancestorRef)
 
-			podInitialized := corev1.PodCondition{Type: corev1.PodInitialized, Status: corev1.ConditionTrue}
-
-			// Wait for the keycloak pod to be configured with the test user and client
-			WaitForPods(t, suite.Client, ns, map[string]string{"job-name": "setup-keycloak"}, corev1.PodSucceeded, podInitialized)
-
-			// Initialize the test OIDC client that will keep track of the state of the OIDC login process
-			client, err := NewOIDCTestClient(
-				WithLoggingOptions(t.Log, true),
-				// Map the application and keycloak cluster DNS name to the gateway address
-				WithCustomAddressMappings(map[string]string{
-					"www.example.com:80":                    gwAddr,
-					"keycloak.gateway-conformance-infra:80": gwAddr,
-				}),
-			)
-			require.NoError(t, err)
-
-			if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, 5*time.Minute, true,
-				func(_ context.Context) (done bool, err error) {
-					tlog.Logf(t, "sending request to %s", testURL)
-
-					// Send a request to the http route with OIDC configured.
-					// It will be redirected to the keycloak login page
-					res, err := client.Get(testURL, true)
-					require.NoError(t, err, "Failed to get the login page")
-					require.Equal(t, 200, res.StatusCode, "Expected 200 OK")
-
-					// Parse the response body to get the URL where the login page would post the user-entered credentials
-					if err := client.ParseLoginForm(res.Body, keyCloakLoginFormID); err != nil {
-						tlog.Logf(t, "failed to parse login form: %v", err)
-						return false, nil
-					}
-
-					t.Log("successfully parsed login form")
-					return true, nil
-				}); err != nil {
-				t.Errorf("failed to parse login form: %v", err)
-			}
-
-			// Submit the login form to the IdP.
-			// This will authenticate and redirect back to the application
-			res, err := client.Login(map[string]string{"username": username, "password": password, "credentialId": ""})
-			require.NoError(t, err, "Failed to login to the IdP")
-
-			// Verify that we get the expected response from the application
-			body, err := io.ReadAll(res.Body)
-			require.NoError(t, err)
-			require.Equal(t, http.StatusOK, res.StatusCode)
-			require.Contains(t, string(body), "infra-backend-v1", "Expected response from the application")
-
-			// Verify that we can access the application without logging in again
-			res, err = client.Get(testURL, false)
-			require.NoError(t, err)
-			require.Equal(t, http.StatusOK, res.StatusCode)
-			require.Contains(t, string(body), "infra-backend-v1", "Expected response from the application")
-
-			// Verify that we can logout
-			// Note: OAuth2 filter just clears its cookies and does not log out from the IdP.
-			res, err = client.Get(logoutURL, false)
-			require.NoError(t, err)
-			require.Equal(t, http.StatusFound, res.StatusCode)
-
-			// After logout, OAuth2 filter will redirect back to the root of the host, e.g, "www.example.com".
-			// Ideally, this should redirect to the application's root, e.g, "www.example.com/myapp",
-			// but Envoy OAuth2 filter does not support this yet.
-			require.Equal(t, "http://www.example.com/", res.Header.Get("Location"), "Expected redirect to the root of the host")
-
-			// Verify that the oauth2 cookies have been deleted
-			var cookieDeleted bool
-			deletedCookies := res.Header.Values("Set-Cookie")
-			regx := regexp.MustCompile("^IdToken-.+=deleted.+")
-			for _, cookie := range deletedCookies {
-				if regx.Match([]byte(cookie)) {
-					cookieDeleted = true
-				}
-			}
-			require.True(t, cookieDeleted, "IdToken cookie not deleted")
+			testOIDC(t, suite)
 		})
 
 		t.Run("http route without oidc authentication", func(t *testing.T) {
@@ -185,3 +96,102 @@ var OIDCTest = suite.ConformanceTest{
 		})
 	},
 }
+
+func testOIDC(t *testing.T, suite *suite.ConformanceTestSuite) {
+	var (
+		testURL   = "http://www.example.com/myapp"
+		logoutURL = "http://www.example.com/myapp/logout"
+		route     = "http-with-oidc"
+		sp        = "oidc-test"
+		ns        = "gateway-conformance-infra"
+	)
+
+	routeNN := types.NamespacedName{Name: route, Namespace: ns}
+	gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+	gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+
+	ancestorRef := gwapiv1a2.ParentReference{
+		Group:     gatewayapi.GroupPtr(gwapiv1.GroupName),
+		Kind:      gatewayapi.KindPtr(resource.KindGateway),
+		Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+		Name:      gwapiv1.ObjectName(gwNN.Name),
+	}
+	SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: sp, Namespace: ns}, suite.ControllerName, ancestorRef)
+
+	podInitialized := corev1.PodCondition{Type: corev1.PodInitialized, Status: corev1.ConditionTrue}
+
+	// Wait for the keycloak pod to be configured with the test user and client
+	WaitForPods(t, suite.Client, ns, map[string]string{"job-name": "setup-keycloak"}, corev1.PodSucceeded, podInitialized)
+
+	// Initialize the test OIDC client that will keep track of the state of the OIDC login process
+	client, err := NewOIDCTestClient(
+		WithLoggingOptions(t.Log, true),
+		// Map the application and keycloak cluster DNS name to the gateway address
+		WithCustomAddressMappings(map[string]string{
+			"www.example.com:80":                    gwAddr,
+			"keycloak.gateway-conformance-infra:80": gwAddr,
+		}),
+	)
+	require.NoError(t, err)
+
+	if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, 5*time.Minute, true,
+		func(_ context.Context) (done bool, err error) {
+			tlog.Logf(t, "sending request to %s", testURL)
+
+			// Send a request to the http route with OIDC configured.
+			// It will be redirected to the keycloak login page
+			res, err := client.Get(testURL, true)
+			require.NoError(t, err, "Failed to get the login page")
+			require.Equal(t, 200, res.StatusCode, "Expected 200 OK")
+
+			// Parse the response body to get the URL where the login page would post the user-entered credentials
+			if err := client.ParseLoginForm(res.Body, keyCloakLoginFormID); err != nil {
+				tlog.Logf(t, "failed to parse login form: %v", err)
+				return false, nil
+			}
+
+			t.Log("successfully parsed login form")
+			return true, nil
+		}); err != nil {
+		t.Errorf("failed to parse login form: %v", err)
+	}
+
+	// Submit the login form to the IdP.
+	// This will authenticate and redirect back to the application
+	res, err := client.Login(map[string]string{"username": username, "password": password, "credentialId": ""})
+	require.NoError(t, err, "Failed to login to the IdP")
+
+	// Verify that we get the expected response from the application
+	body, err := io.ReadAll(res.Body)
+	require.NoError(t, err)
+	require.Equal(t, http.StatusOK, res.StatusCode)
+	require.Contains(t, string(body), "infra-backend-v1", "Expected response from the application")
+
+	// Verify that we can access the application without logging in again
+	res, err = client.Get(testURL, false)
+	require.NoError(t, err)
+	require.Equal(t, http.StatusOK, res.StatusCode)
+	require.Contains(t, string(body), "infra-backend-v1", "Expected response from the application")
+
+	// Verify that we can logout
+	// Note: OAuth2 filter just clears its cookies and does not log out from the IdP.
+	res, err = client.Get(logoutURL, false)
+	require.NoError(t, err)
+	require.Equal(t, http.StatusFound, res.StatusCode)
+
+	// After logout, OAuth2 filter will redirect back to the root of the host, e.g, "www.example.com".
+	// Ideally, this should redirect to the application's root, e.g, "www.example.com/myapp",
+	// but Envoy OAuth2 filter does not support this yet.
+	require.Equal(t, "http://www.example.com/", res.Header.Get("Location"), "Expected redirect to the root of the host")
+
+	// Verify that the oauth2 cookies have been deleted
+	var cookieDeleted bool
+	deletedCookies := res.Header.Values("Set-Cookie")
+	regx := regexp.MustCompile("^IdToken-.+=deleted.+")
+	for _, cookie := range deletedCookies {
+		if regx.Match([]byte(cookie)) {
+			cookieDeleted = true
+		}
+	}
+	require.True(t, cookieDeleted, "IdToken cookie not deleted")
+}