diff --git a/plugins/wasm-go/extensions/oauth/README.md b/plugins/wasm-go/extensions/oauth/README.md new file mode 100644 index 0000000000..01e14ec88d --- /dev/null +++ b/plugins/wasm-go/extensions/oauth/README.md @@ -0,0 +1,91 @@ +# 功能说明 +`OAuth2`插件实现了基于JWT(JSON Web Tokens)进行OAuth2 Access Token签发的能力, 遵循[RFC9068](https://datatracker.ietf.org/doc/html/rfc9068)规范 + +# 插件配置说明 + +## 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ----------- | --------------- | ------------------------------------------- | ------ | ----------------------------------------------------------- | +| `consumers` | array of object | 必填 | - | 配置服务的调用者,用于对请求进行认证 | +| `issuer` | string | 选填 | Higress-Gateway | 用于填充JWT中的issuer | +| `auth_path` | string | 选填 | /oauth2/token | 指定路径后缀用于签发Token,路由级配置时,要确保首先能匹配对应的路由 | +| `global_credentials` | bool | 选填 | ture | 是否开启全局凭证,即允许路由A下的auth_path签发的Token可以用于访问路由B | +| `auth_header_name` | string | 选填 | Authorization | 用于指定从哪个请求头获取JWT | +| `token_ttl` | number | 选填 | 7200 | token从签发后多久内有效,单位为秒 | +| `clock_skew_seconds` | number | 选填 | 60 | 校验JWT的exp和iat字段时允许的时钟偏移量,单位为秒 | +| `keep_token` | bool | 选填 | ture | 转发给后端时是否保留JWT | + +`consumers`中每一项的配置字段说明如下: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| ----------------------- | ----------------- | -------- | ------------------------------------------------- | ------------------------ | +| `name` | string | 必填 | - | 配置该consumer的名称 | +| `client_id` | string | 必填 | - | OAuth2 client id | +| `client_secret` | string | 必填 | - | OAuth2 client secret | + + +**注意:** +- 对于开启该配置的路由,如果路径后缀和`auth_path`匹配,则该路由到原目标服务,而是用于生成Token +- 如果关闭`global_credentials`,请确保启用此插件的路由不是精确匹配路由,此时若存在另一条前缀匹配路由,则可能导致预期外行为 +- 对于通过认证鉴权的请求,请求的header会被添加一个`X-Mse-Consumer`字段,用以标识调用者的名称。 + +## 配置示例 + +```yaml +consumers: + - name: consumer1 + client_id: 9515b564-0b1d-11ee-9c4c-00163e1250b5 + client_secret: 9e55de56-0b1d-11ee-b8ec-00163e1250b5 + - name: consumer2 + client_id: 8521b564-0b1d-11ee-9c4c-00163e1250b5 + client_secret: 8520b564-0b1d-11ee-9c4c-00163e1250b5 +issuer: Higress-Gateway +auth_path: /oauth2/token +global_credentials: true +auth_header_name: Authorization +token_ttl: 7200 +clock_skew_seconds: 3153600000 +keep_token: true +``` + +#### 使用 Client Credential 授权模式 + +**获取 AccessToken** + +```bash + +# 通过 GET 方法获取 + +curl 'http://test.com/oauth2/token?grant_type=client_credentials&client_id=12345678-xxxx-xxxx-xxxx-xxxxxxxxxxxx&client_secret=abcdefgh-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + +# 通过 POST 方法获取 (需要先匹配到有真实目标服务的路由) + +curl 'http://test.com/oauth2/token' -H 'content-type: application/x-www-form-urlencoded' -d 'grant_type=client_credentials&client_id=12345678-xxxx-xxxx-xxxx-xxxxxxxxxxxx&client_secret=abcdefgh-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + +# 获取响应中的 access_token 字段即可: +{ + "token_type": "bearer", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uXC9hdCtqd3QifQ.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiMTIzNDU2NzgteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IiwiZXhwIjoxNjg3OTUxNDYzLCJpYXQiOjE2ODc5NDQyNjMsImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjEwOTU5ZDFiLThkNjEtNGRlYy1iZWE3LTk0ODEwMzc1YjYzYyIsInN1YiI6ImNvbnN1bWVyMSJ9.NkT_rG3DcV9543vBQgneVqoGfIhVeOuUBwLJJ4Wycb0", + "expires_in": 7200 +} + +``` + +**使用 AccessToken 请求** + +```bash + +curl 'http://test.com' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uXC9hdCtqd3QifQ.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiMTIzNDU2NzgteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IiwiZXhwIjoxNjg3OTUxNDYzLCJpYXQiOjE2ODc5NDQyNjMsImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjEwOTU5ZDFiLThkNjEtNGRlYy1iZWE3LTk0ODEwMzc1YjYzYyIsInN1YiI6ImNvbnN1bWVyMSJ9.NkT_rG3DcV9543vBQgneVqoGfIhVeOuUBwLJJ4Wycb0' + +``` +因为 test.com 仅授权了 consumer2,但这个 Access Token 是基于 consumer1 的 `client_id`,`client_secret` 获取的,因此将返回 `403 Access Denied` + + +# 常见错误码说明 + +| HTTP 状态码 | 出错信息 | 原因说明 | +| ----------- | ---------------------- | -------------------------------------------------------------------------------- | +| 401 | Invalid Jwt token | 请求头未提供JWT, 或者JWT格式错误,或过期等原因 | +| 403 | Access Denied | 无权限访问当前路由 | + diff --git a/plugins/wasm-go/extensions/oauth/VERSION b/plugins/wasm-go/extensions/oauth/VERSION new file mode 100644 index 0000000000..afaf360d37 --- /dev/null +++ b/plugins/wasm-go/extensions/oauth/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/oauth/go.mod b/plugins/wasm-go/extensions/oauth/go.mod new file mode 100644 index 0000000000..f2e730c34c --- /dev/null +++ b/plugins/wasm-go/extensions/oauth/go.mod @@ -0,0 +1,24 @@ +module github.com/alibaba/higress/plugins/wasm-go/extensions/oauth + +go 1.19 + +replace github.com/alibaba/higress/plugins/wasm-go => ../.. + +require ( + github.com/alibaba/higress/plugins/wasm-go v1.3.1 + github.com/golang-jwt/jwt/v5 v5.1.0 + github.com/google/uuid v1.4.0 + github.com/pkg/errors v0.9.1 + github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 + github.com/tidwall/gjson v1.17.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect + github.com/magefile/mage v1.14.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/stretchr/testify v1.8.3 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) diff --git a/plugins/wasm-go/extensions/oauth/go.sum b/plugins/wasm-go/extensions/oauth/go.sum new file mode 100644 index 0000000000..683e4c0c44 --- /dev/null +++ b/plugins/wasm-go/extensions/oauth/go.sum @@ -0,0 +1,25 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.1.0 h1:UGKbA/IPjtS6zLcdB7i5TyACMgSbOTiR8qzXgw8HWQU= +github.com/golang-jwt/jwt/v5 v5.1.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.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/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M= +github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/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 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/plugins/wasm-go/extensions/oauth/main.go b/plugins/wasm-go/extensions/oauth/main.go new file mode 100644 index 0000000000..e8fe6ade14 --- /dev/null +++ b/plugins/wasm-go/extensions/oauth/main.go @@ -0,0 +1,642 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "net/url" + "strings" + "time" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + jwt "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" + "github.com/tidwall/gjson" +) + +func main() { + wrapper.SetCtx( + "oauth", + wrapper.ParseOverrideConfigBy(parseGlobalConfig, parseOverrideRuleConfig), + wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), + wrapper.ProcessRequestBodyBy(onHttpRequestBody), + ) +} + +// @Name oauth +// @Category auth +// @Phase AUTHN +// @Priority 320 +// @Title zh-CN OAuth +// @Description zh-CN 本插件实现了JWT(JSON Web Tokens)进行OAuth2 Access Token签发、认证鉴权的能力 +// @Description en-US This plugin implements functions based on JWT(JSON Web Tokens) to issue and authenticate OAuth2 Access tokens +// @IconUrl https://img.alicdn.com/imgextra/i4/O1CN01BPFGlT1pGZ2VDLgaH_!!6000000005333-2-tps-42-42.png +// @Version 1.0.0 + +// @Contact.name Higress Team +// @Contact.url http://higress.io/ +// @Contact.email admin@higress.io + +// @Example +// metadata: +// name: oauth +// namespace: higress-system +// spec: +// defaultConfig: +// consumers: +// - name: consumer1 +// client_id: 9515b564-0b1d-11ee-9c4c-00163e1250b5 +// client_secret: 9e55de56-0b1d-11ee-b8ec-00163e1250b5 +// - name: consumer2 +// client_id: 8521b564-0b1d-11ee-9c4c-00163e1250b5 +// client_secret: 8520b564-0b1d-11ee-9c4c-00163e1250b5 +// - name: consumer3 +// client_id: 4987b564-0b1d-11ee-9c4c-00163e1250b5 +// client_secret: 498766s4-0b1d-11ee-9c4c-00163e1250b5 +// - name: consumer4 +// client_id: 5559qv64-0b1d-11ee-9c4c-00163e1250b5 +// client_secret: 58as2a84-0b1d-11ee-9c4c-00163e1250b5 +// issuer: Higress-Gateway +// auth_path: /oauth2/token +// global_credentials: true +// auth_header_name: Authorization +// token_ttl: 7200 +// clock_skew_seconds: 3153600000 +// keep_token: true +// global_auth: true +// matchRules: +// # 规则一:按路由名称匹配生效 +// - ingress: +// - "higress-conformance-infra/wasmplugin-oauth" +// - "asd" +// config: +// allow: +// - consumer1 + +// # 规则二:按域名匹配生效 +// - domain: +// - "*.example.com" +// - foo.com +// config: +// allow: +// - consumer3 +// @End + +type OAuthConfig struct { + // @Title 调用方列表 + // @Title en-US Consumer List + // @Description 服务调用方列表,用于对请求进行认证。 + // @Description en-US List of service consumers which will be used in request authentication. + // @Scope GLOBAL + consumers map[string]Consumer `yaml:"consumers"` + + // @Title 签发者 + // @Title en-US + // @Description JWT服务签发者,用于填充JWT中的issuer + // @Description en-US Issuer of JWT service. + // @Scope GLOBAL + issuer string `yaml:"issuer"` + + // @Title 签发路径 + // @Title en-US Authentication Path + // @Description 签发token时使用的特定路由后缀,当有路由级配置,需确保路由与该签发路径匹配 + // @Description en-US Specified route suffix for issuing tokens. If route level is configured, ensure the route matches the authPath + // @Scope GLOBAL + authPath string `yaml:"auth_path"` + + // @Title 是否开启全局凭证 + // @Title en-US enable credentials globally or not + // @Description 是否允许路由A下的auth_path签发的Token可以用于访问路由B + // @Description en-US for example. whether to allow the token issued by auth_path in route A to be used to access route B + // @Scope GLOBAL + globalCredentials bool `yaml:"global_credentials"` + + // @Title 签发请求头的名称 + // @Title en-US name of the issuing request header + // @Description 用于指定从哪个请求头获取JWT + // @Description en-US It is used to specify which request header to get the JWT from + // @Scope GLOBAL + authHeaderName string `yaml:"auth_header_name"` + + // @Title token的有效时长,单位为秒 + // @Title en-US Time to live for a token, in seconds + // @Scope GLOBAL + tokenTtl uint64 `yaml:"token_ttl"` + + // @Title 时钟偏移量,单位为秒 + // @Title en-US Clock offset, in seconds + // @Description 校验JWT的exp和iat字段时允许的时钟偏移量 + // @Description en-US The clock offset allowed when verifying the exp and iat fields of JWT + // @Scope GLOBAL + clockSkewSeconds uint64 `yaml:"clock_skew_seconds"` + + // @Description 转发给后端时是否保留JWT + // @Description en-US Whether to retain JWT when forwarding to back-end + // @Scope GLOBAL + keepToken bool `yaml:"keep_token"` + + // @Title 授权访问的调用方列表 + // @Title en-US Allowed Consumers + // @Description 对于匹配上述条件的请求,允许访问的调用方列表,列表包含调用者名称。依附特定路由/域名规则而存在 + // @Description en-US Consumers to be allowed for matched requests. Consisting of client_name. It exists based on specific routing/domain rules. + // @Scope RULELOCAL + allow []string `yaml:"allow"` + + // @Title 是否开启全局认证 + // @Title en-US Enable Global Auth + // @Description 若配置为true,则全局生效认证机制; 若配置为false,则只对做了配置的域名和路由生效认证机制; 若不配置则仅当没有域名和路由配置时全局生效(兼容机制) + // @Description en-US en-US If set to false, only consumer info will be accepted from the global config. Auth feature shall only be enabled if the corresponding domain or route is configured. + // @Scope GLOBAL + globalAuth *bool `yaml:"global_auth"` +} + +type Consumer struct { + name string `yaml:"name"` + clientId string `yaml:"client_id"` + + // @Title 调用方密钥 + // @Title en-US Secret key + // @Description 签发JWT时,使用该密钥进行加密生成签名 + // @Description en-US When a JWT is issued, the secret is used to encrypt and generate a signature. + clientSecret string `yaml:"client_secret"` +} + +type Res struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +// 直接转json需要保证struct成员首字母大写 +type TokenResponse struct { + TokenType string `json:"token_type"` + AccessToken string `json:"access_token"` + ExpireTime uint `json:"expires_in"` +} + +var ( + BearerPrefix = "Bearer " + ClientCredentialsGrant = "client_credentials" + checkBodyParams = false + routeName = "" + DefaultAudience = "default" + TypeHeader = "application/at+jwt" + ruleSet = false // oauth认证是否至少在一个 domain 或 route 上生效 +) + +// parseGlobalConfig 读取json中的数据到global中,除Consumer的数据检查外,OAuthConfig中的其他数据在json中不存在时赋默认值 +// +// @param json +// @param global 初始为空 +// @param log +// @return error +func parseGlobalConfig(json gjson.Result, global *OAuthConfig, log wrapper.Log) error { + global.issuer = "Higress-Gateway" + global.authPath = "/oauth2/token" + global.globalCredentials = true + global.authHeaderName = "Authorization" + global.tokenTtl = 7200 + global.clockSkewSeconds = 60 + global.keepToken = true + + nameSet := make(map[string]int) + global.consumers = make(map[string]Consumer) + consumers := json.Get("consumers") + + if !consumers.Exists() { + return errors.New("consumers is required") + } + if len(consumers.Array()) == 0 { + return errors.New("consumers cannot be empty") + } + for _, item := range consumers.Array() { + name := item.Get("name") + if !name.Exists() || name.String() == "" { + return errors.New("consumer name is required") + } + + if nameSet[name.String()] >= 1 { + return errors.Errorf("duplicate name: %s", name.String()) + } + + nameSet[name.String()]++ + + clientId := item.Get("client_id") + if !clientId.Exists() || clientId.String() == "" { + return errors.New("consumer client_id is required") + } + if _, ok := global.consumers[clientId.String()]; ok { + return errors.Errorf("duplicate consumer client_id: %s", clientId.String()) + } + + clientSecret := item.Get("client_secret") + if !clientSecret.Exists() || clientSecret.String() == "" { + return errors.New("consumer client_secret is required") + } + + consumer := Consumer{ + name: name.String(), + clientId: clientId.String(), + clientSecret: clientSecret.String(), + } + global.consumers[clientId.String()] = consumer + } + + issuer := json.Get("issuer") + if issuer.Exists() { + global.issuer = issuer.String() + } + + authPath := json.Get("auth_path") + if authPath.Exists() { + global.authPath = authPath.String() + } + + globalCredentials := json.Get("global_credentials") + if globalCredentials.Exists() { + global.globalCredentials = globalCredentials.Bool() + } + + authHeaderName := json.Get("auth_header_name") + if authHeaderName.Exists() { + global.authHeaderName = authHeaderName.String() + } + + tokenTtl := json.Get("token_ttl") + if tokenTtl.Exists() { + global.tokenTtl = tokenTtl.Uint() + } + + keepToken := json.Get("keep_token") + if keepToken.Exists() { + global.keepToken = keepToken.Bool() + + } + + clockSkewSeconds := json.Get("clock_skew_seconds") + if clockSkewSeconds.Exists() { + global.clockSkewSeconds = clockSkewSeconds.Uint() + } + + globalAuth := json.Get("global_auth") + if globalAuth.Exists() { + ga := globalAuth.Bool() + global.globalAuth = &ga + } + return nil +} + +func parseOverrideRuleConfig(json gjson.Result, global OAuthConfig, config *OAuthConfig, log wrapper.Log) error { + // override config via global + *config = global + + allowJson := json.Get("allow") + + allow := make([]string, 0) + + if !allowJson.Exists() || len(allowJson.Array()) == 0 { + log.Debug("allow is empty originally or not set") + } else { + for _, item := range allowJson.Array() { + allow = append(allow, item.String()) + } + ruleSet = true + } + config.allow = allow + return nil +} + +func onHttpRequestHeaders(ctx wrapper.HttpContext, config OAuthConfig, log wrapper.Log) types.Action { + var res Res + token := "" + errMsg := "" + + routeName, _ := proxywasm.GetProperty([]string{"route_name"}) + path, _ := proxywasm.GetHttpRequestHeader(":path") + + var uriEnd int + paramsPos := strings.Index(path, "?") + method, _ := proxywasm.GetHttpRequestHeader(":method") + if paramsPos == -1 { + uriEnd = len(path) + } else { + uriEnd = paramsPos + } + if endsWith(path[:uriEnd], config.authPath) { + if method == "GET" { + generateToken(config, string(routeName), path, &token, &errMsg) + goto done + } + + if method == "POST" { + contentType, _ := proxywasm.GetHttpRequestHeader("content-type") + if find := strings.Contains(strings.ToLower(contentType), "application/x-www-form-urlencoded"); !find { + errMsg = "Invalid or unsupported content-type" + goto done + } + + checkBodyParams = true + } + + done: + if errMsg != "" { + res.Code = 400 + res.Msg = errMsg + data, _ := json.Marshal(res) + // TODO: SendHttpResponse和cpp版本的sendLocalResponse参数列表略有不同,暂不确定如何转成go的形式 + proxywasm.SendHttpResponse(400, nil, data, -1) + return types.ActionPause + } + if token != "" { + tR := TokenResponse{"bearer", token, uint(config.tokenTtl)} + tokenResponse, _ := json.Marshal(tR) + proxywasm.SendHttpResponse(200, nil, tokenResponse, -1) + + } + return types.ActionContinue + } + if valid := parseTokenValid(config, string(routeName), &errMsg, log); valid { + return types.ActionContinue + } else { + return types.ActionPause + } +} + +func onHttpRequestBody(ctx wrapper.HttpContext, config OAuthConfig, body []byte, log wrapper.Log) types.Action { + + var res Res + token := "" + errMsg := "" + if !checkBodyParams { + return types.ActionContinue + } + if len(body) == 0 { + errMsg = "Authorize parameters are missing" + return types.ActionContinue + } + + // 目前只支持content-type=application/x-www-form-urlencoded,因此直接将body当url处理来得到参数 + if tokenSuccess := generateToken(config, routeName, "?"+string(body), &token, &errMsg); tokenSuccess { + tR := TokenResponse{"bearer", token, uint(config.tokenTtl)} + tokenResponse, _ := json.Marshal(tR) + proxywasm.SendHttpResponse(200, nil, tokenResponse, -1) + return types.ActionContinue + } + + res.Code = 400 + res.Msg = errMsg + data, _ := json.Marshal(res) + proxywasm.SendHttpResponse(400, nil, data, -1) + return types.ActionContinue +} + +func generateToken(config OAuthConfig, routeName string, raw_params string, token *string, errMsg *string) bool { + var consumer Consumer + u, err := url.Parse(raw_params) + if err != nil { + *errMsg = err.Error() + return false + } + params := u.Query() + + consumer.clientId = params.Get("client_id") + consumer.clientSecret = params.Get("client_secret") + grantType := params.Get("grant_type") + + if len(grantType) == 0 { + *errMsg = "grant_type is missing" + return false + } + if grantType != ClientCredentialsGrant { + *errMsg = "grant type " + grantType + " is not supported." + return false + } + + if len(consumer.clientId) == 0 { + *errMsg = "client_id is missing" + return false + } + + consumerInConfig, exist := config.consumers[consumer.clientId] + if !exist { + *errMsg = "invalid client_id or client_secret" + return false + } + + if len(consumerInConfig.clientSecret) == 0 { + *errMsg = "client_secret is missing" + return false + } + + if consumer.clientSecret != consumerInConfig.clientSecret { + *errMsg = "invalid client_id or client_secret" + return false + } + + var audience string + if config.globalCredentials { + audience = DefaultAudience + } else { + audience = routeName + } + nowTime := time.Now() + claims := jwt.MapClaims{ + "client_id": consumer.clientId, + "sub": consumerInConfig.name, + "exp": jwt.NewNumericDate(nowTime.Add(time.Duration(config.tokenTtl) * time.Second)), + "jti": uuid.New().String(), + "iat": jwt.NewNumericDate(time.Now()), + "iss": config.issuer, + "aud": audience, + } + tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenObj.Header["typ"] = TypeHeader + + *token, err = tokenObj.SignedString([]byte(consumer.clientSecret)) + if err != nil { + *errMsg = "jwt sign failed: " + err.Error() + } + + return true +} + +// 基础认证:token解码->判断consumer合法性->token解密验证,失败返回401 +// 签发路由匹配:当globalCredentials为false时,需保证token签发路由与当前路由匹配,失败返回403,做签发路由匹配之前必须做基础认证token解码 +// 路由规则匹配:在 allow 列表中查找,如果找到则认证通过,否则认证失败,返回403 + +// - global_auth == true 开启全局生效: +// - 若当前 domain/route 未配置 allow 列表,即未配置该插件,则基础认证->签发路由匹配 (1*) +// - 若当前 domain/route 配置了该插件:则基础认证->签发路由匹配->路由规则匹配 +// +// - global_auth == false 非全局生效:(2*) +// - 若当前 domain/route 未配置该插件:则直接放行 +// - 若当前 domain/route 配置了该插件:则基础认证->签发路由匹配->路由规则匹配 +// +// - global_auth 未设置: +// - 若没有一个 domain/route 配置该插件,默认全局生效:则基础认证->签发路由匹配 (1*) +// - 若有至少一个 domain/route 配置该插件,默认非全局生效:遵循 (2*) + +// TODO:函数命名不够准确,不仅包含了检验token的逻辑,还包含了不验token直接放行的逻辑 +func parseTokenValid(config OAuthConfig, routeName string, errMsg *string, log wrapper.Log) bool { + var ( + noAllow = len(config.allow) == 0 // 未配置 allow 列表,表示插件在该 domain/route 未生效 + globalAuthNoSet = config.globalAuth == nil + // globalAuthSetTrue = !globalAuthNoSet && *config.globalAuth + globalAuthSetFalse = !globalAuthNoSet && !*config.globalAuth + verified = false + ) + + // 不做基础认证,签发路由匹配、和路由规则匹配而直接放行: + // - global_auth == false 且 当前 domain/route 未配置该插件 + // - global_auth 未设置 且 有至少一个 domain/route 配置该插件(视为非全局生效,只对做了配置的域名和路由生效认证机制),且当前domain/route未配置该插件 + if noAllow && (globalAuthSetFalse || (globalAuthNoSet && ruleSet)) { + log.Debug("authorization is not required") + return true + } + + { + // 基础认证 + auth, err := proxywasm.GetHttpRequestHeader(config.authHeaderName) + if err != nil { + log.Debug("auth header is empty") + goto failed + } + tokenIndexStart := strings.Index(auth, BearerPrefix) + if tokenIndexStart < 0 { + log.Debug("auth header is not a bearer token") + goto failed + } + tokenIndexStart += len(BearerPrefix) + tokenString := auth[tokenIndexStart:] + + // 按照jwt三段式进行解码 + payloadIndex, signatureIndex := strings.Index(tokenString, ".")+1, strings.LastIndex(tokenString, ".")+1 + if len(tokenString) == 0 || payloadIndex <= 0 || signatureIndex == payloadIndex { + log.Debug("token not in jwt's format") + goto failed + } + + rawPayload, err := base64.RawStdEncoding.DecodeString(tokenString[payloadIndex : signatureIndex-1]) + if err != nil { + log.Debugf("token decode fail: %s", err.Error()) + goto failed + } + var decodedPayload map[string]interface{} + + err = json.Unmarshal(rawPayload, &decodedPayload) + + // 从jwt解码结果中取出client_id + rawClientIdInToken, exist := decodedPayload["client_id"] + if !exist { + log.Debug("client_id not found in token") + goto failed + } + + clientId, ok := rawClientIdInToken.(string) + if !ok { + log.Debugf("invalid client_id, token: %s", tokenString) + goto failed + } + + consumer, exist := config.consumers[clientId] + if !exist { + log.Debugf("client_id not found: %s", clientId) + goto failed + } + + // 判断该client合法性后,再用其对应的client_secret将jwt解密 + _, err = jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + + return []byte(consumer.clientSecret), nil + }, jwt.WithLeeway(time.Duration(config.clockSkewSeconds)*(time.Second))) + + if err != nil { + log.Debugf("token verify failed, token: %s, reason: %s", tokenString, err.Error()) + goto failed + } + + // 以上基础认证不通过时,返回401,以上条件都通过时,进行签发路由匹配和路由规则匹配,若不符合规则返回403 + verified = true + + // 签发路由匹配 + if !config.globalCredentials { + rawAudienceInToken, exist := decodedPayload["aud"] + if !exist { + log.Debug("audience not found in token") + goto failed + } + + audience, ok := rawAudienceInToken.(string) + if !ok { + log.Debugf("invalid audience, token: %s", tokenString) + goto failed + } + + if audience != routeName { + log.Debugf("audience: %s not match this route: %s", audience, routeName) + goto failed + } + } + + // 满足某些条件时需进行路由规则匹配 + // 当前domain/route已配置该插件时,不论global_auth的值,都进行路由规则匹配 + if !noAllow { + if !contains(config.allow, consumer.name) { + routeName, _ := proxywasm.GetProperty([]string{"route_name"}) + log.Debugf("consumer: %s is not in route's: %s allow_set", consumer.name, routeName) + goto failed + } + } + + // 其余情况不做路由规则匹配,验证过程结束 + // - global_auth == true 且 当前 domain/route 未配置该插件 + // - global_auth 未设置 且 没有任何一个 domain/route 配置该插件 + + if !config.keepToken { + err = proxywasm.RemoveHttpRequestHeader(config.authHeaderName) + if err != nil { + log.Debug("failed to remove jwt in request header") + } + } + err = proxywasm.AddHttpRequestHeader("X-Mse-Consumer", consumer.name) + if err != nil { + log.Debug("failed to set request header") + } + log.Debugf("consumer %q authenticated", consumer.name) + return true + } +failed: + var res Res + if !verified { + res.Code = 401 + res.Msg = "Invalid Jwt token: " + *errMsg + data, _ := json.Marshal(res) + proxywasm.SendHttpResponse(401, nil, data, -1) + } else { + res.Code = 403 + res.Msg = "Access Denied: " + *errMsg + data, _ := json.Marshal(res) + proxywasm.SendHttpResponse(403, nil, data, -1) + } + return false +} + +func contains(arr []string, item string) bool { + for _, i := range arr { + if i == item { + return true + } + } + return false +} + +func endsWith(str, suffix string) bool { + if len(str) < len(suffix) { + return false + } + return str[len(str)-len(suffix):] == suffix +} diff --git a/test/e2e/conformance/tests/go-wasm-oauth-basic.go b/test/e2e/conformance/tests/go-wasm-oauth-basic.go new file mode 100644 index 0000000000..b52864628f --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-oauth-basic.go @@ -0,0 +1,416 @@ +// Copyright (c) 2022 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" +) + +func init() { + Register(WasmPluginsOAuthBasic) + +} + +var WasmPluginsOAuthBasic = suite.ConformanceTest{ + ShortName: "WasmPluginsOAuthBasic", + Description: "The Ingress in the higress-conformance-infra namespace test the oauth WASM plugin.", + Manifests: []string{"tests/go-wasm-oauth-basic.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 1: GET, path lacks ", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 2: GET, path lacks grant_type", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token?", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 3: GET, path lacks client_id", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token?grant_type=client_credentials", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 4: GET, path lacks client_secret", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token?grant_type=client_credentials&client_id=9515b564-0b1d-11ee-9c4c-00163e1250b5", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 5: GET, consumer_id not found in configs", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token?grant_type=client_credentials&client_id=c05&client_secret=xxxxx", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 6: GET, Failed token service with consumerid and secret not matched", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token?grant_type=client_credentials&client_id=9515b564-0b1d-11ee-9c4c-00163e1250b5&client_secret=c01xxxx", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 7: success by GET method (consumer1)", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token?grant_type=client_credentials&client_id=9515b564-0b1d-11ee-9c4c-00163e1250b5&client_secret=9e55de56-0b1d-11ee-b8ec-00163e1250b5", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + // TODO: CompareRequest不支持200且请求未到达backend的情况 + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 8: success by GET method (consumer2)", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token?grant_type=client_credentials&client_id=8521b564-0b1d-11ee-9c4c-00163e1250b5&client_secret=8520b564-0b1d-11ee-9c4c-00163e1250b5", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 9:POST, body lacks grant_type", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token", + Method: "POST", + ContentType: http.ContentTypeFormUrlencoded, + Body: []byte(`client_id=8521b564-0b1d-11ee-9c4c-00163e1250b5&client_secret=8520b564-0b1d-11ee-9c4c-00163e1250b5`), + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 10: POST, body lacks client_id", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token", + Method: "POST", + ContentType: http.ContentTypeFormUrlencoded, + Body: []byte(`grant_type=client_credentials&client_secret=8520b564-0b1d-11ee-9c4c-00163e1250b5`), + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 11: POST, body lacks client_secret", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token", + Method: "POST", + ContentType: http.ContentTypeFormUrlencoded, + Body: []byte(`grant_type=client_credentials&client_id=8521b564-0b1d-11ee-9c4c-00163e1250b5`), + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 12: POST, consumer_id not found in configs", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token", + Method: "POST", + ContentType: http.ContentTypeFormUrlencoded, + Body: []byte(`grant_type=client_credentials&client_id=c05&client_secret=xxxxx`), + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 13: POST, Failed token service with consumerid and secret not matched", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token", + Method: "POST", + ContentType: http.ContentTypeFormUrlencoded, + Body: []byte(`grant_type=client_credentials&client_id=9515b564-0b1d-11ee-9c4c-00163e1250b5&client_secret=c01xxxx`), + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 1: generate token, case 14: success by POST method, consumser info in request body (consumer2)", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo/oauth2/token", + Method: "POST", + ContentType: http.ContentTypeFormUrlencoded, + Body: []byte(`grant_type=client_credentials&client_id=8521b564-0b1d-11ee-9c4c-00163e1250b5&client_secret=8520b564-0b1d-11ee-9c4c-00163e1250b5`), + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 2: invalid token, case 1: not a bearer token", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{"Authorization": "alksdjf"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 401, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 2: invalid token, case 2: token not fit jwt's format", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{"Authorization": "Bearer alksdjf"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 401, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 2: invalid token, case 3: invalid token", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiOTUxNTVhNjQtMGIxZC0xcWVlLTljNGMtMDAxcXdlMTI1MGI1IiwiZXhwIjoxNzAxMzYzODc0LCJpYXQiOjE3MDEzNTY2NzQsImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjYyM2UyMmQ5LTc1MTctNGEwOC04ZDc2LTliZjBlNDljYjEyYyIsInN1YiI6IiJ9.IB2-T_v9aHRfOyd_QQcNIMtdjA8q5pHfCeixMi5-b0E"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 401, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 4: AuthZ, case 1: token verify fail, client not in the route's allowset", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + // consumer2 + Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiODUyMWI1NjQtMGIxZC0xMWVlLTljNGMtMDAxNjNlMTI1MGI1IiwiZXhwIjoxNzAxNDIzOTU1LCJpYXQiOjE3MDE0MTY3NTUsImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjU1NDVkZDRhLWU4YjYtNDY2NC04ZDE4LWY3Yjk5YWVmYzQ1YyIsInN1YiI6ImNvbnN1bWVyMiJ9.FhxLbFFW0h3O3S8MH3vjFRj54xSmQIVVEC8IxGNpIcU"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "scene 4: AuthZ, case 2: token verify success, client in the route's allowset", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + // consumer1 + Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiOTUxNWI1NjQtMGIxZC0xMWVlLTljNGMtMDAxNjNlMTI1MGI1IiwiZXhwIjoxNzAxNDE4NzU5LCJpYXQiOjE3MDE0MTE1NTksImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjQ0YTMzYjc4LWNmYWItNGYzYS1iZDQ3LTQ1Y2Y5ZjM0YjVmZSIsInN1YiI6ImNvbnN1bWVyMSJ9.EIDCTVx4Wt6u5fRngFwgRo-qfDSKp6sUg4fKA7MYpuE"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + } + + t.Run("WasmPlugins oauth basic", 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-oauth-basic.yaml b/test/e2e/conformance/tests/go-wasm-oauth-basic.yaml new file mode 100644 index 0000000000..9b2714d7a8 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-oauth-basic.yaml @@ -0,0 +1,82 @@ +# Copyright (c) 2022 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: + nginx.ingress.kubernetes.io/app-root: "/foo" + name: wasmplugin-oauth + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- + +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: oauth + namespace: higress-system +spec: + defaultConfig: + consumers: + - name: consumer1 + client_id: 9515b564-0b1d-11ee-9c4c-00163e1250b5 + client_secret: 9e55de56-0b1d-11ee-b8ec-00163e1250b5 + - name: consumer2 + client_id: 8521b564-0b1d-11ee-9c4c-00163e1250b5 + client_secret: 8520b564-0b1d-11ee-9c4c-00163e1250b5 + - name: consumer3 + client_id: 4987b564-0b1d-11ee-9c4c-00163e1250b5 + client_secret: 498766s4-0b1d-11ee-9c4c-00163e1250b5 + - name: consumer4 + client_id: 5559qv64-0b1d-11ee-9c4c-00163e1250b5 + client_secret: 58as2a84-0b1d-11ee-9c4c-00163e1250b5 + issuer: Higress-Gateway + auth_path: /oauth2/token + global_credentials: true + auth_header_name: Authorization + token_ttl: 7200 + clock_skew_seconds: 3153600000 + keep_token: true + # global_auth is unset + + matchRules: + # 规则一:按路由名称匹配生效 + - ingress: + - "higress-conformance-infra/wasmplugin-oauth" + - "asd" + config: + allow: + - consumer1 + + # 规则二:按域名匹配生效 + - domain: + - "*.example.com" + - foo.com + config: + allow: + - consumer3 + url: file:///opt/plugins/wasm-go/extensions/oauth/plugin.wasm diff --git a/test/e2e/conformance/tests/go-wasm-oauth-empty-consumer.go b/test/e2e/conformance/tests/go-wasm-oauth-empty-consumer.go new file mode 100644 index 0000000000..277978e51a --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-oauth-empty-consumer.go @@ -0,0 +1,64 @@ +// Copyright (c) 2022 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" +) + +func init() { + Register(WasmPluginsOAuthEmptyConsumer) + +} + +var WasmPluginsOAuthEmptyConsumer = suite.ConformanceTest{ + ShortName: "WasmPluginsOAuthEmptyConsumer", + Description: "The Ingress in the higress-conformance-infra namespace test the oauth WASM plugin, with empty consumer config", + Manifests: []string{"tests/go-wasm-oauth-empty-consumer.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TestCaseName: "case 1: empty consumer", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiOTUxNTVhNjQtMGIxZC0xcWVlLTljNGMtMDAxcXdlMTI1MGI1IiwiZXhwIjoxNzAxMzYzODc0LCJpYXQiOjE3MDEzNTY2NzQsImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjYyM2UyMmQ5LTc1MTctNGEwOC04ZDc2LTliZjBlNDljYjEyYyIsInN1YiI6IiJ9.IB2-T_v9aHRfOyd_QQcNIMtdjA8q5pHfCeixMi5-b0E"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 401, + }, + ExpectedResponseNoRequest: true, + }, + }, + } + + t.Run("WasmPlugins oauth empty consumer", 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-oauth-empty-consumer.yaml b/test/e2e/conformance/tests/go-wasm-oauth-empty-consumer.yaml new file mode 100644 index 0000000000..4c24a70877 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-oauth-empty-consumer.yaml @@ -0,0 +1,58 @@ +# Copyright (c) 2022 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: + nginx.ingress.kubernetes.io/app-root: "/foo" + name: wasmplugin-oauth + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- + +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: oauth + namespace: higress-system +spec: + defaultConfig: + consumers: + issuer: Higress-Gateway + auth_path: /oauth2/token + global_credentials: true + auth_header_name: Authorization + token_ttl: 7200 + clock_skew_seconds: 3153600000 + keep_token: true + # global_auth is unset + matchRules: + - ingress: + - "higress-conformance-infra/wasmplugin-oauth" + config: + allow: + url: file:///opt/plugins/wasm-go/extensions/oauth/plugin.wasm \ No newline at end of file diff --git a/test/e2e/conformance/tests/go-wasm-oauth-expired-token.go b/test/e2e/conformance/tests/go-wasm-oauth-expired-token.go new file mode 100644 index 0000000000..d99107b08a --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-oauth-expired-token.go @@ -0,0 +1,64 @@ +// Copyright (c) 2022 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" +) + +func init() { + Register(WasmPluginsOAuthExpiredToken) +} + +var WasmPluginsOAuthExpiredToken = suite.ConformanceTest{ + ShortName: "WasmPluginsOAuthExpiredToken", + Description: "The Ingress in the higress-conformance-infra namespace test the oauth WASM plugin.", + Manifests: []string{"tests/go-wasm-oauth-expired-token.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TestCaseName: "case 1: expired token", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiOTUxNWI1NjQtMGIxZC0xMWVlLTljNGMtMDAxNjNlMTI1MGI1IiwiZXhwIjoxNzAxNDE4NzU5LCJpYXQiOjE3MDE0MTE1NTksImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjQ0YTMzYjc4LWNmYWItNGYzYS1iZDQ3LTQ1Y2Y5ZjM0YjVmZSIsInN1YiI6ImNvbnN1bWVyMSJ9.EIDCTVx4Wt6u5fRngFwgRo-qfDSKp6sUg4fKA7MYpuE"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 401, + }, + ExpectedResponseNoRequest: true, + }, + }, + + } + + t.Run("WasmPlugins oauth expired token", 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-oauth-expired-token.yaml b/test/e2e/conformance/tests/go-wasm-oauth-expired-token.yaml new file mode 100644 index 0000000000..0c8ef7fd3e --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-oauth-expired-token.yaml @@ -0,0 +1,62 @@ +# Copyright (c) 2022 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: + nginx.ingress.kubernetes.io/app-root: "/foo" + name: wasmplugin-oauth + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- + +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: oauth + namespace: higress-system +spec: + defaultConfig: + consumers: + - name: consumer1 + client_id: 9515b564-0b1d-11ee-9c4c-00163e1250b5 + client_secret: 9e55de56-0b1d-11ee-b8ec-00163e1250b5 + issuer: Higress-Gateway + auth_path: /oauth2/token + global_credentials: true + auth_header_name: Authorization + token_ttl: 7200 + clock_skew_seconds: 60 + keep_token: true + # global_auth is unset + matchRules: + - ingress: + - "higress-conformance-infra/wasmplugin-oauth" + config: + allow: + - consumer1 + url: file:///opt/plugins/wasm-go/extensions/oauth/plugin.wasm diff --git a/test/e2e/conformance/tests/go-wasm-oauth-global-auth.go b/test/e2e/conformance/tests/go-wasm-oauth-global-auth.go new file mode 100644 index 0000000000..bde052d5d8 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-oauth-global-auth.go @@ -0,0 +1,104 @@ +// Copyright (c) 2022 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" +) + +func init() { + Register(WasmPluginsOAuthGlobalAuth) +} + +var WasmPluginsOAuthGlobalAuth = suite.ConformanceTest{ + ShortName: "WasmPluginsOAuthGlobalAuth", + Description: "The Ingress in the higress-conformance-infra namespace test the oauth WASM plugin, with global_auth false", + Manifests: []string{"tests/go-wasm-oauth-global-auth.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TestCaseName: "case 1: with no ruleset under this route, oauth let every request pass", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo2.com", + Path: "/foo", + Headers: map[string]string{"Buthorization": "Aearer x.x.x"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "case 2: with some ruleset under this route, oauth checks token and route rule. token verify success, client in the route's allowset", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo1.com", + Path: "/foo", + // consumer1 + Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiOTUxNWI1NjQtMGIxZC0xMWVlLTljNGMtMDAxNjNlMTI1MGI1IiwiZXhwIjoxNzAxNDE4NzU5LCJpYXQiOjE3MDE0MTE1NTksImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjQ0YTMzYjc4LWNmYWItNGYzYS1iZDQ3LTQ1Y2Y5ZjM0YjVmZSIsInN1YiI6ImNvbnN1bWVyMSJ9.EIDCTVx4Wt6u5fRngFwgRo-qfDSKp6sUg4fKA7MYpuE"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "case 3: with some ruleset under this route, oauth checks token and route rule. token verify failed, client not in the route's allowset", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo1.com", + Path: "/foo", + // consumer2 + Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiODUyMWI1NjQtMGIxZC0xMWVlLTljNGMtMDAxNjNlMTI1MGI1IiwiZXhwIjoxNzAxNDIzOTU1LCJpYXQiOjE3MDE0MTY3NTUsImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjU1NDVkZDRhLWU4YjYtNDY2NC04ZDE4LWY3Yjk5YWVmYzQ1YyIsInN1YiI6ImNvbnN1bWVyMiJ9.FhxLbFFW0h3O3S8MH3vjFRj54xSmQIVVEC8IxGNpIcU"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + }, + }, + } + + t.Run("WasmPlugins oauth global 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-oauth-global-auth.yaml b/test/e2e/conformance/tests/go-wasm-oauth-global-auth.yaml new file mode 100644 index 0000000000..7b567a07f0 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-oauth-global-auth.yaml @@ -0,0 +1,90 @@ +# Copyright (c) 2022 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-oauth1 + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo1.com" + http: + paths: + - pathType: Prefix + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + name: wasmplugin-oauth2 + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo2.com" + http: + paths: + - pathType: Prefix + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- + +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: oauth + namespace: higress-system +spec: + defaultConfig: + consumers: + - name: consumer1 + client_id: 9515b564-0b1d-11ee-9c4c-00163e1250b5 + client_secret: 9e55de56-0b1d-11ee-b8ec-00163e1250b5 + - name: consumer2 + client_id: 8521b564-0b1d-11ee-9c4c-00163e1250b5 + client_secret: 8520b564-0b1d-11ee-9c4c-00163e1250b5 + issuer: Higress-Gateway + auth_path: /oauth2/token + global_credentials: true + auth_header_name: Authorization + token_ttl: 7200 + clock_skew_seconds: 3153600000 + keep_token: true + global_auth: false + matchRules: + - ingress: + - "higress-conformance-infra/wasmplugin-oauth1" + config: + allow: + - consumer1 + - ingress: + - "higress-conformance-infra/wasmplugin-oauth2" + config: + # warn:allow字段可以不写或者allow字段下面的列表为空,起到的效果是相同的。但是测试发现如果有config字段,但是config下不写任何字段会导致插件失效 + allow: + # - consumer2 + url: file:///opt/plugins/wasm-go/extensions/oauth/plugin.wasm diff --git a/test/e2e/conformance/tests/go-wasm-oauth-route-auth.go b/test/e2e/conformance/tests/go-wasm-oauth-route-auth.go new file mode 100644 index 0000000000..b66acb091e --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-oauth-route-auth.go @@ -0,0 +1,83 @@ +// Copyright (c) 2022 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" +) + +func init() { + Register(WasmPluginsOAuthRouteAuth) +} + +var WasmPluginsOAuthRouteAuth = suite.ConformanceTest{ + ShortName: "WasmPluginsOAuthRouteAuth", + Description: "The Ingress in the higress-conformance-infra namespace test the oauth WASM plugin, disabling credentials globally", + Manifests: []string{"tests/go-wasm-oauth-route-auth.yaml"}, + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TestCaseName: "case 1: audience not match this route", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiOTUxNWI1NjQtMGIxZC0xMWVlLTljNGMtMDAxNjNlMTI1MGI1IiwiZXhwIjoxNzAxNTI2MDY2LCJpYXQiOjE3MDE1MTg4NjYsImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6Ijc4MDYwYjVmLWRiY2EtNDljZi04MmM2LWEwNzRiY2UyY2QzZCIsInN1YiI6ImNvbnN1bWVyMSJ9.NOu7nDL7ebRoV7AnBx2L_JS4dp-G7b9ERk-s2YPS2BI"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + ExpectedResponseNoRequest: true, + }, + }, + { + Meta: http.AssertionMeta{ + TestCaseName: "case 2: audience match this route", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Headers: map[string]string{"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJoaWdyZXNzLWNvbmZvcm1hbmNlLWluZnJhL3dhc21wbHVnaW4tb2F1dGgiLCJjbGllbnRfaWQiOiI5NTE1YjU2NC0wYjFkLTExZWUtOWM0Yy0wMDE2M2UxMjUwYjUiLCJleHAiOjE3MDE1MjYwNjYsImlhdCI6MTcwMTUxODg2NiwiaXNzIjoiSGlncmVzcy1HYXRld2F5IiwianRpIjoiNzgwNjBiNWYtZGJjYS00OWNmLTgyYzYtYTA3NGJjZTJjZDNkIiwic3ViIjoiY29uc3VtZXIxIn0.Hml1oK6Vkzd_G5AAAQFFHJ_O20JjeXts8XZxBvGjheA"}, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: true, + }, + }, + } + + t.Run("WasmPlugins oauth route 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-oauth-route-auth.yaml b/test/e2e/conformance/tests/go-wasm-oauth-route-auth.yaml new file mode 100644 index 0000000000..06e6343ba0 --- /dev/null +++ b/test/e2e/conformance/tests/go-wasm-oauth-route-auth.yaml @@ -0,0 +1,62 @@ +# Copyright (c) 2022 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: + nginx.ingress.kubernetes.io/app-root: "/foo" + name: wasmplugin-oauth + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- + +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: oauth + namespace: higress-system +spec: + defaultConfig: + consumers: + - name: consumer1 + client_id: 9515b564-0b1d-11ee-9c4c-00163e1250b5 + client_secret: 9e55de56-0b1d-11ee-b8ec-00163e1250b5 + issuer: Higress-Gateway + auth_path: /oauth2/token + global_credentials: false + auth_header_name: Authorization + token_ttl: 7200 + clock_skew_seconds: 3153600000 + keep_token: true + + matchRules: + - ingress: + - "higress-conformance-infra/wasmplugin-oauth" + config: + allow: + - consumer1 + url: file:///opt/plugins/wasm-go/extensions/oauth/plugin.wasm