diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 977ff683740..6adcd8d1398 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -8,8 +8,11 @@ package gatewayapi import ( "encoding/json" "fmt" + "net" "net/http" + "net/url" "sort" + "strconv" "strings" v1 "k8s.io/api/core/v1" @@ -416,6 +419,7 @@ func (t *Translator) buildOIDC( var ( oidc = policy.Spec.OIDC clientSecret *v1.Secret + provider *ir.OIDCProvider err error ) @@ -438,11 +442,13 @@ func (t *Translator) buildOIDC( // Discover the token and authorization endpoints from the issuer's // well-known url if not explicitly specified - provider := oidc.Provider.DeepCopy() - if err := discoverEndpointsFromIssuer(provider); err != nil { + 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) return &ir.OIDC{ @@ -479,16 +485,22 @@ type OpenIDConfig struct { // 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) error { +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 fmt.Errorf("error fetching endpoints from issuer: %w", err) + return nil, fmt.Errorf("error fetching endpoints from issuer: %w", err) } - provider.TokenEndpoint = &tokenEndpoint - provider.AuthorizationEndpoint = &authorizationEndpoint + return &ir.OIDCProvider{ + TokenEndpoint: tokenEndpoint, + AuthorizationEndpoint: authorizationEndpoint, + }, nil } - return nil + + return &ir.OIDCProvider{ + TokenEndpoint: *provider.TokenEndpoint, + AuthorizationEndpoint: *provider.AuthorizationEndpoint, + }, nil } func fetchEndpointsFromIssuer(issuerURL string) (string, string, error) { @@ -508,3 +520,29 @@ func fetchEndpointsFromIssuer(issuerURL string) (string, string, error) { return config.TokenEndpoint, config.AuthorizationEndpoint, nil } + +// validateTokenEndpoint validates the token endpoint URL +func validateTokenEndpoint(tokenEndpoint string) error { + parsedURL, err := url.Parse(tokenEndpoint) + if err != nil { + return fmt.Errorf("error parsing token endpoint URL: %w", err) + } + + if parsedURL.Scheme != "https" { + return fmt.Errorf("token endpoint URL scheme must be https: %s", tokenEndpoint) + } + + if ip := net.ParseIP(parsedURL.Hostname()); ip != nil { + if v4 := ip.To4(); v4 != nil { + return fmt.Errorf("token endpoint URL must be a domain name: %s", tokenEndpoint) + } + } + + if parsedURL.Port() != "" { + _, err = strconv.Atoi(parsedURL.Port()) + if err != nil { + return fmt.Errorf("error parsing token endpoint URL port: %w", err) + } + } + return nil +} diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml index a334ddd8693..0aa2e0e2289 100755 --- a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml @@ -286,7 +286,6 @@ xdsIR: clientSecret: Y2xpZW50MTpzZWNyZXQK provider: authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth - issuer: https://oauth.foo.com tokenEndpoint: https://oauth.foo.com/token scopes: - openid @@ -314,7 +313,6 @@ xdsIR: clientSecret: Y2xpZW50MTpzZWNyZXQK provider: authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth - issuer: https://accounts.google.com tokenEndpoint: https://oauth2.googleapis.com/token scopes: - openid @@ -340,7 +338,6 @@ xdsIR: clientSecret: Y2xpZW50MTpzZWNyZXQK provider: authorizationEndpoint: https://oauth.bar.com/oauth2/v2/auth - issuer: https://oauth.bar.com tokenEndpoint: https://oauth.bar.com/token scopes: - openid diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 2c5179d9d23..311c7a0fe17 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -335,7 +335,7 @@ type JWT struct { // +k8s:deepcopy-gen=true type OIDC struct { // The OIDC Provider configuration. - Provider egv1a1.OIDCProvider `json:"provider" yaml:"provider"` + Provider OIDCProvider `json:"provider" yaml:"provider"` // The OIDC client ID to be used in the // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). @@ -353,6 +353,14 @@ type OIDC struct { Scopes []string `json:"scopes,omitempty" yaml:"scopes,omitempty"` } +type OIDCProvider struct { + // The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). + AuthorizationEndpoint string `json:"authorizationEndpoint,omitempty"` + + // The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). + TokenEndpoint string `json:"tokenEndpoint,omitempty"` +} + // Validate the fields within the HTTPRoute structure func (h HTTPRoute) Validate() error { var errs error diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index edf7e1146cf..c7855aff720 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -690,7 +690,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 - in.Provider.DeepCopyInto(&out.Provider) + out.Provider = in.Provider if in.ClientSecret != nil { in, out := &in.ClientSecret, &out.ClientSecret *out = make([]byte, len(*in)) diff --git a/internal/xds/translator/accesslog.go b/internal/xds/translator/accesslog.go index 0fa06c9a211..1ecf6c78760 100644 --- a/internal/xds/translator/accesslog.go +++ b/internal/xds/translator/accesslog.go @@ -250,7 +250,7 @@ func processClusterForAccessLog(tCtx *types.ResourceVersionTable, al *ir.AccessL name: clusterName, settings: []*ir.DestinationSetting{ds}, tSocket: nil, - endpointType: DefaultEndpointType, + endpointType: EndpointTypeDNS, }); err != nil && !errors.Is(err, ErrXdsClusterExists) { return err } diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go index 4afdc47d799..63cb2dfb502 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -43,7 +43,7 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster { cluster.TransportSocket = args.tSocket } - if args.endpointType == Static { + if args.endpointType == EndpointTypeStatic { cluster.ClusterDiscoveryType = &clusterv3.Cluster_Type{Type: clusterv3.Cluster_EDS} cluster.EdsClusterConfig = &clusterv3.Cluster_EdsClusterConfig{ ServiceName: args.name, diff --git a/internal/xds/translator/cluster_test.go b/internal/xds/translator/cluster_test.go index b7a024e6cf5..f4b71c59b44 100644 --- a/internal/xds/translator/cluster_test.go +++ b/internal/xds/translator/cluster_test.go @@ -31,7 +31,7 @@ func TestBuildXdsCluster(t *testing.T) { args := &xdsClusterArgs{ name: bootstrapXdsCluster.Name, tSocket: bootstrapXdsCluster.TransportSocket, - endpointType: DefaultEndpointType, + endpointType: EndpointTypeDNS, } dynamicXdsCluster := buildXdsCluster(args) diff --git a/internal/xds/translator/cors.go b/internal/xds/translator/cors.go index 14ce8232dca..71663f21733 100644 --- a/internal/xds/translator/cors.go +++ b/internal/xds/translator/cors.go @@ -88,9 +88,9 @@ func listenerContainsCORS(irListener *ir.HTTPListener) bool { return false } -// patchRouteWithCORSConfig patches the provided route with the CORS config if +// patchRouteWithCORS patches the provided route with the CORS config if // applicable. -func patchRouteWithCORSConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error { +func patchRouteWithCORS(route *routev3.Route, irRoute *ir.HTTPRoute) error { if route == nil { return errors.New("xds route is nil") } diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go index 5318450ac9a..6c6ae99f994 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -7,6 +7,7 @@ package translator import ( "sort" + "strings" routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" @@ -37,14 +38,16 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { order := 50 // Set a rational order for all the filters. - switch filter.Name { - case wellknown.CORS: + switch { + case filter.Name == wellknown.CORS: order = 1 - case jwtAuthnFilter: + case isOAuth2Filter(filter): order = 2 - case wellknown.HTTPRateLimit: + case filter.Name == jwtAuthnFilter: order = 3 - case wellknown.Router: + case filter.Name == wellknown.HTTPRateLimit: + order = 4 + case filter.Name == wellknown.Router: order = 100 } @@ -110,6 +113,11 @@ func (t *Translator) patchHCMWithFilters( return err } + // Add oauth2 filters, if needed. + if err := patchHCMWithOAuth2Filters(mgr, irListener); err != nil { + return err + } + // Add the router filter mgr.HttpFilters = append(mgr.HttpFilters, xdsfilters.HTTPRouter) @@ -118,8 +126,29 @@ func (t *Translator) patchHCMWithFilters( return nil } -// patchRouteWithFilters appends per-route filter configurations to the route. -func patchRouteWithFilters( +// patchRouteCfgWithPerRouteConfig appends per-route filter configurations to the +// route config. +// This is a generic way to add per-route filter configurations for all filters +// that has none-native per-route configuration support. +// - For the filter type that without native per-route configuration support, EG +// adds a filter for each route in the HCM filter chain. +// - patchRouteCfgWithPerRouteConfig disables all the filters in the +// typedFilterConfig of the route config. +// - PatchRouteWithPerRouteConfig enables the corresponding oauth2 filter for each +// route in the typedFilterConfig of the route. +// +// The filter types that have non-native per-route support: oauth2, basic authn +// Note: The filter types that have native per-route configuration support should +// use their own native per-route configuration. +func patchRouteCfgWithPerRouteConfig( + routeCfg *routev3.RouteConfiguration, + irListener *ir.HTTPListener) error { + // Only supports the oauth2 filter for now, other filters will be added later. + return patchRouteCfgWithOAuth2Filter(routeCfg, irListener) +} + +// patchRouteWithPerRouteConfig appends per-route filter configurations to the route. +func patchRouteWithPerRouteConfig( route *routev3.Route, irRoute *ir.HTTPRoute) error { // TODO: Convert this into a generic interface for API Gateway features. @@ -130,14 +159,26 @@ func patchRouteWithFilters( } // Add the cors per route config to the route, if needed. - if err := patchRouteWithCORSConfig(route, irRoute); err != nil { + if err := patchRouteWithCORS(route, irRoute); err != nil { return err } // Add the jwt per route config to the route, if needed. - if err := patchRouteWithJWTConfig(route, irRoute); err != nil { + if err := patchRouteWithJWT(route, irRoute); err != nil { + return err + } + + // Add the oauth2 per route config to the route, if needed. + if err := patchRouteWithOAuth2(route, irRoute); err != nil { return err } return nil } + +// isOAuth2Filter returns true if the provided filter is an OAuth2 filter. +func isOAuth2Filter(filter *hcmv3.HttpFilter) bool { + // Multiple oauth2 filters are added to the HCM filter chain, one for each + // route. The oauth2 filter name is prefixed with "envoy.filters.http.oauth2". + return strings.HasPrefix(filter.Name, oauth2Filter) +} diff --git a/internal/xds/translator/jwt_authn.go b/internal/xds/translator/jwt_authn.go index 34bf2841e48..ae50494ac68 100644 --- a/internal/xds/translator/jwt_authn.go +++ b/internal/xds/translator/jwt_authn.go @@ -8,22 +8,17 @@ package translator import ( "errors" "fmt" - "net" - "net/url" - "strconv" - "strings" corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" jwtauthnv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" - "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/envoyproxy/go-control-plane/pkg/wellknown" + "github.com/tetratelabs/multierror" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/durationpb" - "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/ir" "github.com/envoyproxy/gateway/internal/utils/ptr" "github.com/envoyproxy/gateway/internal/xds/types" @@ -97,64 +92,68 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication, reqMap := make(map[string]*jwtauthnv3.JwtRequirement) for _, route := range irListener.Routes { - if route != nil && routeContainsJWTAuthn(route) { - var reqs []*jwtauthnv3.JwtRequirement - for i := range route.JWT.Providers { - irProvider := route.JWT.Providers[i] - // Create the cluster for the remote jwks, if it doesn't exist. - jwksCluster, err := newJWKSCluster(&irProvider) - if err != nil { - return nil, err - } - - remote := &jwtauthnv3.JwtProvider_RemoteJwks{ - RemoteJwks: &jwtauthnv3.RemoteJwks{ - HttpUri: &corev3.HttpUri{ - Uri: irProvider.RemoteJWKS.URI, - HttpUpstreamType: &corev3.HttpUri_Cluster{ - Cluster: jwksCluster.name, - }, - Timeout: &durationpb.Duration{Seconds: 5}, + if route == nil || !routeContainsJWTAuthn(route) { + continue + } + + var reqs []*jwtauthnv3.JwtRequirement + for i := range route.JWT.Providers { + irProvider := route.JWT.Providers[i] + // Create the cluster for the remote jwks, if it doesn't exist. + jwksCluster, err := url2Cluster(irProvider.RemoteJWKS.URI) + if err != nil { + return nil, err + } + + remote := &jwtauthnv3.JwtProvider_RemoteJwks{ + RemoteJwks: &jwtauthnv3.RemoteJwks{ + HttpUri: &corev3.HttpUri{ + Uri: irProvider.RemoteJWKS.URI, + HttpUpstreamType: &corev3.HttpUri_Cluster{ + Cluster: jwksCluster.name, }, - CacheDuration: &durationpb.Duration{Seconds: 5 * 60}, - AsyncFetch: &jwtauthnv3.JwksAsyncFetch{}, - RetryPolicy: &corev3.RetryPolicy{}, - }, - } - - claimToHeaders := []*jwtauthnv3.JwtClaimToHeader{} - for _, claimToHeader := range irProvider.ClaimToHeaders { - claimToHeader := &jwtauthnv3.JwtClaimToHeader{HeaderName: claimToHeader.Header, ClaimName: claimToHeader.Claim} - claimToHeaders = append(claimToHeaders, claimToHeader) - } - jwtProvider := &jwtauthnv3.JwtProvider{ - Issuer: irProvider.Issuer, - Audiences: irProvider.Audiences, - JwksSourceSpecifier: remote, - PayloadInMetadata: irProvider.Issuer, - ClaimToHeaders: claimToHeaders, - } - - providerKey := fmt.Sprintf("%s/%s", route.Name, irProvider.Name) - jwtProviders[providerKey] = jwtProvider - reqs = append(reqs, &jwtauthnv3.JwtRequirement{ - RequiresType: &jwtauthnv3.JwtRequirement_ProviderName{ - ProviderName: providerKey, + Timeout: &durationpb.Duration{Seconds: 5}, }, - }) + CacheDuration: &durationpb.Duration{Seconds: 5 * 60}, + AsyncFetch: &jwtauthnv3.JwksAsyncFetch{}, + RetryPolicy: &corev3.RetryPolicy{}, + }, } - if len(reqs) == 1 { - reqMap[route.Name] = reqs[0] - } else { - orListReqs := &jwtauthnv3.JwtRequirement{ - RequiresType: &jwtauthnv3.JwtRequirement_RequiresAny{ - RequiresAny: &jwtauthnv3.JwtRequirementOrList{ - Requirements: reqs, - }, + + claimToHeaders := []*jwtauthnv3.JwtClaimToHeader{} + for _, claimToHeader := range irProvider.ClaimToHeaders { + claimToHeader := &jwtauthnv3.JwtClaimToHeader{ + HeaderName: claimToHeader.Header, + ClaimName: claimToHeader.Claim} + claimToHeaders = append(claimToHeaders, claimToHeader) + } + jwtProvider := &jwtauthnv3.JwtProvider{ + Issuer: irProvider.Issuer, + Audiences: irProvider.Audiences, + JwksSourceSpecifier: remote, + PayloadInMetadata: irProvider.Issuer, + ClaimToHeaders: claimToHeaders, + } + + providerKey := fmt.Sprintf("%s/%s", route.Name, irProvider.Name) + jwtProviders[providerKey] = jwtProvider + reqs = append(reqs, &jwtauthnv3.JwtRequirement{ + RequiresType: &jwtauthnv3.JwtRequirement_ProviderName{ + ProviderName: providerKey, + }, + }) + } + if len(reqs) == 1 { + reqMap[route.Name] = reqs[0] + } else { + orListReqs := &jwtauthnv3.JwtRequirement{ + RequiresType: &jwtauthnv3.JwtRequirement_RequiresAny{ + RequiresAny: &jwtauthnv3.JwtRequirementOrList{ + Requirements: reqs, }, - } - reqMap[route.Name] = orListReqs + }, } + reqMap[route.Name] = orListReqs } } @@ -194,9 +193,9 @@ func buildXdsUpstreamTLSSocket() (*corev3.TransportSocket, error) { }, nil } -// patchRouteWithJWTConfig patches the provided route with a JWT PerRouteConfig, if the +// patchRouteWithJWT patches the provided route with a JWT PerRouteConfig, if the // route doesn't contain it. -func patchRouteWithJWTConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error { +func patchRouteWithJWT(route *routev3.Route, irRoute *ir.HTTPRoute) error { if route == nil { return errors.New("xds route is nil") } @@ -228,100 +227,56 @@ func patchRouteWithJWTConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error return nil } -type jwksCluster struct { - name string - hostname string - port uint32 - isStatic bool -} - // createJWKSClusters creates JWKS clusters from the provided routes, if needed. func createJWKSClusters(tCtx *types.ResourceVersionTable, routes []*ir.HTTPRoute) error { - if tCtx == nil || - tCtx.XdsResources == nil || - tCtx.XdsResources[resource.ClusterType] == nil || - len(routes) == 0 { - return nil + if tCtx == nil || tCtx.XdsResources == nil { + return errors.New("xds resource table is nil") } + var errs error for _, route := range routes { - if routeContainsJWTAuthn(route) { - for i := range route.JWT.Providers { - provider := route.JWT.Providers[i] - jwks, err := newJWKSCluster(&provider) - epType := DefaultEndpointType - if jwks.isStatic { - epType = Static - } - if err != nil { - return err - } - ds := &ir.DestinationSetting{ - Weight: ptr.To(uint32(1)), - Endpoints: []*ir.DestinationEndpoint{ir.NewDestEndpoint(jwks.hostname, jwks.port)}, - } - tSocket, err := buildXdsUpstreamTLSSocket() - if err != nil { - return err - } - if err := addXdsCluster(tCtx, &xdsClusterArgs{ - name: jwks.name, - settings: []*ir.DestinationSetting{ds}, - tSocket: tSocket, - endpointType: epType, - }); err != nil && !errors.Is(err, ErrXdsClusterExists) { - return err - } - } + if !routeContainsJWTAuthn(route) { + continue } - } - return nil -} - -// newJWKSCluster returns a jwksCluster from the provided provider. -func newJWKSCluster(provider *v1alpha1.JWTProvider) (*jwksCluster, error) { - static := false - if provider == nil { - return nil, errors.New("nil provider") - } - - u, err := url.Parse(provider.RemoteJWKS.URI) - if err != nil { - return nil, err - } - - var strPort string - switch u.Scheme { - case "https": - strPort = "443" - default: - return nil, fmt.Errorf("unsupported JWKS URI scheme %s", u.Scheme) - } - - if u.Port() != "" { - strPort = u.Port() - } + for i := range route.JWT.Providers { + var ( + jwks *urlCluster + ds *ir.DestinationSetting + tSocket *corev3.TransportSocket + err error + ) + + provider := route.JWT.Providers[i] + jwks, err = url2Cluster(provider.RemoteJWKS.URI) + if err != nil { + errs = multierror.Append(errs, err) + continue + } - name := fmt.Sprintf("%s_%s", strings.ReplaceAll(u.Hostname(), ".", "_"), strPort) + ds = &ir.DestinationSetting{ + Weight: ptr.To(uint32(1)), + Endpoints: []*ir.DestinationEndpoint{ir.NewDestEndpoint(jwks.hostname, jwks.port)}, + } - port, err := strconv.Atoi(strPort) - if err != nil { - return nil, err - } + tSocket, err = buildXdsUpstreamTLSSocket() + if err != nil { + errs = multierror.Append(errs, err) + continue + } - if ip := net.ParseIP(u.Hostname()); ip != nil { - if v4 := ip.To4(); v4 != nil { - static = true + if err = addXdsCluster(tCtx, &xdsClusterArgs{ + name: jwks.name, + settings: []*ir.DestinationSetting{ds}, + tSocket: tSocket, + endpointType: jwks.endpointType, + }); err != nil && !errors.Is(err, ErrXdsClusterExists) { + errs = multierror.Append(errs, err) + } } } - return &jwksCluster{ - name: name, - hostname: u.Hostname(), - port: uint32(port), - isStatic: static, - }, nil + return errs } // listenerContainsJWTAuthn returns true if JWT authentication exists for the diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go new file mode 100644 index 00000000000..b79fe8039b1 --- /dev/null +++ b/internal/xds/translator/oidc.go @@ -0,0 +1,426 @@ +// 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. + +package translator + +import ( + "crypto/rand" + "errors" + "fmt" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + oauth2v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/oauth2/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + "github.com/golang/protobuf/ptypes/duration" + "github.com/tetratelabs/multierror" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/utils/ptr" + "github.com/envoyproxy/gateway/internal/xds/types" +) + +const ( + oauth2Filter = "envoy.filters.http.oauth2" + defaultTokenEndpointTimeout = 10 + redirectURL = "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" + redirectPathMatcher = "/oauth2/callback" + defaultSignoutPath = "/signout" +) + +// patchHCMWithOAuth2Filters builds and appends the oauth2 Filters to the HTTP +// Connection Manager if applicable, and it does not already exist. +// Note: this method creates an oauth2 filter for each route that contains an OIDC config. +func patchHCMWithOAuth2Filters(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { + var errs error + + if mgr == nil { + return errors.New("hcm is nil") + } + + if irListener == nil { + return errors.New("ir listener is nil") + } + + for _, route := range irListener.Routes { + if !routeContainsOIDC(route) { + continue + } + + filter, err := buildHCMOAuth2Filter(route) + if err != nil { + errs = multierror.Append(errs, err) + continue + } + + // skip if the filter already exists + for _, existingFilter := range mgr.HttpFilters { + if filter.Name == existingFilter.Name { + continue + } + } + + mgr.HttpFilters = append(mgr.HttpFilters, filter) + } + + return nil +} + +// buildHCMOAuth2Filter returns an OAuth2 HTTP filter from the provided IR HTTPRoute. +func buildHCMOAuth2Filter(route *ir.HTTPRoute) (*hcmv3.HttpFilter, error) { + oauth2Proto, err := oauth2Config(route) + if err != nil { + return nil, err + } + + if err := oauth2Proto.ValidateAll(); err != nil { + return nil, err + } + + OAuth2Any, err := anypb.New(oauth2Proto) + if err != nil { + return nil, err + } + + return &hcmv3.HttpFilter{ + Name: oauth2FilterName(route), + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: OAuth2Any, + }, + }, nil +} + +func oauth2FilterName(route *ir.HTTPRoute) string { + return fmt.Sprintf("%s_%s", oauth2Filter, route.Name) +} + +func oauth2Config(route *ir.HTTPRoute) (*oauth2v3.OAuth2, error) { + cluster, err := url2Cluster(route.OIDC.Provider.TokenEndpoint) + if err != nil { + return nil, err + } + if cluster.endpointType == EndpointTypeStatic { + return nil, fmt.Errorf( + "static IP cluster is not allowed: %s", + route.OIDC.Provider.TokenEndpoint) + } + + oauth2 := &oauth2v3.OAuth2{ + Config: &oauth2v3.OAuth2Config{ + TokenEndpoint: &corev3.HttpUri{ + Uri: route.OIDC.Provider.TokenEndpoint, + HttpUpstreamType: &corev3.HttpUri_Cluster{ + Cluster: cluster.name, + }, + Timeout: &duration.Duration{ + Seconds: defaultTokenEndpointTimeout, + }, + }, + AuthorizationEndpoint: route.OIDC.Provider.AuthorizationEndpoint, + RedirectUri: redirectURL, + RedirectPathMatcher: &matcherv3.PathMatcher{ + Rule: &matcherv3.PathMatcher_Path{ + Path: &matcherv3.StringMatcher{ + MatchPattern: &matcherv3.StringMatcher_Exact{ + Exact: redirectPathMatcher, + }, + }, + }, + }, + SignoutPath: &matcherv3.PathMatcher{ + Rule: &matcherv3.PathMatcher_Path{ + Path: &matcherv3.StringMatcher{ + MatchPattern: &matcherv3.StringMatcher_Exact{ + Exact: defaultSignoutPath, + }, + }, + }, + }, + ForwardBearerToken: true, + Credentials: &oauth2v3.OAuth2Credentials{ + ClientId: route.OIDC.ClientID, + TokenSecret: &tlsv3.SdsSecretConfig{ + Name: oauth2ClientSecretName(route), + SdsConfig: makeConfigSource(), + }, + TokenFormation: &oauth2v3.OAuth2Credentials_HmacSecret{ + HmacSecret: &tlsv3.SdsSecretConfig{ + Name: oauth2HMACSecretName(route), + SdsConfig: makeConfigSource(), + }, + }, + }, + AuthType: oauth2v3.OAuth2Config_BASIC_AUTH, // every OIDC provider supports basic auth + AuthScopes: route.OIDC.Scopes, + }, + } + return oauth2, nil +} + +// routeContainsOIDC returns true if OIDC exists for the provided route. +func routeContainsOIDC(irRoute *ir.HTTPRoute) bool { + if irRoute == nil { + return false + } + + if irRoute != nil && + irRoute.OIDC != nil { + return true + } + + return false +} + +// createOAuth2TokenEndpointClusters creates token endpoint clusters from the +// provided routes, if needed. +func createOAuth2TokenEndpointClusters(tCtx *types.ResourceVersionTable, + routes []*ir.HTTPRoute) error { + if tCtx == nil || tCtx.XdsResources == nil { + return errors.New("xds resource table is nil") + } + + var errs error + for _, route := range routes { + if !routeContainsOIDC(route) { + continue + } + + var ( + cluster *urlCluster + ds *ir.DestinationSetting + tSocket *corev3.TransportSocket + err error + ) + + cluster, err = url2Cluster(route.OIDC.Provider.TokenEndpoint) + if err != nil { + errs = multierror.Append(errs, err) + continue + } + + // 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 = multierror.Append(errs, fmt.Errorf( + "static IP cluster is not allowed: %s", + route.OIDC.Provider.TokenEndpoint)) + continue + } + + tlsContext := &tlsv3.UpstreamTlsContext{ + Sni: cluster.hostname, + } + + tlsContextAny, err := anypb.New(tlsContext) + if err != nil { + errs = multierror.Append(errs, err) + continue + } + tSocket = &corev3.TransportSocket{ + Name: "envoy.transport_sockets.tls", + ConfigType: &corev3.TransportSocket_TypedConfig{ + TypedConfig: tlsContextAny, + }, + } + + ds = &ir.DestinationSetting{ + Weight: ptr.To(uint32(1)), + Endpoints: []*ir.DestinationEndpoint{ir.NewDestEndpoint( + cluster.hostname, + cluster.port), + }, + } + + if err = addXdsCluster(tCtx, &xdsClusterArgs{ + name: cluster.name, + settings: []*ir.DestinationSetting{ds}, + tSocket: tSocket, + endpointType: cluster.endpointType, + }); err != nil && !errors.Is(err, ErrXdsClusterExists) { + errs = multierror.Append(errs, err) + } + } + + return errs +} + +// createOAuth2Secrets creates OAuth2 client and HMAC secrets from the provided +// routes, if needed. +func createOAuth2Secrets(tCtx *types.ResourceVersionTable, routes []*ir.HTTPRoute) error { + var errs error + + for _, route := range routes { + if !routeContainsOIDC(route) { + continue + } + + clientSecret := buildOAuth2ClientSecret(route) + if err := tCtx.AddXdsResource(resourcev3.SecretType, clientSecret); err != nil { + errs = multierror.Append(errs, err) + } + + hmacSecret, err := buildOAuth2HMACSecret(route) + if err != nil { + errs = multierror.Append(errs, err) + } + if err := tCtx.AddXdsResource(resourcev3.SecretType, hmacSecret); err != nil { + errs = multierror.Append(errs, err) + } + } + + return errs +} + +func buildOAuth2ClientSecret(route *ir.HTTPRoute) *tlsv3.Secret { + clientSecret := &tlsv3.Secret{ + Name: oauth2ClientSecretName(route), + Type: &tlsv3.Secret_GenericSecret{ + GenericSecret: &tlsv3.GenericSecret{ + Secret: &corev3.DataSource{ + Specifier: &corev3.DataSource_InlineBytes{ + InlineBytes: route.OIDC.ClientSecret, + }, + }, + }, + }, + } + + return clientSecret +} + +func buildOAuth2HMACSecret(route *ir.HTTPRoute) (*tlsv3.Secret, error) { + hmac, err := generateHMACSecretKey() + if err != nil { + return nil, fmt.Errorf("failed to generate hmack secret key: %w", err) + } + hmacSecret := &tlsv3.Secret{ + Name: oauth2HMACSecretName(route), + Type: &tlsv3.Secret_GenericSecret{ + GenericSecret: &tlsv3.GenericSecret{ + Secret: &corev3.DataSource{ + Specifier: &corev3.DataSource_InlineBytes{ + InlineBytes: hmac, + }, + }, + }, + }, + } + + return hmacSecret, nil +} + +func oauth2ClientSecretName(route *ir.HTTPRoute) string { + return fmt.Sprintf("%s/oauth2/client_secret", route.Name) +} + +func oauth2HMACSecretName(route *ir.HTTPRoute) string { + return fmt.Sprintf("%s/oauth2/hmac_secret", route.Name) +} + +func generateHMACSecretKey() ([]byte, error) { + // Set the desired length of the secret key in bytes + keyLength := 32 + + // Create a byte slice to hold the random bytes + key := make([]byte, keyLength) + + // Read random bytes from the cryptographically secure random number generator + _, err := rand.Read(key) + if err != nil { + return nil, err + } + + return key, nil +} + +// patchRouteCfgWithOAuth2Filter patches the provided route configuration with +// the oauth2 filter if applicable. +// Note: this method disables all the oauth2 filters by default. The filter will +// be enabled per-route in the typePerFilterConfig of the route. +func patchRouteCfgWithOAuth2Filter(routeCfg *routev3.RouteConfiguration, irListener *ir.HTTPListener) error { + if routeCfg == nil { + return errors.New("route configuration is nil") + } + if irListener == nil { + return errors.New("ir listener is nil") + } + + var errs error + for _, route := range irListener.Routes { + if !routeContainsOIDC(route) { + continue + } + + perRouteFilterName := oauth2FilterName(route) + filterCfg := routeCfg.TypedPerFilterConfig + + if _, ok := filterCfg[perRouteFilterName]; ok { + // This should not happen since this is the only place where the oauth2 + // filter is added in a route. + errs = multierror.Append(errs, fmt.Errorf( + "route config already contains oauth2 config: %+v", route)) + continue + } + + // Disable all the filters by default. The filter will be enabled + // per-route in the typePerFilterConfig of the route. + routeCfgAny, err := anypb.New(&routev3.FilterConfig{Disabled: true}) + if err != nil { + errs = multierror.Append(errs, err) + continue + } + + if filterCfg == nil { + routeCfg.TypedPerFilterConfig = make(map[string]*anypb.Any) + } + + routeCfg.TypedPerFilterConfig[perRouteFilterName] = routeCfgAny + } + return errs +} + +// patchRouteWithOAuth2 patches the provided route with the oauth2 config if +// applicable. +// Note: this method enables the corresponding oauth2 filter for the provided route. +func patchRouteWithOAuth2(route *routev3.Route, irRoute *ir.HTTPRoute) error { + if route == nil { + return errors.New("xds route is nil") + } + if irRoute == nil { + return errors.New("ir route is nil") + } + if irRoute.OIDC == nil { + return nil + } + + perRouteFilterName := oauth2FilterName(irRoute) + filterCfg := route.GetTypedPerFilterConfig() + if _, ok := filterCfg[perRouteFilterName]; ok { + // This should not happen since this is the only place where the oauth2 + // filter is added in a route. + return fmt.Errorf("route already contains oauth2 config: %+v", route) + } + + // Enable the corresponding oauth2 filter for this route. + routeCfgAny, err := anypb.New(&routev3.FilterConfig{ + Config: &anypb.Any{}, + }) + if err != nil { + return err + } + + if filterCfg == nil { + route.TypedPerFilterConfig = make(map[string]*anypb.Any) + } + + route.TypedPerFilterConfig[perRouteFilterName] = routeCfgAny + + return nil +} diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go index 427c6bf7ec5..a8c2abcd5ca 100644 --- a/internal/xds/translator/ratelimit.go +++ b/internal/xds/translator/ratelimit.go @@ -8,7 +8,6 @@ package translator import ( "bytes" "errors" - "net/url" "strconv" "strings" @@ -446,7 +445,7 @@ func (t *Translator) createRateLimitServiceCluster(tCtx *types.ResourceVersionTa name: clusterName, settings: []*ir.DestinationSetting{ds}, tSocket: tSocket, - endpointType: DefaultEndpointType, + endpointType: EndpointTypeDNS, }); err != nil && !errors.Is(err, ErrXdsClusterExists) { return err } diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index e6fb30a41a4..b5dd1d61a12 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -17,7 +17,7 @@ import ( "github.com/envoyproxy/gateway/internal/ir" ) -func buildXdsRoute(httpRoute *ir.HTTPRoute) *routev3.Route { +func buildXdsRoute(httpRoute *ir.HTTPRoute) (*routev3.Route, error) { router := &routev3.Route{ Name: httpRoute.Name, Match: buildXdsRouteMatch(httpRoute.PathMatch, httpRoute.HeaderMatches, httpRoute.QueryParamMatches), @@ -77,11 +77,11 @@ func buildXdsRoute(httpRoute *ir.HTTPRoute) *routev3.Route { } // Add per route filter configs to the route, if needed. - if err := patchRouteWithFilters(router, httpRoute); err != nil { - return nil // TODO zhaohuabing we need to handle this error + if err := patchRouteWithPerRouteConfig(router, httpRoute); err != nil { + return nil, err } - return router + return router, nil } func buildXdsRouteMatch(pathMatch *ir.StringMatch, headerMatches []*ir.StringMatch, queryParamMatches []*ir.StringMatch) *routev3.RouteMatch { diff --git a/internal/xds/translator/shared_types.go b/internal/xds/translator/shared_types.go new file mode 100644 index 00000000000..ba326e6271e --- /dev/null +++ b/internal/xds/translator/shared_types.go @@ -0,0 +1,64 @@ +// 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. + +package translator + +import ( + "fmt" + "net" + "net/url" + "strconv" + "strings" +) + +const ( + defaultPort = 443 +) + +// urlCluster is a cluster that is created from a URL. +type urlCluster struct { + name string + hostname string + port uint32 + endpointType EndpointType +} + +// url2Cluster returns a urlCluster from the provided url. +func url2Cluster(strURL string) (*urlCluster, error) { + epType := EndpointTypeDNS + + // The URL should have already been validated in the gateway API translator. + u, err := url.Parse(strURL) + if err != nil { + return nil, err + } + + if u.Scheme != "https" { + return nil, fmt.Errorf("unsupported URI scheme %s", u.Scheme) + } + + port := defaultPort + if u.Port() != "" { + port, err = strconv.Atoi(u.Port()) + if err != nil { + return nil, err + } + } + + name := fmt.Sprintf("%s_%d", strings.ReplaceAll(u.Hostname(), ".", "_"), port) + + if ip := net.ParseIP(u.Hostname()); ip != nil { + if v4 := ip.To4(); v4 != nil { + epType = EndpointTypeStatic + } + } + + return &urlCluster{ + name: name, + hostname: u.Hostname(), + port: uint32(port), + endpointType: epType, + }, nil +} diff --git a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml new file mode 100644 index 00000000000..efbd8b9ea69 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml @@ -0,0 +1,67 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "first-route" + hostname: "*" + pathMatch: + exact: "foo" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + oidc: + clientID: client.oauth.foo.com + clientSecret: Y2xpZW50MTpzZWNyZXQK + provider: + authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth + tokenEndpoint: https://oauth.foo.com/token + scopes: + - openid + - email + - profile + - name: "second-route" + hostname: "*" + pathMatch: + exact: "bar" + destination: + name: "second-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + oidc: + clientID: client.oauth.bar.com + clientSecret: Y2xpZW50MTpzZWNyZXQK + provider: + authorizationEndpoint: https://oauth.bar.com/oauth2/v2/auth + tokenEndpoint: https://oauth.bar.com/token + scopes: + - openid + - email + - profile + - name: "third-route" + hostname: "*" + pathMatch: + exact: "test" + destination: + name: "third-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + oidc: + clientID: test.oauth.bar.com + clientSecret: Y2xpZW50MTpzZWNyZXQK + provider: + authorizationEndpoint: https://oauth.bar.com/oauth2/v2/auth + tokenEndpoint: https://oauth.bar.com/token + scopes: + - openid + - email + - profile diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc.clusters.yaml new file mode 100755 index 00000000000..5843a79b8f0 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/oidc.clusters.yaml @@ -0,0 +1,100 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: second-route-dest + lbPolicy: LEAST_REQUEST + name: second-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- 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 +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + dnsRefreshRate: 30s + lbPolicy: LEAST_REQUEST + loadAssignment: + clusterName: oauth_foo_com_443 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: oauth.foo.com + portValue: 443 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: oauth_foo_com_443/backend/0 + name: oauth_foo_com_443 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + respectDnsTtl: true + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + sni: oauth.foo.com + type: STRICT_DNS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + dnsRefreshRate: 30s + lbPolicy: LEAST_REQUEST + loadAssignment: + clusterName: oauth_bar_com_443 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: oauth.bar.com + portValue: 443 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: oauth_bar_com_443/backend/0 + name: oauth_bar_com_443 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + respectDnsTtl: true + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + sni: oauth.bar.com + type: STRICT_DNS diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc.endpoints.yaml new file mode 100755 index 00000000000..475b89a087c --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/oidc.endpoints.yaml @@ -0,0 +1,36 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: first-route-dest/backend/0 +- clusterName: second-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: second-route-dest/backend/0 +- 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.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml new file mode 100755 index 00000000000..2a001af7866 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml @@ -0,0 +1,135 @@ +- 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: + - name: envoy.filters.http.oauth2_first-route + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2 + config: + authScopes: + - openid + - email + - profile + authType: BASIC_AUTH + authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth + credentials: + clientId: client.oauth.foo.com + hmacSecret: + name: first-route/oauth2/hmac_secret + sdsConfig: + ads: {} + resourceApiVersion: V3 + tokenSecret: + name: first-route/oauth2/client_secret + sdsConfig: + ads: {} + resourceApiVersion: V3 + forwardBearerToken: true + redirectPathMatcher: + path: + exact: /oauth2/callback + redirectUri: '%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback' + signoutPath: + path: + exact: /signout + tokenEndpoint: + cluster: oauth_foo_com_443 + timeout: 10s + uri: https://oauth.foo.com/token + - name: envoy.filters.http.oauth2_second-route + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2 + config: + authScopes: + - openid + - email + - profile + authType: BASIC_AUTH + authorizationEndpoint: https://oauth.bar.com/oauth2/v2/auth + credentials: + clientId: client.oauth.bar.com + hmacSecret: + name: second-route/oauth2/hmac_secret + sdsConfig: + ads: {} + resourceApiVersion: V3 + tokenSecret: + name: second-route/oauth2/client_secret + sdsConfig: + ads: {} + resourceApiVersion: V3 + forwardBearerToken: true + redirectPathMatcher: + path: + exact: /oauth2/callback + redirectUri: '%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback' + signoutPath: + path: + exact: /signout + tokenEndpoint: + cluster: oauth_bar_com_443 + timeout: 10s + uri: https://oauth.bar.com/token + - name: envoy.filters.http.oauth2_third-route + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2 + config: + authScopes: + - openid + - email + - profile + authType: BASIC_AUTH + authorizationEndpoint: https://oauth.bar.com/oauth2/v2/auth + credentials: + clientId: test.oauth.bar.com + hmacSecret: + name: third-route/oauth2/hmac_secret + sdsConfig: + ads: {} + resourceApiVersion: V3 + tokenSecret: + name: third-route/oauth2/client_secret + sdsConfig: + ads: {} + resourceApiVersion: V3 + forwardBearerToken: true + redirectPathMatcher: + path: + exact: /oauth2/callback + redirectUri: '%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback' + signoutPath: + path: + exact: /signout + tokenEndpoint: + cluster: oauth_bar_com_443 + timeout: 10s + uri: https://oauth.bar.com/token + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + upgradeConfigs: + - upgradeType: websocket + useRemoteAddress: true + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc.routes.yaml new file mode 100755 index 00000000000..74831a07eb8 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/oidc.routes.yaml @@ -0,0 +1,44 @@ +- ignorePortInHostMatching: true + name: first-listener + typedPerFilterConfig: + envoy.filters.http.oauth2_first-route: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.oauth2_second-route: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.oauth2_third-route: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + path: foo + name: first-route + route: + cluster: first-route-dest + typedPerFilterConfig: + envoy.filters.http.oauth2_first-route: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} + - match: + path: bar + name: second-route + route: + cluster: second-route-dest + typedPerFilterConfig: + envoy.filters.http.oauth2_second-route: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} + - match: + path: test + name: third-route + route: + cluster: third-route-dest + typedPerFilterConfig: + envoy.filters.http.oauth2_third-route: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} diff --git a/internal/xds/translator/tracing.go b/internal/xds/translator/tracing.go index 78127f283b3..c7ee389088e 100644 --- a/internal/xds/translator/tracing.go +++ b/internal/xds/translator/tracing.go @@ -132,7 +132,7 @@ func processClusterForTracing(tCtx *types.ResourceVersionTable, tracing *ir.Trac name: clusterName, settings: []*ir.DestinationSetting{ds}, tSocket: nil, - endpointType: DefaultEndpointType, + endpointType: EndpointTypeDNS, }); err != nil && !errors.Is(err, ErrXdsClusterExists) { return err } diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index e65e8cac08a..1dfce60a648 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -195,7 +195,10 @@ func (t *Translator) processHTTPListenerXdsTranslation(tCtx *types.ResourceVersi } // 1:1 between IR HTTPRoute and xDS config.route.v3.Route - xdsRoute := buildXdsRoute(httpRoute) + xdsRoute, err := buildXdsRoute(httpRoute) + if err != nil { + return err + } // Check if an extension want to modify the route we just generated // If no extension exists (or it doesn't subscribe to this hook) then this is a quick no-op. @@ -210,7 +213,7 @@ func (t *Translator) processHTTPListenerXdsTranslation(tCtx *types.ResourceVersi name: httpRoute.Destination.Name, settings: httpRoute.Destination.Settings, tSocket: nil, - endpointType: Static, + endpointType: EndpointTypeStatic, loadBalancer: httpRoute.LoadBalancer, }); err != nil && !errors.Is(err, ErrXdsClusterExists) { return err @@ -223,7 +226,7 @@ func (t *Translator) processHTTPListenerXdsTranslation(tCtx *types.ResourceVersi name: mirrorDest.Name, settings: mirrorDest.Settings, tSocket: nil, - endpointType: Static, + endpointType: EndpointTypeStatic, }); err != nil && !errors.Is(err, ErrXdsClusterExists) { return err } @@ -240,6 +243,11 @@ func (t *Translator) processHTTPListenerXdsTranslation(tCtx *types.ResourceVersi } xdsRouteCfg.VirtualHosts = append(xdsRouteCfg.VirtualHosts, vHostsList...) + // Add per-route filter configs to the route config. + if err := patchRouteCfgWithPerRouteConfig(xdsRouteCfg, httpListener); err != nil { + return err + } + // TODO: Make this into a generic interface for API Gateway features. // https://github.com/envoyproxy/gateway/issues/882 // Check if a ratelimit cluster exists, if not, add it, if its needed. @@ -252,6 +260,16 @@ func (t *Translator) processHTTPListenerXdsTranslation(tCtx *types.ResourceVersi return err } + // Create oauth2 token endpoint clusters, if needed. + if err := createOAuth2TokenEndpointClusters(tCtx, httpListener.Routes); err != nil { + return err + } + + // Create oauth2 client and HMAC secrets, if needed. + if err := createOAuth2Secrets(tCtx, httpListener.Routes); err != nil { + return err + } + // Check if an extension want to modify the listener that was just configured/created // If no extension exists (or it doesn't subscribe to this hook) then this is a quick no-op if err := processExtensionPostListenerHook(tCtx, xdsListener, t.ExtensionManager); err != nil { @@ -269,7 +287,7 @@ func processTCPListenerXdsTranslation(tCtx *types.ResourceVersionTable, tcpListe name: tcpListener.Destination.Name, settings: tcpListener.Destination.Settings, tSocket: nil, - endpointType: Static, + endpointType: EndpointTypeStatic, }); err != nil && !errors.Is(err, ErrXdsClusterExists) { return err } @@ -305,7 +323,7 @@ func processUDPListenerXdsTranslation(tCtx *types.ResourceVersionTable, udpListe name: udpListener.Destination.Name, settings: udpListener.Destination.Settings, tSocket: nil, - endpointType: Static, + endpointType: EndpointTypeStatic, }); err != nil && !errors.Is(err, ErrXdsClusterExists) { return err } @@ -416,7 +434,7 @@ func addXdsCluster(tCtx *types.ResourceVersionTable, args *xdsClusterArgs) error xdsCluster := buildXdsCluster(args) xdsEndpoints := buildXdsClusterLoadAssignment(args.name, args.settings) // Use EDS for static endpoints - if args.endpointType == Static { + if args.endpointType == EndpointTypeStatic { if err := tCtx.AddXdsResource(resourcev3.EndpointType, xdsEndpoints); err != nil { return err } @@ -440,7 +458,6 @@ type xdsClusterArgs struct { type EndpointType int const ( - DefaultEndpointType EndpointType = iota - Static - EDS + EndpointTypeDNS EndpointType = iota + EndpointTypeStatic ) diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index 15fac37f495..40f8c22d1e4 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -189,6 +189,9 @@ func TestTranslateXds(t *testing.T) { { name: "jwt-single-route-single-match", }, + { + name: "oidc", + }, } for _, tc := range testCases {