Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jwt authn to SecurityPolicy #2079

Merged
merged 3 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions api/v1alpha1/securitypolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,24 @@ type SecurityPolicySpec struct {
TargetRef gwapiv1a2.PolicyTargetReferenceWithSectionName `json:"targetRef"`

// CORS defines the configuration for Cross-Origin Resource Sharing (CORS).
//
// +optional
CORS *CORS `json:"cors,omitempty"`

// JWT defines the configuration for JSON Web Token (JWT) authentication.
//
// +optional
JWT *JWT `json:"jwt,omitempty"`
}

// CORS defines the configuration for Cross-Origin Resource Sharing (CORS).
type CORS struct {
// AllowOrigins defines the origins that are allowed to make requests.
// +kubebuilder:validation:MinItems=1
AllowOrigins []StringMatch `json:"allowOrigins,omitempty" yaml:"allowOrigins,omitempty"`
AllowOrigins []StringMatch `json:"allowOrigins,omitempty" yaml:"allowOrigins"`
// AllowMethods defines the methods that are allowed to make requests.
// +kubebuilder:validation:MinItems=1
AllowMethods []string `json:"allowMethods,omitempty" yaml:"allowMethods,omitempty"`
AllowMethods []string `json:"allowMethods,omitempty" yaml:"allowMethods"`
// AllowHeaders defines the headers that are allowed to be sent with requests.
AllowHeaders []string `json:"allowHeaders,omitempty" yaml:"allowHeaders,omitempty"`
// ExposeHeaders defines the headers that can be exposed in the responses.
Expand All @@ -62,6 +69,58 @@ type CORS struct {
MaxAge *metav1.Duration `json:"maxAge,omitempty" yaml:"maxAge,omitempty"`
}

// JWT defines the configuration for JSON Web Token (JWT) authentication.
type JWT struct {

// Providers defines the JSON Web Token (JWT) authentication provider type.
//
// When multiple JWT providers are specified, the JWT is considered valid if
// any of the providers successfully validate the JWT. For additional details,
// see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter.html.
//
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=4
Providers []JWTProvider `json:"providers"`
}

// JWTProvider defines how a JSON Web Token (JWT) can be verified.
type JWTProvider struct {
// Name defines a unique name for the JWT provider. A name can have a variety of forms,
// including RFC1123 subdomains, RFC 1123 labels, or RFC 1035 labels.
//
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Name string `json:"name"`

// Issuer is the principal that issued the JWT and takes the form of a URL or email address.
// For additional details, see https://tools.ietf.org/html/rfc7519#section-4.1.1 for
// URL format and https://rfc-editor.org/rfc/rfc5322.html for email format. If not provided,
// the JWT issuer is not checked.
//
// +kubebuilder:validation:MaxLength=253
// +optional
Issuer string `json:"issuer,omitempty"`

// Audiences is a list of JWT audiences allowed access. For additional details, see
// https://tools.ietf.org/html/rfc7519#section-4.1.3. If not provided, JWT audiences
// are not checked.
//
// +kubebuilder:validation:MaxItems=8
// +optional
Audiences []string `json:"audiences,omitempty"`

// RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote
// HTTP/HTTPS endpoint.
RemoteJWKS RemoteJWKS `json:"remoteJWKS"`

// ClaimToHeaders is a list of JWT claims that must be extracted into HTTP request headers
// For examples, following config:
// The claim must be of type; string, int, double, bool. Array type claims are not supported
//
ClaimToHeaders []ClaimToHeader `json:"claimToHeaders,omitempty"`
// TODO: Add TBD JWT fields based on defined use cases.
}

// StringMatch defines how to match any strings.
// This is a general purpose match condition that can be used by other EG APIs
// that need to match against a string.
Expand Down
1 change: 1 addition & 0 deletions api/v1alpha1/validation/authenticationfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
)

// TODO zhaohuabing remove this file after deprecating authentication filter
// ValidateAuthenticationFilter validates the provided filter. The only supported
// ValidateAuthenticationFilter type is "JWT".
func ValidateAuthenticationFilter(filter *egv1a1.AuthenticationFilter) error {
Expand Down
1 change: 1 addition & 0 deletions api/v1alpha1/validation/authenticationfilter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
)

// TODO zhaohuabing remove this file after deprecating authentication filter
func TestValidateAuthenticationFilter(t *testing.T) {
testCases := []struct {
name string
Expand Down
117 changes: 117 additions & 0 deletions api/v1alpha1/validation/securitypolicy_validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// 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 validation

import (
"errors"
"fmt"
"net/mail"
"net/url"

utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/validation"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
)

// ValidateSecurityPolicy validates the provided SecurityPolicy.
func ValidateSecurityPolicy(policy *egv1a1.SecurityPolicy) error {
var errs []error
if policy == nil {
return errors.New("policy is nil")
}
if err := validateSecurityPolicySpec(&policy.Spec); err != nil {
errs = append(errs, errors.New("policy is nil"))
}

return utilerrors.NewAggregate(errs)
}

// validateSecurityPolicySpec validates the provided spec.
func validateSecurityPolicySpec(spec *egv1a1.SecurityPolicySpec) error {
var errs []error

sum := 0
switch {
case spec == nil:
errs = append(errs, errors.New("spec is nil"))
case spec.CORS != nil:
sum++

Check warning on line 42 in api/v1alpha1/validation/securitypolicy_validate.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/validation/securitypolicy_validate.go#L39-L42

Added lines #L39 - L42 were not covered by tests
case spec.JWT != nil:
sum++
}
if sum == 0 {
errs = append(errs, errors.New("no security policy is specified"))
}

// Return early if any errors exist.
if len(errs) != 0 {
return utilerrors.NewAggregate(errs)
}

if err := ValidateJWTProvider(spec.JWT.Providers); err != nil {
errs = append(errs, err)
}

return utilerrors.NewAggregate(errs)
}

// ValidateJWTProvider validates the provided JWT authentication configuration.
func ValidateJWTProvider(providers []egv1a1.JWTProvider) error {
var errs []error

var names []string
for _, provider := range providers {
switch {
case len(provider.Name) == 0:
errs = append(errs, errors.New("jwt provider cannot be an empty string"))
case len(provider.Issuer) != 0:
// Issuer can take the format of a URL or an email address.
if _, err := url.ParseRequestURI(provider.Issuer); err != nil {
_, err := mail.ParseAddress(provider.Issuer)
if err != nil {
errs = append(errs, fmt.Errorf("invalid issuer; must be a URL or email address: %v", err))
}
}
case len(provider.RemoteJWKS.URI) == 0:
errs = append(errs, fmt.Errorf("uri must be set for remote JWKS provider: %s", provider.Name))
}
if _, err := url.ParseRequestURI(provider.RemoteJWKS.URI); err != nil {
errs = append(errs, fmt.Errorf("invalid remote JWKS URI: %v", err))
}

if len(errs) == 0 {
if strErrs := validation.IsQualifiedName(provider.Name); len(strErrs) != 0 {
for _, strErr := range strErrs {
errs = append(errs, errors.New(strErr))
}
}
// Ensure uniqueness among provider names.
if names == nil {
names = append(names, provider.Name)
} else {
for _, name := range names {
if name == provider.Name {
errs = append(errs, fmt.Errorf("provider name %s must be unique", provider.Name))
} else {
names = append(names, provider.Name)
}
}
}
}

for _, claimToHeader := range provider.ClaimToHeaders {
switch {
case len(claimToHeader.Header) == 0:
errs = append(errs, fmt.Errorf("header must be set for claimToHeader provider: %s", claimToHeader.Header))
case len(claimToHeader.Claim) == 0:
errs = append(errs, fmt.Errorf("claim must be set for claimToHeader provider: %s", claimToHeader.Claim))
}
}
}

return utilerrors.NewAggregate(errs)
}
Loading