Skip to content

Commit

Permalink
feat(plugin): implement golang version of plugin jwt-auth (alibaba#743)
Browse files Browse the repository at this point in the history
Signed-off-by: Ink33 <Ink33@smlk.org>
  • Loading branch information
Ink-33 authored Jun 6, 2024
1 parent 6a40d83 commit ed976c6
Show file tree
Hide file tree
Showing 24 changed files with 2,713 additions and 1 deletion.
2 changes: 2 additions & 0 deletions plugins/wasm-go/extensions/jwt-auth/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM scratch
COPY main.wasm plugin.wasm
5 changes: 5 additions & 0 deletions plugins/wasm-go/extensions/jwt-auth/Makefile
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions plugins/wasm-go/extensions/jwt-auth/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0
34 changes: 34 additions & 0 deletions plugins/wasm-go/extensions/jwt-auth/config/checker.go
Original file line number Diff line number Diff line change
@@ -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
}
125 changes: 125 additions & 0 deletions plugins/wasm-go/extensions/jwt-auth/config/config.go
Original file line number Diff line number Diff line change
@@ -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"`
}
138 changes: 138 additions & 0 deletions plugins/wasm-go/extensions/jwt-auth/config/parser.go
Original file line number Diff line number Diff line change
@@ -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
}
22 changes: 22 additions & 0 deletions plugins/wasm-go/extensions/jwt-auth/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
Loading

0 comments on commit ed976c6

Please sign in to comment.