Skip to content

Commit

Permalink
feat: add jwt authn to SecurityPolicy (#2079)
Browse files Browse the repository at this point in the history
* add jwt authn to SecurityPolicy

Signed-off-by: huabing zhao <[email protected]>

* rename JWTAuthentication to JWT

Signed-off-by: huabing zhao <[email protected]>

* fix test

Signed-off-by: huabing zhao <[email protected]>

---------

Signed-off-by: huabing zhao <[email protected]>
  • Loading branch information
zhaohuabing authored Oct 26, 2023
1 parent 6b8794e commit c3ee960
Show file tree
Hide file tree
Showing 40 changed files with 2,832 additions and 73 deletions.
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++
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

0 comments on commit c3ee960

Please sign in to comment.