Skip to content

Commit

Permalink
Introduce JWT validation (#18)
Browse files Browse the repository at this point in the history
* Add OWNERS

* Add OWNERS

* Create base jwt validation
  • Loading branch information
Jakub Błaszczyk authored Aug 27, 2019
1 parent f406c28 commit 02f4bb1
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 3 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ auth:
mode:
name: INCLUDE
config:
paths:
- path: '/a'
scopes:
- read
Expand Down Expand Up @@ -98,6 +99,7 @@ service:
auth:
name: OAUTH
config:
paths:
- path: '/a'
scopes:
- write
Expand Down
33 changes: 33 additions & 0 deletions api/v2alpha1/jwt_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package v2alpha1

import "k8s.io/apimachinery/pkg/runtime"

// JWTModeConfig config for JWT mode
type JWTModeConfig struct {
Issuer string `json:"issuer"`
JWKS []string `json:"jwks,omitempty"`
Mode InternalConfig `json:"mode"`
}

// InternalConfig internal config, specific for JWT modes
type InternalConfig struct {
Name string `json:"name"`
Config *runtime.RawExtension `json:"config,omitempty"`
}

// JWTModeALL representation of config for the ALL mode
type JWTModeALL struct {
Scopes []string `json:"scopes"`
}

// JWTModeInclude representation of config for the INCLUDE mode
type JWTModeInclude struct {
Paths []IncludePath `json:"paths"`
}

// IncludePath Path for INCLUDE mode
type IncludePath struct {
Path string `json:"path"`
Scopes []string `json:"scopes"`
Methods []string `json:"methods"`
}
108 changes: 108 additions & 0 deletions api/v2alpha1/zz_generated.deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,114 @@ func (in *GatewayResourceStatus) DeepCopy() *GatewayResourceStatus {
return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IncludePath) DeepCopyInto(out *IncludePath) {
*out = *in
if in.Scopes != nil {
in, out := &in.Scopes, &out.Scopes
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Methods != nil {
in, out := &in.Methods, &out.Methods
*out = make([]string, len(*in))
copy(*out, *in)
}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IncludePath.
func (in *IncludePath) DeepCopy() *IncludePath {
if in == nil {
return nil
}
out := new(IncludePath)
in.DeepCopyInto(out)
return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InternalConfig) DeepCopyInto(out *InternalConfig) {
*out = *in
if in.Config != nil {
in, out := &in.Config, &out.Config
*out = new(runtime.RawExtension)
(*in).DeepCopyInto(*out)
}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalConfig.
func (in *InternalConfig) DeepCopy() *InternalConfig {
if in == nil {
return nil
}
out := new(InternalConfig)
in.DeepCopyInto(out)
return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JWTModeALL) DeepCopyInto(out *JWTModeALL) {
*out = *in
if in.Scopes != nil {
in, out := &in.Scopes, &out.Scopes
*out = make([]string, len(*in))
copy(*out, *in)
}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTModeALL.
func (in *JWTModeALL) DeepCopy() *JWTModeALL {
if in == nil {
return nil
}
out := new(JWTModeALL)
in.DeepCopyInto(out)
return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JWTModeConfig) DeepCopyInto(out *JWTModeConfig) {
*out = *in
if in.JWKS != nil {
in, out := &in.JWKS, &out.JWKS
*out = make([]string, len(*in))
copy(*out, *in)
}
in.Mode.DeepCopyInto(&out.Mode)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTModeConfig.
func (in *JWTModeConfig) DeepCopy() *JWTModeConfig {
if in == nil {
return nil
}
out := new(JWTModeConfig)
in.DeepCopyInto(out)
return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JWTModeInclude) DeepCopyInto(out *JWTModeInclude) {
*out = *in
if in.Paths != nil {
in, out := &in.Paths, &out.Paths
*out = make([]IncludePath, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTModeInclude.
func (in *JWTModeInclude) DeepCopy() *JWTModeInclude {
if in == nil {
return nil
}
out := new(JWTModeInclude)
in.DeepCopyInto(out)
return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OauthModeConfig) DeepCopyInto(out *OauthModeConfig) {
*out = *in
Expand Down
40 changes: 40 additions & 0 deletions config/samples/invalid.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,43 @@ spec:
- path: /foo
scopes: [foo]
methods: [POST]
---
apiVersion: gateway.kyma-project.io/v2alpha1
kind: Gate
metadata:
name: jwt-bad-issuer
spec:
gateway: kyma-gateway.kyma-system.svc.cluster.local
service:
name: foo-service
port: 8080
host: foo.bar
auth:
name: JWT
config:
issuer: not-a-valid-url
jwks: []
mode:
name: ALL
config:
scopes: ["foo", "bar"]
---
apiVersion: gateway.kyma-project.io/v2alpha1
kind: Gate
metadata:
name: jwt-bad-mode
spec:
gateway: kyma-gateway.kyma-system.svc.cluster.local
service:
name: foo-service
port: 8080
host: foo.bar
auth:
name: JWT
config:
issuer: http://dex.kyma.local
jwks: []
mode:
name: FOO
config:
foo: bar
20 changes: 20 additions & 0 deletions config/samples/valid.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,23 @@ spec:
# - path: /foo
# scopes: [foo, bar]
# methods: [GET]
# ---
# apiVersion: gateway.kyma-project.io/v2alpha1
# kind: Gate
# metadata:
# name: jwt-all
# spec:
# gateway: kyma-gateway.kyma-system.svc.cluster.local
# service:
# name: foo-service
# port: 8080
# host: foo.bar
# auth:
# name: JWT
# config:
# issuer: http://dex.kyma.local
# jwks: []
# mode:
# name: ALL
# config:
# scopes: ["foo", "bar"]
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/kyma-incubator/api-gateway
go 1.12

require (
github.com/ghodss/yaml v1.0.0
github.com/go-logr/logr v0.1.0
github.com/google/go-cmp v0.3.1 // indirect
github.com/onsi/ginkgo v1.6.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5I
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/zapr v0.1.0 h1:h+WVe9j6HAA01niTJPA/kKH0i7e0rLZBCwauQFcRE54=
Expand Down
49 changes: 48 additions & 1 deletion internal/validation/jwt.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,56 @@
package validation

import "k8s.io/apimachinery/pkg/runtime"
import (
"encoding/json"
"fmt"
"net/url"

gatewayv2alpha1 "github.com/kyma-incubator/api-gateway/api/v2alpha1"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
)

var (
jwtModes = []string{"ALL", "INCLUDE", "EXCLUDE"}
)

type jwt struct{}

func (j *jwt) Validate(config *runtime.RawExtension) error {
var template gatewayv2alpha1.JWTModeConfig

if !configNotEmpty(config) {
return fmt.Errorf("supplied config cannot be empty")
}
err := json.Unmarshal(config.Raw, &template)
if err != nil {
return errors.WithStack(err)
}
if !j.isValidURL(template.Issuer) {
return fmt.Errorf("issuer field is empty or not a valid url")
}
if !j.isValidMode(template.Mode.Name) {
return fmt.Errorf("supplied mode is invalid: %v, valid modes are: ALL, INCLUDE, EXCLUDE", template.Mode.Name)
}
return nil
}

func (j *jwt) isValidURL(toTest string) bool {
if len(toTest) == 0 {
return false
}
_, err := url.ParseRequestURI(toTest)
if err != nil {
return false
}
return true
}

func (j *jwt) isValidMode(mode string) bool {
for _, b := range jwtModes {
if b == mode {
return true
}
}
return false
}
52 changes: 52 additions & 0 deletions internal/validation/jwt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package validation_test

import (
"testing"

"github.com/ghodss/yaml"
gatewayv2alpha1 "github.com/kyma-incubator/api-gateway/api/v2alpha1"
"github.com/kyma-incubator/api-gateway/internal/validation"
"gotest.tools/assert"
"k8s.io/apimachinery/pkg/runtime"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
)

var (
validYamlForJWT = `
issuer: http://dex.kyma.local
jwks: ["a", "b"]
mode:
name: ALL
config:
scopes: ["foo", "bar"]
`
invalidIssuer = `
issuer: this-is-not-an-url
`
invalidJWTMode = `
issuer: http://dex.kyma.local
jwks: ["a", "b"]
mode:
name: CLASSIFIED_MODE_DONT_USE
config:
top: secret
`
logJWT = logf.Log.WithName("jwt-validate-test")
)

func TestJWTValidate(t *testing.T) {
strategy, err := validation.NewFactory(logJWT).StrategyFor(gatewayv2alpha1.JWT)
assert.NilError(t, err)

jsonData, err := yaml.YAMLToJSON([]byte(invalidIssuer))
assert.NilError(t, err)
assert.Error(t, strategy.Validate(&runtime.RawExtension{Raw: jsonData}), "issuer field is empty or not a valid url")

jsonData, err = yaml.YAMLToJSON([]byte(invalidJWTMode))
assert.NilError(t, err)
assert.Error(t, strategy.Validate(&runtime.RawExtension{Raw: jsonData}), "supplied mode is invalid: CLASSIFIED_MODE_DONT_USE, valid modes are: ALL, INCLUDE, EXCLUDE")

jsonData, err = yaml.YAMLToJSON([]byte(validYamlForJWT))
assert.NilError(t, err)
assert.NilError(t, strategy.Validate(&runtime.RawExtension{Raw: jsonData}))
}
4 changes: 2 additions & 2 deletions internal/validation/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ func (o *oauth) Validate(config *runtime.RawExtension) error {
if len(template.Paths) == 0 {
return fmt.Errorf("supplied config does not match internal template")
}
if hasDuplicates(template.Paths) {
if o.hasDuplicates(template.Paths) {
return fmt.Errorf("supplied config is invalid: multiple definitions of the same path detected")
}
return nil
}

func hasDuplicates(elements []gatewayv2alpha1.Option) bool {
func (o *oauth) hasDuplicates(elements []gatewayv2alpha1.Option) bool {
encountered := map[string]bool{}
// Create a map of all unique elements.
for v := range elements {
Expand Down

0 comments on commit 02f4bb1

Please sign in to comment.