feat(plugin): implement golang version of plugin jwt-auth (#743)

Signed-off-by: Ink33 <Ink33@smlk.org>
This commit is contained in:
Ink33
2024-06-06 10:22:51 +08:00
committed by GitHub
parent 6a40d83ec0
commit ed976c6d06
24 changed files with 2713 additions and 1 deletions

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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
}