diff --git a/plugins/wasm-go/extensions/jwt-auth/Dockerfile b/plugins/wasm-go/extensions/jwt-auth/Dockerfile new file mode 100644 index 000000000..50c0bbe3b --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/Dockerfile @@ -0,0 +1,2 @@ +FROM scratch +COPY main.wasm plugin.wasm diff --git a/plugins/wasm-go/extensions/jwt-auth/Makefile b/plugins/wasm-go/extensions/jwt-auth/Makefile new file mode 100644 index 000000000..e43f1b692 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/Makefile @@ -0,0 +1,5 @@ +build: + go mod tidy + tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags="custommalloc nottinygc_finalizer" + +default: build \ No newline at end of file diff --git a/plugins/wasm-go/extensions/jwt-auth/VERSION b/plugins/wasm-go/extensions/jwt-auth/VERSION new file mode 100644 index 000000000..6c6aa7cb0 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/VERSION @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/jwt-auth/config/checker.go b/plugins/wasm-go/extensions/jwt-auth/config/checker.go new file mode 100644 index 000000000..a5dc3a0c8 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/config/checker.go @@ -0,0 +1,34 @@ +// Copyright (c) 2023 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +type GlobalAuthType int + +const ( + GlobalAuthTrue GlobalAuthType = 10000 + iota + GlobalAuthFalse + GlobalAuthNoSet +) + +func (c *JWTAuthConfig) GlobalAuthCheck() GlobalAuthType { + if c.GlobalAuth == nil { + return GlobalAuthNoSet + } + + if *c.GlobalAuth { + return GlobalAuthTrue + } + return GlobalAuthFalse +} diff --git a/plugins/wasm-go/extensions/jwt-auth/config/config.go b/plugins/wasm-go/extensions/jwt-auth/config/config.go new file mode 100644 index 000000000..f71b4ed72 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/config/config.go @@ -0,0 +1,125 @@ +// Copyright (c) 2023 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +var ( + // DefaultClaimToHeaderOverride 是 claim_to_override 中 override 字段的默认值 + DefaultClaimToHeaderOverride = true + + // DefaultClockSkewSeconds 是 ClockSkewSeconds 的默认值 + DefaultClockSkewSeconds = int64(60) + + // DefaultKeepToken 是 KeepToken 的默认值 + DefaultKeepToken = true + + // DefaultFromHeader 是 from_header 的默认值 + DefaultFromHeader = []FromHeader{{ + Name: "Authorization", + ValuePrefix: "Bearer ", + }} + + // DefaultFromParams 是 from_params 的默认值 + DefaultFromParams = []string{"access_token"} + + // DefaultFromCookies 是 from_cookies 的默认值 + DefaultFromCookies = []string{} +) + +// JWTAuthConfig defines the struct of the global config of higress wasm plugin jwt-auth. +// https://higress.io/zh-cn/docs/plugins/jwt-auth +type JWTAuthConfig struct { + // 全局配置 + // + // Consumers 配置服务的调用者,用于对请求进行认证 + Consumers []*Consumer `json:"consumers"` + + // 全局配置 + // + // GlobalAuth 若配置为true,则全局生效认证机制; + // 若配置为false,则只对做了配置的域名和路由生效认证机制; + // 若不配置则仅当没有域名和路由配置时全局生效(兼容机制) + GlobalAuth *bool `json:"global_auth,omitempty"` + + // 域名和路由级配置 + // + // Allow 对于符合匹配条件的请求,配置允许访问的consumer名称 + Allow []string `json:"allow"` +} + +// Consumer 配置服务的调用者,用于对请求进行认证 +type Consumer struct { + // Name 配置该consumer的名称 + Name string `json:"name"` + + // JWKs 指定的json格式字符串,是由验证JWT中签名的公钥(或对称密钥)组成的Json Web Key Set + // + // https://www.rfc-editor.org/rfc/rfc7517 + JWKs string `json:"jwks"` + + // Issuer JWT的签发者,需要和payload中的iss字段保持一致 + Issuer string `json:"issuer"` + + // ClaimsToHeaders 抽取JWT的payload中指定字段,设置到指定的请求头中转发给后端 + ClaimsToHeaders *[]ClaimsToHeader `json:"claims_to_headers,omitempty"` + + // FromHeaders 从指定的请求头中抽取JWT + // + // 默认值为 [{"name":"Authorization","value_prefix":"Bearer "}] + // + // 只有当from_headers,from_params,from_cookies均未配置时,才会使用默认值 + FromHeaders *[]FromHeader `json:"from_headers,omitempty"` + + // FromParams 从指定的URL参数中抽取JWT + // + // 默认值为 access_token + // + // 只有当from_headers,from_params,from_cookies均未配置时,才会使用默认值 + FromParams *[]string `json:"from_params,omitempty"` + + // FromCookies 从指定的cookie中抽取JWT + FromCookies *[]string `json:"from_cookies,omitempty"` + + // ClockSkewSeconds 校验JWT的exp和iat字段时允许的时钟偏移量,单位为秒 + // + // 默认值为 60 + ClockSkewSeconds *int64 `json:"clock_skew_seconds,omitempty"` + + // KeepToken 转发给后端时是否保留JWT + // + // 默认值为 true + KeepToken *bool `json:"keep_token,omitempty"` +} + +// ClaimsToHeader 抽取JWT的payload中指定字段,设置到指定的请求头中转发给后端 +type ClaimsToHeader struct { + // Claim JWT payload中的指定字段,要求必须是字符串或无符号整数类型 + Claim string `json:"claim"` + + // Header 从payload取出字段的值设置到这个请求头中,转发给后端 + Header string `json:"header"` + + // Override true时,存在同名请求头会进行覆盖;false时,追加同名请求头 + // + // 默认值为 true + Override *bool `json:"override,omitempty"` +} + +// FromHeader 从指定的请求头中抽取JWT +type FromHeader struct { + // Name 抽取JWT的请求header + Name string `json:"name"` + // ValuePrefix 对请求header的value去除此前缀,剩余部分作为JWT + ValuePrefix string `json:"value_prefix"` +} diff --git a/plugins/wasm-go/extensions/jwt-auth/config/parser.go b/plugins/wasm-go/extensions/jwt-auth/config/parser.go new file mode 100644 index 000000000..83bd8f878 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/config/parser.go @@ -0,0 +1,138 @@ +// Copyright (c) 2023 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "encoding/json" + "fmt" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/go-jose/go-jose/v3" + "github.com/tidwall/gjson" +) + +// RuleSet 插件是否至少在一个 domain 或 route 上生效 +var RuleSet bool + +// ParseGlobalConfig 从wrapper提供的配置中解析并转换到插件运行时需要使用的配置。 +// 此处解析的是全局配置,域名和路由级配置由 ParseRuleConfig 负责。 +func ParseGlobalConfig(json gjson.Result, config *JWTAuthConfig, log wrapper.Log) error { + RuleSet = false + consumers := json.Get("consumers") + if !consumers.IsArray() { + return fmt.Errorf("failed to parse configuration for consumers: consumers is not a array") + } + + consumerNames := map[string]struct{}{} + for _, v := range consumers.Array() { + c, err := ParseConsumer(v, consumerNames) + if err != nil { + log.Warn(err.Error()) + continue + } + config.Consumers = append(config.Consumers, c) + } + if len(config.Consumers) == 0 { + return fmt.Errorf("at least one consumer should be configured for a rule") + } + + return nil +} + +// ParseRuleConfig 从wrapper提供的配置中解析并转换到插件运行时需要使用的配置。 +// 此处解析的是域名和路由级配置,全局配置由 ParseConfig 负责。 +func ParseRuleConfig(json gjson.Result, global JWTAuthConfig, config *JWTAuthConfig, log wrapper.Log) error { + // override config via global + *config = global + + allow := json.Get("allow") + if !allow.Exists() { + return fmt.Errorf("allow is required") + } + + if len(allow.Array()) == 0 { + return fmt.Errorf("allow cannot be empty") + } + + for _, item := range allow.Array() { + config.Allow = append(config.Allow, item.String()) + } + + RuleSet = true + return nil +} + +func ParseConsumer(consumer gjson.Result, names map[string]struct{}) (c *Consumer, err error) { + c = &Consumer{} + + // 从gjson中取得原始JSON字符串,并使用标准库反序列化,以降低代码复杂度。 + err = json.Unmarshal([]byte(consumer.Raw), c) + if err != nil { + return nil, fmt.Errorf("failed to parse consumer: %s", err.Error()) + } + + // 检查consumer是否重复 + if _, ok := names[c.Name]; ok { + return nil, fmt.Errorf("consumer already exists: %s", c.Name) + } + + // 检查JWKs是否合法 + jwks := &jose.JSONWebKeySet{} + err = json.Unmarshal([]byte(c.JWKs), jwks) + if err != nil { + return nil, fmt.Errorf("jwks is invalid, consumer:%s, status:%s, jwks:%s", c.Name, err.Error(), c.JWKs) + } + + // 检查是否需要使用默认jwt抽取来源 + if c.FromHeaders == nil && c.FromParams == nil && c.FromCookies == nil { + c.FromHeaders = &DefaultFromHeader + c.FromParams = &DefaultFromParams + c.FromCookies = &DefaultFromCookies + } + + // 检查ClaimsToHeaders + if c.ClaimsToHeaders != nil { + // header去重 + c2h := map[string]struct{}{} + + // 此处需要先把指针解引用到临时变量 + tmp := *c.ClaimsToHeaders + for i := range tmp { + if _, ok := c2h[tmp[i].Header]; ok { + return nil, fmt.Errorf("claim to header already exists: %s", c2h[tmp[i].Header]) + } + c2h[tmp[i].Header] = struct{}{} + + // 为Override填充默认值 + if tmp[i].Override == nil { + tmp[i].Override = &DefaultClaimToHeaderOverride + } + } + } + + // 为ClockSkewSeconds填充默认值 + if c.ClockSkewSeconds == nil { + c.ClockSkewSeconds = &DefaultClockSkewSeconds + } + + // 为KeepToken填充默认值 + if c.KeepToken == nil { + c.KeepToken = &DefaultKeepToken + } + + // consumer合法,记录consumer名称 + names[c.Name] = struct{}{} + return c, nil +} diff --git a/plugins/wasm-go/extensions/jwt-auth/go.mod b/plugins/wasm-go/extensions/jwt-auth/go.mod new file mode 100644 index 000000000..805b0af4a --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/go.mod @@ -0,0 +1,22 @@ +module github.com/alibaba/higress/plugins/wasm-go/extensions/jwt-auth + +go 1.19 + +replace github.com/alibaba/higress/plugins/wasm-go => ../.. + +require ( + github.com/alibaba/higress/plugins/wasm-go v1.3.5 + github.com/go-jose/go-jose/v3 v3.0.3 + github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc + github.com/tidwall/gjson v1.17.1 +) + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect + github.com/magefile/mage v1.15.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/resp v0.1.1 // indirect + golang.org/x/crypto v0.23.0 // indirect +) diff --git a/plugins/wasm-go/extensions/jwt-auth/go.sum b/plugins/wasm-go/extensions/jwt-auth/go.sum new file mode 100644 index 000000000..655b48cf4 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/go.sum @@ -0,0 +1,71 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA= +github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew= +github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ= +github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE= +github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/plugins/wasm-go/extensions/jwt-auth/go.work.sum b/plugins/wasm-go/extensions/jwt-auth/go.work.sum new file mode 100644 index 000000000..d4ca0c121 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/go.work.sum @@ -0,0 +1,4 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/wasm-go/extensions/jwt-auth/handler/claims.go b/plugins/wasm-go/extensions/jwt-auth/handler/claims.go new file mode 100644 index 000000000..c1333ad56 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/handler/claims.go @@ -0,0 +1,33 @@ +// Copyright (c) 2023 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "fmt" + + cfg "github.com/alibaba/higress/plugins/wasm-go/extensions/jwt-auth/config" + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" +) + +func claimsToHeader(claims map[string]any, cth []cfg.ClaimsToHeader) { + for i := range cth { + if v, ok := claims[cth[i].Claim]; ok { + if *cth[i].Override { + proxywasm.ReplaceHttpRequestHeader(cth[i].Header, fmt.Sprint(v)) + } + proxywasm.AddHttpRequestHeader(cth[i].Header, fmt.Sprint(v)) + } + } +} diff --git a/plugins/wasm-go/extensions/jwt-auth/handler/extractor.go b/plugins/wasm-go/extensions/jwt-auth/handler/extractor.go new file mode 100644 index 000000000..1261376d2 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/handler/extractor.go @@ -0,0 +1,146 @@ +// Copyright (c) 2023 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "net/url" + "strings" + + cfg "github.com/alibaba/higress/plugins/wasm-go/extensions/jwt-auth/config" +) + +// extracToken 从三个来源中依次尝试抽取Token,若找不到Token则返回空字符串 +func extractToken(keepToken bool, consumer *cfg.Consumer, header HeaderProvider, log Logger) string { + token := "" + + // 1. 从header中抽取token + if h := consumer.FromHeaders; h != nil { + token = extractFromHeader(keepToken, *h, header, log) + } + if token != "" { + return token + } + + // 2. 从params中抽取token + if p := consumer.FromParams; p != nil { + token = extractFromParams(keepToken, *p, header, log) + } + if token != "" { + return token + } + + // 3. 从cookies中抽取token + if c := consumer.FromCookies; c != nil { + token = extractFromCookies(keepToken, *c, header, log) + } + + // 此处无需判空 + return token +} + +func extractFromHeader(keepToken bool, headers []cfg.FromHeader, header HeaderProvider, log Logger) (token string) { + for i := range headers { + + // proxywasm 获取到的 header name 均为小写,此处需做修改 + lowerName := strings.ToLower(headers[i].Name) + token, err := header.GetHttpRequestHeader(lowerName) + if err != nil { + log.Warnf("failed to get authorization: %v", err) + continue + } + + if token != "" { + if !strings.HasPrefix(token, headers[i].ValuePrefix) { + log.Warnf("authorization has no prefix %q", headers[i].ValuePrefix) + return "" + } + if !keepToken { + _ = header.RemoveHttpRequestHeader(lowerName) + } + return strings.TrimPrefix(token, headers[i].ValuePrefix) + } + } + return "" +} + +func extractFromParams(keepToken bool, params []string, header HeaderProvider, log Logger) (token string) { + urlparams, err := header.GetHttpRequestHeader(":path") + if err != nil { + log.Warnf("failed to get authorization: %v", err) + return "" + } + + url, _ := url.Parse(urlparams) + query := url.Query() + + for i := range params { + token := query.Get(params[i]) + if token != "" { + if !keepToken { + query.Del(params[i]) + } + return token + } + } + return "" +} + +func extractFromCookies(keepToken bool, cookies []string, header HeaderProvider, log Logger) (token string) { + requestCookies, err := header.GetHttpRequestHeader("cookie") + if err != nil { + log.Warnf("failed to get authorization: %v", err) + return "" + } + + for i := range cookies { + token := findCookie(requestCookies, cookies[i]) + if token != "" { + if !keepToken { + _ = header.ReplaceHttpRequestHeader("cookie", deleteCookie(requestCookies, cookies[i])) + } + return token + } + } + + return "" +} + +func findCookie(cookie string, key string) string { + value := "" + pairs := strings.Split(cookie, ";") + + for _, pair := range pairs { + pair = strings.TrimSpace(pair) + kv := strings.Split(pair, "=") + if kv[0] == key { + value = kv[1] + break + } + } + return value +} + +func deleteCookie(cookie string, key string) string { + result := "" + pairs := strings.Split(cookie, ";") + + for _, pair := range pairs { + pair = strings.TrimSpace(pair) + if !strings.HasPrefix(pair, key) { + result += pair + ";" + } + } + return strings.TrimSuffix(result, ";") +} diff --git a/plugins/wasm-go/extensions/jwt-auth/handler/handler.go b/plugins/wasm-go/extensions/jwt-auth/handler/handler.go new file mode 100644 index 000000000..41dfbc1bb --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/handler/handler.go @@ -0,0 +1,159 @@ +// Copyright (c) 2023 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "time" + + cfg "github.com/alibaba/higress/plugins/wasm-go/extensions/jwt-auth/config" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types" +) + +// jwt-auth 插件认证逻辑与 basic-auth 一致: +// - global_auth == true 开启全局生效: +// - 若当前 domain/route 未配置 allow 列表,即未配置该插件:则在所有 consumers 中查找,如果找到则认证通过,否则认证失败 (1*) +// - 若当前 domain/route 配置了该插件:则在 allow 列表中查找,如果找到则认证通过,否则认证失败 +// +// - global_auth == false 非全局生效:(2*) +// - 若当前 domain/route 未配置该插件:则直接放行 +// - 若当前 domain/route 配置了该插件:则在 allow 列表中查找,如果找到则认证通过,否则认证失败 +// +// - global_auth 未设置: +// - 若没有一个 domain/route 配置该插件:则遵循 (1*) +// - 若有至少一个 domain/route 配置该插件:则遵循 (2*) +// +// https://github.com/alibaba/higress/blob/e09edff827b94fa5bcc149bbeadc905361100c2a/plugins/wasm-go/extensions/basic-auth/main.go#L191 +func OnHTTPRequestHeaders(ctx wrapper.HttpContext, config cfg.JWTAuthConfig, log wrapper.Log) types.Action { + var ( + noAllow = len(config.Allow) == 0 // 未配置 allow 列表,表示插件在该 domain/route 未生效 + globalAuthNoSet = config.GlobalAuthCheck() == cfg.GlobalAuthNoSet + globalAuthSetTrue = config.GlobalAuthCheck() == cfg.GlobalAuthTrue + globalAuthSetFalse = config.GlobalAuthCheck() == cfg.GlobalAuthFalse + ) + + // 不需要认证而直接放行的情况: + // - global_auth == false 且 当前 domain/route 未配置该插件 + // - global_auth 未设置 且 有至少一个 domain/route 配置该插件 且 当前 domain/route 未配置该插件 + if globalAuthSetFalse || (cfg.RuleSet && globalAuthNoSet) { + if noAllow { + log.Info("authorization is not required") + return types.ActionContinue + } + } + + header := &proxywasmProvider{} + actionMap := map[string]func() types.Action{} + unAuthzConsumer := "" + + // 匹配consumer + for i := range config.Consumers { + err := consumerVerify(config.Consumers[i], time.Now(), header, log) + if err != nil { + log.Warn(err.Error()) + if v, ok := err.(*ErrDenied); ok { + actionMap[config.Consumers[i].Name] = v.denied + } + continue + } + + // 全局生效: + // - global_auth == true 且 当前 domain/route 未配置该插件 + // - global_auth 未设置 且 没有任何一个 domain/route 配置该插件 + if (globalAuthSetTrue && noAllow) || (globalAuthNoSet && !cfg.RuleSet) { + log.Infof("consumer %q authenticated", config.Consumers[i].Name) + return authenticated(config.Consumers[i].Name) + } + + // 全局生效,但当前 domain/route 配置了 allow 列表 + if globalAuthSetTrue && !noAllow { + if !contains(config.Consumers[i].Name, config.Allow) { + log.Warnf("jwt verify failed, consumer %q not allow", + config.Consumers[i].Name) + actionMap[config.Consumers[i].Name] = deniedUnauthorizedConsumer + unAuthzConsumer = config.Consumers[i].Name + continue + } + log.Infof("consumer %q authenticated", config.Consumers[i].Name) + return authenticated(config.Consumers[i].Name) + } + + // 非全局生效 + if globalAuthSetFalse || (globalAuthNoSet && cfg.RuleSet) { + if !noAllow { // 配置了 allow 列表 + if !contains(config.Consumers[i].Name, config.Allow) { + log.Warnf("jwt verify failed, consumer %q not allow", + config.Consumers[i].Name) + actionMap[config.Consumers[i].Name] = deniedUnauthorizedConsumer + unAuthzConsumer = config.Consumers[i].Name + continue + } + log.Infof("consumer %q authenticated", config.Consumers[i].Name) + return authenticated(config.Consumers[i].Name) + } + } + + // switch config.GlobalAuthCheck() { + + // case cfg.GlobalAuthNoSet: + // if !cfg.RuleSet { + // log.Infof("consumer %q authenticated", config.Consumers[i].Name) + // return authenticated(config.Consumers[i].Name) + // } + // case cfg.GlobalAuthTrue: + // if len(config.Allow) == 0 { + // log.Infof("consumer %q authenticated", config.Consumers[i].Name) + // return authenticated(config.Consumers[i].Name) + // } + // fallthrough // 若 allow 列表不为空,则 fallthrough 到需要检查 allow 列表的逻辑中 + + // // 全局生效设置为 false + // case cfg.GlobalAuthFalse: + // if !contains(config.Consumers[i].Name, config.Allow) { + // log.Warnf("jwt verify failed, consumer %q not allow", + // config.Consumers[i].Name) + // actionMap[config.Consumers[i].Name] = deniedUnauthorizedConsumer + // unAuthzConsumer = config.Consumers[i].Name + // continue + // } + // log.Infof("consumer %q authenticated", config.Consumers[i].Name) + // return authenticated(config.Consumers[i].Name) + // } + } + + if len(config.Allow) == 1 { + if unAuthzConsumer != "" { + log.Warnf("consumer %q denied", unAuthzConsumer) + return deniedUnauthorizedConsumer() + } + if v, ok := actionMap[config.Allow[0]]; ok { + log.Warnf("consumer %q denied", config.Allow[0]) + return v() + } + } + + // 拒绝兜底 + log.Warnf("all consumers verify failed") + return deniedNotAllow() +} + +func contains(str string, arr []string) bool { + for _, i := range arr { + if i == str { + return true + } + } + return false +} diff --git a/plugins/wasm-go/extensions/jwt-auth/handler/verify.go b/plugins/wasm-go/extensions/jwt-auth/handler/verify.go new file mode 100644 index 000000000..12706cd52 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/handler/verify.go @@ -0,0 +1,212 @@ +// Copyright (c) 2023 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "encoding/json" + "fmt" + "time" + + cfg "github.com/alibaba/higress/plugins/wasm-go/extensions/jwt-auth/config" + "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3/jwt" + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types" +) + +var protectionSpace = "MSE Gateway" // 认证失败时,返回响应头 WWW-Authenticate: JWT realm=MSE Gateway + +type ErrDenied struct { + msg string + denied func() types.Action +} + +type Logger interface { + Warnf(format string, args ...interface{}) +} + +type HeaderProvider interface { + GetHttpRequestHeader(key string) (string, error) + ReplaceHttpRequestHeader(key string, value string) error + RemoveHttpRequestHeader(key string) error +} + +type proxywasmProvider struct{} + +func (p *proxywasmProvider) GetHttpRequestHeader(key string) (string, error) { + return proxywasm.GetHttpRequestHeader(key) +} + +func (p *proxywasmProvider) ReplaceHttpRequestHeader(key string, value string) error { + return proxywasm.ReplaceHttpRequestHeader(key, value) +} + +func (p *proxywasmProvider) RemoveHttpRequestHeader(key string) error { + return proxywasm.RemoveHttpRequestHeader(key) +} + +func (e *ErrDenied) Error() string { + return e.msg +} + +func consumerVerify(consumer *cfg.Consumer, verifyTime time.Time, header HeaderProvider, log Logger) error { + tokenStr := extractToken(*consumer.KeepToken, consumer, header, log) + if tokenStr == "" { + return &ErrDenied{ + msg: fmt.Sprintf("jwt is missing, consumer: %s", consumer.Name), + denied: deniedJWTMissing, + } + } + + // 当前版本的higress暂不支持jwe,此处用ParseSigned + token, err := jwt.ParseSigned(tokenStr) + if err != nil { + return &ErrDenied{ + msg: fmt.Sprintf("jwt parse failed, consumer: %s, token: %s, reason: %s", + consumer.Name, + tokenStr, + err.Error(), + ), + denied: deniedJWTVerificationFails, + } + } + + // 此处可以直接使用 JSON 反序列 jwks + jwks := jose.JSONWebKeySet{} + err = json.Unmarshal([]byte(consumer.JWKs), &jwks) + if err != nil { + return &ErrDenied{ + msg: fmt.Sprintf("jwt parse failed, consumer: %s, token: %s, reason: %s", + consumer.Name, + tokenStr, + err.Error(), + ), + denied: deniedJWTVerificationFails, + } + } + + out := jwt.Claims{} + rawClaims := map[string]any{} + + // 提前确认 kid 状态 + var kid string + var key jose.JSONWebKey + for _, header := range token.Headers { + if header.KeyID != "" { + kid = header.KeyID + break + } + } + // 没有 kid 时选择第一个 key + if kid == "" { + key = jwks.Keys[0] + } + + keys := jwks.Key(kid) + if len(keys) == 0 { // kid 不存在时选择第一个 key + key = jwks.Keys[0] + } else { + key = keys[0] + } + + // Claims 支持直接传入 jose 的 jwk + // 无需额外调用verify,claims内部已进行验证 + err = token.Claims(key, &out) + if err != nil { + return &ErrDenied{ + msg: fmt.Sprintf("jwt verify failed, consumer: %s, token: %s, reason: %s", + consumer.Name, + tokenStr, + err.Error(), + ), + denied: deniedJWTVerificationFails, + } + } + + if out.Issuer != consumer.Issuer { + return &ErrDenied{ + msg: fmt.Sprintf("jwt verify failed, consumer: %s, token: %s, reason: issuer does not equal", + consumer.Name, + tokenStr, + ), + denied: deniedJWTVerificationFails, + } + } + + // 检查是否过期 + err = out.ValidateWithLeeway( + jwt.Expected{ + Issuer: consumer.Issuer, + Time: verifyTime, + }, + time.Duration(*consumer.ClockSkewSeconds)*time.Second, + ) + if err != nil { + return &ErrDenied{ + msg: fmt.Sprintf("jwt verify failed, consumer: %s, token: %s, reason: %s", + consumer.Name, + tokenStr, + err.Error(), + ), + denied: deniedJWTExpired, + } + } + + if consumer.ClaimsToHeaders != nil { + claimsToHeader(rawClaims, *consumer.ClaimsToHeaders) + } + return nil +} + +func deniedJWTMissing() types.Action { + _ = proxywasm.SendHttpResponse(401, WWWAuthenticateHeader(protectionSpace), + []byte("Request denied by JWT Auth check. JWT is missing."), -1) + return types.ActionContinue +} + +func deniedJWTExpired() types.Action { + _ = proxywasm.SendHttpResponse(401, WWWAuthenticateHeader(protectionSpace), + []byte("Request denied by JWT Auth check. JWT is expried."), -1) + return types.ActionContinue +} + +func deniedJWTVerificationFails() types.Action { + _ = proxywasm.SendHttpResponse(401, WWWAuthenticateHeader(protectionSpace), + []byte("Request denied by JWT Auth check. JWT verification fails"), -1) + return types.ActionContinue +} + +func deniedUnauthorizedConsumer() types.Action { + _ = proxywasm.SendHttpResponse(403, WWWAuthenticateHeader(protectionSpace), + []byte("Request denied by JWT Auth check. Unauthorized consumer."), -1) + return types.ActionContinue +} + +func deniedNotAllow() types.Action { + _ = proxywasm.SendHttpResponse(403, WWWAuthenticateHeader(protectionSpace), + []byte("Request denied by JWT Auth check. JWT token not allow."), -1) + return types.ActionContinue +} + +func authenticated(name string) types.Action { + _ = proxywasm.AddHttpRequestHeader("X-Mse-Consumer", name) + return types.ActionContinue +} + +func WWWAuthenticateHeader(realm string) [][2]string { + return [][2]string{ + {"WWW-Authenticate", fmt.Sprintf("JWT realm=%s", realm)}, + } +} diff --git a/plugins/wasm-go/extensions/jwt-auth/handler/verify_test.go b/plugins/wasm-go/extensions/jwt-auth/handler/verify_test.go new file mode 100644 index 000000000..66bf29d70 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/handler/verify_test.go @@ -0,0 +1,129 @@ +package handler + +import ( + "errors" + "testing" + "time" + + "github.com/alibaba/higress/plugins/wasm-go/extensions/jwt-auth/config" + "github.com/tidwall/gjson" +) + +type testLogger struct { + T *testing.T +} + +func (l *testLogger) Warnf(format string, args ...interface{}) { + l.T.Logf(format, args...) +} + +type testProvider struct { + headerMap map[string]string +} + +func (p *testProvider) GetHttpRequestHeader(key string) (string, error) { + if v, ok := p.headerMap[key]; ok { + return v, nil + } + return "", errors.New("no found") +} + +func (p *testProvider) ReplaceHttpRequestHeader(key string, value string) error { + p.headerMap[key] = value + return nil +} + +func (p *testProvider) RemoveHttpRequestHeader(key string) error { + delete(p.headerMap, key) + return nil +} + +const ( + ES256Allow string = "eyJhbGciOiJFUzI1NiIsImtpZCI6InAyNTYiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MjAxOTY4NjQwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.hm71YWfjALshUAgyOu-r9W2WBG_zfqIZZacAbc7oIH1r7dbB0sGQn3wKMWMmOzmxX0UyaVZ0KMk-HFTA1hDnBQ" + ES256Expried string = "eyJhbGciOiJFUzI1NiIsImtpZCI6InAyNTYiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MTcwNDA2NzIwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.9AnXd2rZ6FirHZQAoabyL4xZNz0jr-3LmcV4-pFV3JrdtUT4386Mw5Qan125fUB-rZf_ZBlv0Bft2tWY149fyg" + RS256Allow string = "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MjAxOTY4NjQwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.iO0wPY91b_VNGUMZ1n-Ub-SRmEkDQMFLSi77z49tEzll3UZXwmBraP5udM_OPUAdk9ZO3dbb_fOgdcN9V1H9p5kiTr-l-pZTFTJHrPJj8wC519sYRcCk3wrZ9aXR5tNMwOsMdQb7waTBatDQLmHPWzAoTNBc8mwXkRcv1dmJLvsJgxyCl1I9CMOMPq0fYj1NBvaUDIdVSL1o7GGiriD8-0UIOmS72-I3mbaoCIyVb0h3wx7gnIW3zr0yYWaYoiIgmHLag-eEGxHp4-BjtCqcokU4QVMS91qpH7Mkl1iv2WHEkuDQRJ-nLzYGwXb7Dncx9K5tNWHJuZ-DihIU2oT0aA" + RS256Expried string = "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MTcwNDA2NzIwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.jqzlhBPk9mmvtTT5aCYf-_5uXXSEU5bQ32fx78XeboCnjR9K1CsI4KYUIkXEX3bk66XJQUeSes7lz3gA4Yzkd-v9oADHTgpKnIxzv_5mD0_afIwEFjcalqVbSvCmro4PessQZDnmU7AIzoo3RPSqbmq8xbPVYUH9I-OO8aUu2ATd1HozgxJH1XnRU8k9KMkVW8XhvJXLKZJmnqe3Tu6pCU_tawFlBfBC4fAhMf0yX2CGE0ABAHubcdiI6JXObQmQQ9Or2a-g2a8g_Bw697PoPOsAn0YpTrHst9GcyTpkbNTAq9X8fc5EM7hiDM1FGeMYcaQTdMnOh4HBhP0p4YEhvA" + JWKs string = "{\"keys\":[{\"kty\":\"EC\",\"kid\":\"p256\",\"crv\":\"P-256\",\"x\":\"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\"y\":\"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"},{\"kty\":\"RSA\",\"kid\":\"rsa\",\"n\":\"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\"e\":\"AQAB\"}]}" +) +const ( + consumers = `{ + "consumers": [ + { + "name": "consumer1", + "issuer": "higress-test", + "jwks": "{\n\"keys\": [\n{\n\"kty\": \"EC\",\n\"kid\": \"p256\",\n\"crv\": \"P-256\",\n\"x\": \"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\n\"y\": \"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"\n},\n{\n\"kty\": \"RSA\",\n\"kid\": \"rsa\",\n\"n\": \"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\n\"e\": \"AQAB\"\n}\n]\n}" + }, + { + "name": "consumer_hedaer", + "issuer": "higress-test", + "jwks": "{\n\"keys\": [\n{\n\"kty\": \"EC\",\n\"kid\": \"p256\",\n\"crv\": \"P-256\",\n\"x\": \"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\n\"y\": \"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"\n},\n{\n\"kty\": \"RSA\",\n\"kid\": \"rsa\",\n\"n\": \"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\n\"e\": \"AQAB\"\n}\n]\n}", + "from_headers": [ + { + "name": "jwt", + "value_prefix": "Bearer " + } + ] + }, + { + "name": "consumer_params", + "issuer": "higress-test", + "jwks": "{\n\"keys\": [\n{\n\"kty\": \"EC\",\n\"kid\": \"p256\",\n\"crv\": \"P-256\",\n\"x\": \"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\n\"y\": \"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"\n},\n{\n\"kty\": \"RSA\",\n\"kid\": \"rsa\",\n\"n\": \"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\n\"e\": \"AQAB\"\n}\n]\n}", + "from_params": [ + "jwt_token" + ] + }, + { + "name": "consumer_cookies", + "issuer": "higress-test", + "jwks": "{\n\"keys\": [\n{\n\"kty\": \"EC\",\n\"kid\": \"p256\",\n\"crv\": \"P-256\",\n\"x\": \"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\n\"y\": \"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"\n},\n{\n\"kty\": \"RSA\",\n\"kid\": \"rsa\",\n\"n\": \"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\n\"e\": \"AQAB\"\n}\n]\n}", + "from_cookies": [ + "jwt_token" + ] + } + ] + }` +) + +func TestConsumerVerify(t *testing.T) { + log := &testLogger{ + T: t, + } + cs := []*config.Consumer{} + + c := gjson.Parse(consumers).Get("consumers") + if !c.IsArray() { + t.Error("failed to parse configuration for consumers: consumers is not a array") + return + } + + consumerNames := map[string]struct{}{} + for _, v := range c.Array() { + c, err := config.ParseConsumer(v, consumerNames) + if err != nil { + t.Log(err.Error()) + continue + } + cs = append(cs, c) + } + if len(cs) == 0 { + t.Error("at least one consumer should be configured for a rule") + return + } + + header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + ES256Allow}} + err := consumerVerify(&config.Consumer{ + Name: "consumer1", + JWKs: JWKs, + Issuer: "higress-test", + ClaimsToHeaders: &[]config.ClaimsToHeader{}, + FromHeaders: &[]config.FromHeader{{Name: "jwt", ValuePrefix: "Bearer "}}, + ClockSkewSeconds: &config.DefaultClockSkewSeconds, + KeepToken: &config.DefaultKeepToken, + }, time.Now(), header, log) + + if err != nil { + if v, ok := err.(*ErrDenied); ok { + t.Error(v.msg) + } + } +} diff --git a/plugins/wasm-go/extensions/jwt-auth/main.go b/plugins/wasm-go/extensions/jwt-auth/main.go new file mode 100644 index 000000000..1961278ce --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/main.go @@ -0,0 +1,49 @@ +// Copyright (c) 2023 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/alibaba/higress/plugins/wasm-go/extensions/jwt-auth/config" + "github.com/alibaba/higress/plugins/wasm-go/extensions/jwt-auth/handler" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +// @Name jwt-proxy +// @Category auth +// @Phase UNSPECIFIED_PHASE +// @Priority 0 +// @Title zh-CN jwt验证 +// @Description zh-CN 通过jwt进行验证 +// @Version 0.1.0 +// +// @Contact.name Ink33 +// @Contact.url https://github.com/Ink-33 +// @Contact.email ink33@smlk.org +// +// @Example +//{} +// @End + +func main() { + wrapper.SetCtx( + // 插件名称 + "jwt-auth", + // 为解析插件配置,设置自定义函数 + wrapper.ParseConfigBy(config.ParseGlobalConfig), + wrapper.ParseOverrideConfigBy(config.ParseGlobalConfig, config.ParseRuleConfig), + // 为处理请求头,设置自定义函数 + wrapper.ProcessRequestHeadersBy(handler.OnHTTPRequestHeaders), + ) +} diff --git a/plugins/wasm-go/extensions/jwt-auth/option.yaml b/plugins/wasm-go/extensions/jwt-auth/option.yaml new file mode 100644 index 000000000..8be6bfd1c --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/option.yaml @@ -0,0 +1,52 @@ +# File generated by hgctl. Modify as required. + +version: 1.0.0 + +build: + # The official builder image version + builder: + go: 1.19 + tinygo: 0.28.1 + oras: 1.0.0 + # The WASM plugin project directory + input: ./ + # The output of the build products + output: + # Choose between 'files' and 'image' + type: files + # Destination address: when type=files, specify the local directory path, e.g., './out' or + # type=image, specify the remote docker repository, e.g., 'docker.io//' + dest: ./out + # The authentication configuration for pushing image to the docker repository + docker-auth: ~/.docker/config.json + # The directory for the WASM plugin configuration structure + model-dir: ./ + # The WASM plugin configuration structure name + model: PluginConfig + # Enable debug mode + debug: false + +test: + # Test environment name, that is a docker compose project name + name: wasm-test + # The output path to build products, that is the source of test configuration parameters + from-path: ./out + # The test configuration source + test-path: ./test + # Docker compose configuration, which is empty, looks for the following files from 'test-path': + # compose.yaml, compose.yml, docker-compose.yml, docker-compose.yaml + compose-file: + # Detached mode: Run containers in the background + detach: false + +install: + # The namespace of the installation + namespace: higress-system + # Use to validate WASM plugin configuration when install by yaml + spec-yaml: ./out/spec.yaml + # Installation source. Choose between 'from-yaml' and 'from-go-project' + from-yaml: ./test/plugin-conf.yaml + # If 'from-go-src' is non-empty, the output type of the build option must be 'image' + from-go-src: + # Enable debug mode + debug: false diff --git a/plugins/wasm-go/extensions/jwt-auth/test/jwt_test.go b/plugins/wasm-go/extensions/jwt-auth/test/jwt_test.go new file mode 100644 index 000000000..fa2da3f5e --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/test/jwt_test.go @@ -0,0 +1,144 @@ +package test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "os" + "testing" + "time" + + "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3/jwt" +) + +type keySet struct { + Name string + PrivateKey any + PublicKey any +} + +type jwts struct { + JWTs []struct { + Algorithm string `json:"alg"` + Token string `json:"token"` + Type string `json:"type"` + } `json:"jwts"` +} + +func genPrivateKey() (keySets map[string]keySet) { + keySets = map[string]keySet{} + rsaPri, _ := rsa.GenerateKey(rand.Reader, 2048) + keySets["rsa"] = keySet{Name: "rsa", PrivateKey: rsaPri, PublicKey: &rsaPri.PublicKey} + + // ed25519pri, ed25519pub, _ := ed25519.GenerateKey(rand.Reader) + // keySets["ed25519"] = keySet{Name: "ed25519", PrivateKey: ed25519pri, PublicKey: ed25519pub} + + p256Pri, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + keySets["p256"] = keySet{Name: "p256", PrivateKey: p256Pri, PublicKey: &p256Pri.PublicKey} + + // p384Pri, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + // keySets = append(keySets, keySet{Name: "p384", PrivateKey: p384Pri, PublicKey: &p384Pri.PublicKey}) + + // p521Pri, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + // keySets = append(keySets, keySet{Name: "p521", PrivateKey: p521Pri, PublicKey: &p521Pri.PublicKey}) + return +} + +func genJWKs(keySets map[string]keySet) (keys jose.JSONWebKeySet) { + for k := range keySets { + k := jose.JSONWebKey{ + Key: keySets[k].PublicKey, + KeyID: keySets[k].Name, + } + keys.Keys = append(keys.Keys, k) + } + return +} + +func genJWTs(keySets map[string]keySet) (jwts jwts) { + claims := map[string]jwt.Claims{ + "normal": { + Issuer: "higress-test", + Subject: "higress-test", + Audience: []string{"foo", "bar"}, + Expiry: jwt.NewNumericDate(time.Date(2034, 1, 1, 0, 0, 0, 0, time.UTC)), + NotBefore: jwt.NewNumericDate(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + "expried": { + Issuer: "higress-test", + Subject: "higress-test", + Audience: []string{"foo", "bar"}, + Expiry: jwt.NewNumericDate(time.Date(2024, 1, 1, 0, 0, 0, 1, time.UTC)), + NotBefore: jwt.NewNumericDate(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + + sigrsa, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.RS256, + Key: keySets["rsa"].PrivateKey, + }, (&jose.SignerOptions{}).WithType("JWT").WithHeader(jose.HeaderKey("kid"), "rsa")) + if err != nil { + panic(err) + } + + sigp256, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.ES256, + Key: keySets["p256"].PrivateKey, + }, (&jose.SignerOptions{}).WithType("JWT").WithHeader(jose.HeaderKey("kid"), "p256")) + if err != nil { + panic(err) + } + + sigs := map[string]jose.Signer{ + "RS256": sigrsa, + "ES256": sigp256, + } + + for k1, v1 := range sigs { + for k2, v2 := range claims { + raw, _ := jwt.Signed(v1).Claims(v2).CompactSerialize() + jwts.JWTs = append(jwts.JWTs, struct { + Algorithm string "json:\"alg\"" + Token string "json:\"token\"" + Type string "json:\"type\"" + }{ + Algorithm: k1, + Token: raw, + Type: k2, + }) + } + } + return +} + +func TestMain(m *testing.M) { + keySets := genPrivateKey() + keys := genJWKs(keySets) + jwts := genJWTs(keySets) + + jwks, err := json.Marshal(keys) + if err != nil { + panic(err) + } + f, _ := os.Create("keys.json") + if err != nil { + panic(err) + } + defer f.Close() + f.WriteString(string(jwks)) + + jwtsm, err := json.Marshal(&jwts) + if err != nil { + panic(err) + } + f, _ = os.Create("jwts.json") + if err != nil { + panic(err) + } + defer f.Close() + f.WriteString(string(jwtsm)) + m.Run() +} diff --git a/plugins/wasm-go/extensions/jwt-auth/test/jwts.json b/plugins/wasm-go/extensions/jwt-auth/test/jwts.json new file mode 100644 index 000000000..6e1e22d23 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/test/jwts.json @@ -0,0 +1,24 @@ +{ + "jwts": [ + { + "alg": "RS256", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MjAxOTY4NjQwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.iO0wPY91b_VNGUMZ1n-Ub-SRmEkDQMFLSi77z49tEzll3UZXwmBraP5udM_OPUAdk9ZO3dbb_fOgdcN9V1H9p5kiTr-l-pZTFTJHrPJj8wC519sYRcCk3wrZ9aXR5tNMwOsMdQb7waTBatDQLmHPWzAoTNBc8mwXkRcv1dmJLvsJgxyCl1I9CMOMPq0fYj1NBvaUDIdVSL1o7GGiriD8-0UIOmS72-I3mbaoCIyVb0h3wx7gnIW3zr0yYWaYoiIgmHLag-eEGxHp4-BjtCqcokU4QVMS91qpH7Mkl1iv2WHEkuDQRJ-nLzYGwXb7Dncx9K5tNWHJuZ-DihIU2oT0aA", + "type": "normal" + }, + { + "alg": "RS256", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MTcwNDA2NzIwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.jqzlhBPk9mmvtTT5aCYf-_5uXXSEU5bQ32fx78XeboCnjR9K1CsI4KYUIkXEX3bk66XJQUeSes7lz3gA4Yzkd-v9oADHTgpKnIxzv_5mD0_afIwEFjcalqVbSvCmro4PessQZDnmU7AIzoo3RPSqbmq8xbPVYUH9I-OO8aUu2ATd1HozgxJH1XnRU8k9KMkVW8XhvJXLKZJmnqe3Tu6pCU_tawFlBfBC4fAhMf0yX2CGE0ABAHubcdiI6JXObQmQQ9Or2a-g2a8g_Bw697PoPOsAn0YpTrHst9GcyTpkbNTAq9X8fc5EM7hiDM1FGeMYcaQTdMnOh4HBhP0p4YEhvA", + "type": "expried" + }, + { + "alg": "ES256", + "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InAyNTYiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MTcwNDA2NzIwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.9AnXd2rZ6FirHZQAoabyL4xZNz0jr-3LmcV4-pFV3JrdtUT4386Mw5Qan125fUB-rZf_ZBlv0Bft2tWY149fyg", + "type": "expried" + }, + { + "alg": "ES256", + "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InAyNTYiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MjAxOTY4NjQwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.hm71YWfjALshUAgyOu-r9W2WBG_zfqIZZacAbc7oIH1r7dbB0sGQn3wKMWMmOzmxX0UyaVZ0KMk-HFTA1hDnBQ", + "type": "normal" + } + ] +} \ No newline at end of file diff --git a/plugins/wasm-go/extensions/jwt-auth/test/keys.json b/plugins/wasm-go/extensions/jwt-auth/test/keys.json new file mode 100644 index 000000000..7a43c10b6 --- /dev/null +++ b/plugins/wasm-go/extensions/jwt-auth/test/keys.json @@ -0,0 +1,17 @@ +{ + "keys": [ + { + "kty": "RSA", + "kid": "rsa", + "n": "pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q", + "e": "AQAB" + }, + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + } + ] +} \ No newline at end of file diff --git a/test/e2e/conformance/tests/go-wasm-jwt-auth-allow.yaml b/test/e2e/conformance/tests/go-wasm-jwt-auth-allow.yaml new file mode 100644 index 000000000..6e29bd066 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-jwt-auth-allow.yaml @@ -0,0 +1,142 @@ +# Copyright (c) 2024 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + name: wasmplugin-jwt-auth + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: jwt-auth + namespace: higress-system +spec: + defaultConfig: + consumers: + - name: consumer1 + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + }, + { + "kty": "RSA", + "kid": "rsa", + "n": "pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q", + "e": "AQAB" + } + ] + } + - name: consumer_hedaer + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + }, + { + "kty": "RSA", + "kid": "rsa", + "n": "pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q", + "e": "AQAB" + } + ] + } + from_headers: + - name: jwt + value_prefix: "Bearer " + - name: consumer_params + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + }, + { + "kty": "RSA", + "kid": "rsa", + "n": "pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q", + "e": "AQAB" + } + ] + } + from_params: + - jwt_token + - name: consumer_cookies + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + }, + { + "kty": "RSA", + "kid": "rsa", + "n": "pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q", + "e": "AQAB" + } + ] + } + from_cookies: + - jwt_token + global_auth: false + defaultConfigDisable: false + matchRules: + - config: + allow: + - consumer1 + - consumer_hedaer + - consumer_params + - consumer_cookies + configDisable: false + ingress: + - higress-conformance-infra/wasmplugin-jwt-auth + url: file:///opt/plugins/wasm-go/extensions/jwt-auth/plugin.wasm diff --git a/test/e2e/conformance/tests/go-wasm-jwt-auth-deny.yaml b/test/e2e/conformance/tests/go-wasm-jwt-auth-deny.yaml new file mode 100644 index 000000000..9437e4ef1 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-jwt-auth-deny.yaml @@ -0,0 +1,131 @@ +# Copyright (c) 2024 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + name: wasmplugin-jwt-auth + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: jwt-auth + namespace: higress-system +spec: + defaultConfig: + consumers: + - name: consumerEC + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + } + ] + } + - name: consumerRSA + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "RSA", + "kid": "rsa", + "n": "pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q", + "e": "AQAB" + } + ] + } + - name: consumerEC_hedaer + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + } + ] + } + from_headers: + - name: jwt + value_prefix: "Bearer " + - name: consumerEC_params + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + } + ] + } + from_params: + - jwt_token + - name: consumerEC_cookies + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + } + ] + } + from_cookies: + - jwt_token + global_auth: false + defaultConfigDisable: false + matchRules: + - config: + allow: + - consumerEC + - consumerEC_hedaer + - consumerEC_params + - consumerEC_cookies + configDisable: false + ingress: + - higress-conformance-infra/wasmplugin-jwt-auth + url: file:///opt/plugins/wasm-go/extensions/jwt-auth/plugin.wasm diff --git a/test/e2e/conformance/tests/go-wasm-jwt-auth-single-consumer.yaml b/test/e2e/conformance/tests/go-wasm-jwt-auth-single-consumer.yaml new file mode 100644 index 000000000..e8136a29b --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-jwt-auth-single-consumer.yaml @@ -0,0 +1,128 @@ +# Copyright (c) 2024 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + name: wasmplugin-jwt-auth + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: jwt-auth + namespace: higress-system +spec: + defaultConfig: + consumers: + - name: consumerEC + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + } + ] + } + - name: consumerRSA + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "RSA", + "kid": "rsa", + "n": "pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q", + "e": "AQAB" + } + ] + } + - name: consumerEC_hedaer + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + } + ] + } + from_headers: + - name: jwt + value_prefix: "Bearer " + - name: consumerEC_params + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + } + ] + } + from_params: + - jwt_token + - name: consumerEC_cookies + issuer: higress-test + jwks: |- + { + "keys": [ + { + "kty": "EC", + "kid": "p256", + "crv": "P-256", + "x": "GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU", + "y": "5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ" + } + ] + } + from_cookies: + - jwt_token + global_auth: false + defaultConfigDisable: false + matchRules: + - config: + allow: + - consumerEC + configDisable: false + ingress: + - higress-conformance-infra/wasmplugin-jwt-auth + url: file:///opt/plugins/wasm-go/extensions/jwt-auth/plugin.wasm diff --git a/test/e2e/conformance/tests/go-wasm-jwt-auth.go b/test/e2e/conformance/tests/go-wasm-jwt-auth.go new file mode 100644 index 000000000..ab63d3209 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-jwt-auth.go @@ -0,0 +1,944 @@ +// Copyright (c) 2024 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/alibaba/higress/test/e2e/conformance/utils/http" + "github.com/alibaba/higress/test/e2e/conformance/utils/suite" +) + +const ( + ES256Allow string = "eyJhbGciOiJFUzI1NiIsImtpZCI6InAyNTYiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MjAxOTY4NjQwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.hm71YWfjALshUAgyOu-r9W2WBG_zfqIZZacAbc7oIH1r7dbB0sGQn3wKMWMmOzmxX0UyaVZ0KMk-HFTA1hDnBQ" + ES256Expried string = "eyJhbGciOiJFUzI1NiIsImtpZCI6InAyNTYiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MTcwNDA2NzIwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.9AnXd2rZ6FirHZQAoabyL4xZNz0jr-3LmcV4-pFV3JrdtUT4386Mw5Qan125fUB-rZf_ZBlv0Bft2tWY149fyg" + RS256Allow string = "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MjAxOTY4NjQwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.iO0wPY91b_VNGUMZ1n-Ub-SRmEkDQMFLSi77z49tEzll3UZXwmBraP5udM_OPUAdk9ZO3dbb_fOgdcN9V1H9p5kiTr-l-pZTFTJHrPJj8wC519sYRcCk3wrZ9aXR5tNMwOsMdQb7waTBatDQLmHPWzAoTNBc8mwXkRcv1dmJLvsJgxyCl1I9CMOMPq0fYj1NBvaUDIdVSL1o7GGiriD8-0UIOmS72-I3mbaoCIyVb0h3wx7gnIW3zr0yYWaYoiIgmHLag-eEGxHp4-BjtCqcokU4QVMS91qpH7Mkl1iv2WHEkuDQRJ-nLzYGwXb7Dncx9K5tNWHJuZ-DihIU2oT0aA" + RS256Expried string = "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MTcwNDA2NzIwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.jqzlhBPk9mmvtTT5aCYf-_5uXXSEU5bQ32fx78XeboCnjR9K1CsI4KYUIkXEX3bk66XJQUeSes7lz3gA4Yzkd-v9oADHTgpKnIxzv_5mD0_afIwEFjcalqVbSvCmro4PessQZDnmU7AIzoo3RPSqbmq8xbPVYUH9I-OO8aUu2ATd1HozgxJH1XnRU8k9KMkVW8XhvJXLKZJmnqe3Tu6pCU_tawFlBfBC4fAhMf0yX2CGE0ABAHubcdiI6JXObQmQQ9Or2a-g2a8g_Bw697PoPOsAn0YpTrHst9GcyTpkbNTAq9X8fc5EM7hiDM1FGeMYcaQTdMnOh4HBhP0p4YEhvA" +) + +func init() { + Register(WasmPluginsJWTAuthAllow) + Register(WasmPluginsJWTAuthExpried) + Register(WasmPluginsJWTAuthDeny) + Register(WasmPluginsJWTAuthSingleConsumer) +} + +var WasmPluginsJWTAuthAllow = suite.ConformanceTest{ + ShortName: "WasmPluginsJWTAuth", + Description: "The Ingress in the higress-conformance-infra namespace test the jwt-auth WASM plugin.", + Manifests: []string{"tests/go-wasm-jwt-auth-allow.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "1. Default header with ES256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + ES256Allow}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "2. Default header with RS256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + RS256Allow}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "3. Default params with ES256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?access_token=" + ES256Allow, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "4. Default params with RS256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?access_token=" + RS256Allow, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "5. Custom header with ES256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"jwt": "Bearer " + ES256Allow}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "6. Custom header with RS256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"jwt": "Bearer " + RS256Allow}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "7. Custom params with ES256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?jwt_token=" + ES256Allow, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "8. Custom params with RS256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?jwt_token=" + RS256Allow, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "9. Custom cookies with ES256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + Headers: map[string]string{"Cookie": "jwt_token=" + ES256Allow}, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "10. Custom cookies with RS256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + Headers: map[string]string{"Cookie": "jwt_token=" + RS256Allow}, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + } + t.Run("WasmPlugins jwt-auth", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} + +var WasmPluginsJWTAuthExpried = suite.ConformanceTest{ + ShortName: "WasmPluginsJWTAuthExpried", + Description: "The Ingress in the higress-conformance-infra namespace test the jwt-auth WASM plugin.", + Manifests: []string{"tests/go-wasm-jwt-auth-deny.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "1. Default header with expried ES256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + ES256Expried}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "2. Default header with expried RS256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + RS256Expried}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "3. Default params with expried ES256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?access_token=" + ES256Expried, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "4. Default params with expried RS256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?access_token=" + RS256Expried, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "5. Custom header with expried ES256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"jwt": "Bearer " + ES256Expried}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "6. Custom header with expried RS256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"jwt": "Bearer " + RS256Expried}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "7. Custom params with expried ES256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?jwt_token=" + ES256Expried, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "8. Custom params with expried RS256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?jwt_token=" + RS256Expried, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "9. Custom cookies with expried ES256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + Headers: map[string]string{"Cookie": "jwt_token=" + ES256Expried}, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "10. Custom cookies with expried RS256", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + Headers: map[string]string{"Cookie": "jwt_token=" + RS256Expried}, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + } + t.Run("WasmPlugins jwt-auth", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} + +var WasmPluginsJWTAuthDeny = suite.ConformanceTest{ + ShortName: "WasmPluginsJWTAuthDeny", + Description: "The Ingress in the higress-conformance-infra namespace test the jwt-auth WASM plugin.", + Manifests: []string{"tests/go-wasm-jwt-auth-deny.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "1. Default header with RS256 but unauthorized consumer", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + RS256Allow}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "2. No token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "3. Default header with no token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + ""}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "4. Default params with no token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?access_token=" + "", + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "5. Custom header with no token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"jwt": "Bearer " + ""}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "6. Custom params with no token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?jwt_token=" + "", + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "7. Custom cookies with no token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + Headers: map[string]string{"Cookie": "jwt_token=" + ""}, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "8. Default header with fake token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + "faketoken"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "9. Default params with fake token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?access_token=" + "faketoken", + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "10. Custom header with fake token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"jwt": "Bearer " + "faketoken"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "11. Custom params with fake token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?jwt_token=" + "faketoken", + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "12. Custom cookies with fake token", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + Headers: map[string]string{"Cookie": "jwt_token=" + "faketoken"}, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + } + t.Run("WasmPlugins jwt-auth", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} + +var WasmPluginsJWTAuthSingleConsumer = suite.ConformanceTest{ + ShortName: "WasmPluginsJWTAuthSingleConsumer", + Description: "The Ingress in the higress-conformance-infra namespace test the jwt-auth WASM plugin.", + Manifests: []string{"tests/go-wasm-jwt-auth-single-consumer.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "1. Default hedaer with ES256 by single consumer_EC", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + ES256Allow}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "2. Default hedaer with expried ES256 by single consumer_EC", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + ES256Expried}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 401, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "3. Default hedaer with fake token by single consumer_EC", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + "faketoken"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 401, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "4. No token by single consumer_EC", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 401, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "5. Default header with RS256 by single consumer_EC", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info", + UnfollowRedirect: true, + Headers: map[string]string{"Authorization": "Bearer " + RS256Allow}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "6. Default params with ES256 by single consumer_EC", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?access_token=" + ES256Allow, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "7. Default params with expried ES256 by single consumer_EC", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?access_token=" + ES256Expried, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 401, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "8. Default params with fake token by single consumer_EC", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?access_token=" + "faketoken", + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 401, + }, + ExpectedResponseNoRequest: true, + }, + }, + + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + TestCaseName: "9. Default params with RS256 by single consumer_EC", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/info?access_token=" + RS256Allow, + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + } + t.Run("WasmPlugins jwt-auth", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} diff --git a/test/e2e/conformance/tests/go-wasm-simple-jwt-auth.go b/test/e2e/conformance/tests/go-wasm-simple-jwt-auth.go index bf8d91434..ac42dde14 100644 --- a/test/e2e/conformance/tests/go-wasm-simple-jwt-auth.go +++ b/test/e2e/conformance/tests/go-wasm-simple-jwt-auth.go @@ -26,7 +26,7 @@ func init() { } var WasmPluginsJwtAuth = suite.ConformanceTest{ - ShortName: "WasmPluginsJwtAuth", + ShortName: "WasmPluginsSimpleJwtAuth", Description: "The Ingress in the higress-conformance-infra namespace test the simple-jwt-auth wasmplugins.", Manifests: []string{"tests/go-wasm-simple-jwt-auth.yaml"}, Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},