Skip to content
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
15 changes: 10 additions & 5 deletions api/v1alpha1/authenticationfilter_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,18 @@ type JwtAuthenticationFilterProvider struct {
// +kubebuilder:validation:MaxLength=253
Name string `json:"name"`

// Issuer is the principal that issued the JWT. For additional details, see:
// 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
// URL format: https://tools.ietf.org/html/rfc7519#section-4.1.1
// Email format: https://rfc-editor.org/rfc/rfc5322.html
//
// Example:
// URL Example:
// issuer: https://auth.example.com
//
// Email Example:
// issuer: jdoe@example.com
//
// If not provided, the JWT issuer is not checked.
//
// +kubebuilder:validation:MaxLength=253
Expand Down Expand Up @@ -104,8 +109,8 @@ type JwtAuthenticationFilterProvider struct {
// RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote
// HTTP/HTTPS endpoint.
type RemoteJWKS struct {
// URI is the HTTP/HTTPS URI to fetch the JWKS. When using an HTTPS endpoint,
// Envoy's system trust bundle is used to validate the server certificate.
// URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to
// validate the server certificate.
//
// Example:
// uri: https://www.foo.com/oauth2/v1/certs
Expand Down
62 changes: 44 additions & 18 deletions api/v1alpha1/validation/authenticationfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ 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"
)
Expand Down Expand Up @@ -48,32 +50,56 @@ func validateAuthenticationFilterSpec(spec *egv1a1.AuthenticationFilterSpec) err
return utilerrors.NewAggregate(errs)
}

for i := range spec.JwtProviders {
provider := spec.JwtProviders[i]
if err := ValidateJwtProvider(&provider); err != nil {
errs = append(errs, err)
}
if err := ValidateJwtProviders(spec.JwtProviders); err != nil {
errs = append(errs, err)
}

return utilerrors.NewAggregate(errs)
}

// ValidateJwtProvider validates the provided JWT authentication filter provider.
func ValidateJwtProvider(jwt *egv1a1.JwtAuthenticationFilterProvider) error {
// ValidateJwtProviders validates the provided JWT authentication filter providers.
func ValidateJwtProviders(providers []egv1a1.JwtAuthenticationFilterProvider) error {
var errs []error

switch {
case len(jwt.Name) == 0:
errs = append(errs, errors.New("name must be set for jwt provider"))
case len(jwt.Issuer) != 0:
if _, err := url.ParseRequestURI(jwt.Issuer); err != nil {
errs = append(errs, fmt.Errorf("invalid issuer URI: %v", err))
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)
}
}
}
}
case len(jwt.RemoteJWKS.URI) == 0:
errs = append(errs, fmt.Errorf("uri must be set for remote JWKS provider: %s", jwt.Name))
}
if _, err := url.ParseRequestURI(jwt.RemoteJWKS.URI); err != nil {
errs = append(errs, fmt.Errorf("invalid remote JWKS URI: %v", err))
}

return utilerrors.NewAggregate(errs)
Expand Down
126 changes: 125 additions & 1 deletion api/v1alpha1/validation/authenticationfilter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestValidateAuthenticationFilter(t *testing.T) {
expected: false,
},
{
name: "valid authentication filter",
name: "valid authentication filter with url",
filter: &egv1a1.AuthenticationFilter{
TypeMeta: metav1.TypeMeta{
Kind: egv1a1.KindAuthenticationFilter,
Expand All @@ -52,6 +52,60 @@ func TestValidateAuthenticationFilter(t *testing.T) {
},
expected: true,
},
{
name: "valid authentication filter with email",
filter: &egv1a1.AuthenticationFilter{
TypeMeta: metav1.TypeMeta{
Kind: egv1a1.KindAuthenticationFilter,
APIVersion: egv1a1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.AuthenticationFilterSpec{
Type: egv1a1.JwtAuthenticationFilterProviderType,
JwtProviders: []egv1a1.JwtAuthenticationFilterProvider{
{
Name: "test",
Issuer: "test@test.local",
Audiences: []string{"test.local"},
RemoteJWKS: egv1a1.RemoteJWKS{
URI: "https://test.local/jwt/public-key/jwks.json",
},
},
},
},
},
expected: true,
},
{
name: "unqualified authentication provider name",
filter: &egv1a1.AuthenticationFilter{
TypeMeta: metav1.TypeMeta{
Kind: egv1a1.KindAuthenticationFilter,
APIVersion: egv1a1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.AuthenticationFilterSpec{
Type: egv1a1.JwtAuthenticationFilterProviderType,
JwtProviders: []egv1a1.JwtAuthenticationFilterProvider{
{
Name: "unqualified_...",
Issuer: "https://www.test.local",
Audiences: []string{"test.local"},
RemoteJWKS: egv1a1.RemoteJWKS{
URI: "https://test.local/jwt/public-key/jwks.json",
},
},
},
},
},
expected: false,
},
{
name: "unspecified provider name",
filter: &egv1a1.AuthenticationFilter{
Expand Down Expand Up @@ -79,6 +133,49 @@ func TestValidateAuthenticationFilter(t *testing.T) {
},
expected: false,
},
{
name: "non unique provider names",
filter: &egv1a1.AuthenticationFilter{
TypeMeta: metav1.TypeMeta{
Kind: egv1a1.KindAuthenticationFilter,
APIVersion: egv1a1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.AuthenticationFilterSpec{
Type: egv1a1.JwtAuthenticationFilterProviderType,
JwtProviders: []egv1a1.JwtAuthenticationFilterProvider{
{
Name: "unique",
Issuer: "https://www.test.local",
Audiences: []string{"test.local"},
RemoteJWKS: egv1a1.RemoteJWKS{
URI: "https://test.local/jwt/public-key/jwks.json",
},
},
{
Name: "non-unique",
Issuer: "https://www.test.local",
Audiences: []string{"test.local"},
RemoteJWKS: egv1a1.RemoteJWKS{
URI: "https://test.local/jwt/public-key/jwks.json",
},
},
{
Name: "non-unique",
Issuer: "https://www.test.local",
Audiences: []string{"test.local"},
RemoteJWKS: egv1a1.RemoteJWKS{
URI: "https://test.local/jwt/public-key/jwks.json",
},
},
},
},
},
expected: false,
},
{
name: "invalid issuer uri",
filter: &egv1a1.AuthenticationFilter{
Expand Down Expand Up @@ -106,6 +203,33 @@ func TestValidateAuthenticationFilter(t *testing.T) {
},
expected: false,
},
{
name: "inivalid issuer email",
filter: &egv1a1.AuthenticationFilter{
TypeMeta: metav1.TypeMeta{
Kind: egv1a1.KindAuthenticationFilter,
APIVersion: egv1a1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.AuthenticationFilterSpec{
Type: egv1a1.JwtAuthenticationFilterProviderType,
JwtProviders: []egv1a1.JwtAuthenticationFilterProvider{
{
Name: "test",
Issuer: "test@!123...",
Audiences: []string{"test.local"},
RemoteJWKS: egv1a1.RemoteJWKS{
URI: "https://test.local/jwt/public-key/jwks.json",
},
},
},
},
},
expected: false,
},
{
name: "invalid remote jwks uri",
filter: &egv1a1.AuthenticationFilter{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,7 @@ authenticationFilters:
issuer: https://www.test2.local
remoteJWKS:
uri: https://test2.local/jwt/public-key/jwks.json
- name: test3
issuer: https://www.test3.local
remoteJWKS:
uri: https://test3.local/jwt/public-key/jwks.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ xdsIR:
issuer: https://www.test2.local
remoteJWKS:
uri: https://test2.local/jwt/public-key/jwks.json
- name: test3
issuer: https://www.test3.local
remoteJWKS:
uri: https://test3.local/jwt/public-key/jwks.json
destinations:
- host: 7.7.7.7
port: 8080
Expand Down
7 changes: 2 additions & 5 deletions internal/ir/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,8 @@ func (h HTTPRoute) Validate() error {
func (j *JwtRequestAuthentication) Validate() error {
var errs error

for i := range j.Providers {
provider := j.Providers[i]
if err := validation.ValidateJwtProvider(&provider); err != nil {
errs = multierror.Append(errs, err)
}
if err := validation.ValidateJwtProviders(j.Providers); err != nil {
errs = multierror.Append(errs, err)
}

return errs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ spec:
maxItems: 8
type: array
issuer:
description: "Issuer is the principal that issued the JWT.\tFor
additional details, see: \n https://tools.ietf.org/html/rfc7519#section-4.1.1
\n Example: issuer: https://auth.example.com \n If not provided,
the JWT issuer is not checked."
description: "Issuer is the principal that issued the JWT and
takes the form of a URL or email address. For additional details,
see: \n URL format: https://tools.ietf.org/html/rfc7519#section-4.1.1
Email format: https://rfc-editor.org/rfc/rfc5322.html \n URL
Example: issuer: https://auth.example.com \n Email Example:
issuer: jdoe@example.com \n If not provided, the JWT issuer
is not checked."
maxLength: 253
type: string
name:
Expand All @@ -73,10 +76,9 @@ spec:
Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint.
properties:
uri:
description: "URI is the HTTP/HTTPS URI to fetch the JWKS.
When using an HTTPS endpoint, Envoy's system trust bundle
is used to validate the server certificate. \n Example:
uri: https://www.foo.com/oauth2/v1/certs"
description: "URI is the HTTPS URI to fetch the JWKS. Envoy's
system trust bundle is used to validate the server certificate.
\n Example: uri: https://www.foo.com/oauth2/v1/certs"
maxLength: 253
minLength: 1
type: string
Expand Down
Loading