mirror of
https://github.com/alibaba/higress.git
synced 2026-06-05 02:27:28 +08:00
feat(jwt-auth): support remote JWKS (#3838)
Signed-off-by: Betula-L <6059935+Betula-L@users.noreply.github.com> Co-authored-by: Betula-L <6059935+Betula-L@users.noreply.github.com>
This commit is contained in:
@@ -14,6 +14,8 @@
|
||||
|
||||
package config
|
||||
|
||||
import "github.com/go-jose/go-jose/v3"
|
||||
|
||||
var (
|
||||
// DefaultClaimToHeaderOverride 是 claim_to_override 中 override 字段的默认值
|
||||
DefaultClaimToHeaderOverride = true
|
||||
@@ -24,6 +26,12 @@ var (
|
||||
// DefaultKeepToken 是 KeepToken 的默认值
|
||||
DefaultKeepToken = true
|
||||
|
||||
// DefaultJWKsCacheDuration is the default remote JWKS cache duration in seconds.
|
||||
DefaultJWKsCacheDuration = int64(600)
|
||||
|
||||
// DefaultJWKsFetchTimeout is the default remote JWKS fetch timeout in milliseconds.
|
||||
DefaultJWKsFetchTimeout = int64(1500)
|
||||
|
||||
// DefaultFromHeader 是 from_header 的默认值
|
||||
DefaultFromHeader = []FromHeader{{
|
||||
Name: "Authorization",
|
||||
@@ -37,6 +45,9 @@ var (
|
||||
DefaultFromCookies = []string{}
|
||||
)
|
||||
|
||||
// RemoteJWKsMinRefreshIntervalSeconds is the shared lower bound for remote JWKS cache TTL and retry backoff.
|
||||
const RemoteJWKsMinRefreshIntervalSeconds = int64(30)
|
||||
|
||||
// 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 {
|
||||
@@ -45,6 +56,9 @@ type JWTAuthConfig struct {
|
||||
// Consumers 配置服务的调用者,用于对请求进行认证
|
||||
Consumers []*Consumer `json:"consumers"`
|
||||
|
||||
// RuleSet records whether at least one domain or route rule is configured.
|
||||
RuleSet bool `json:"-"`
|
||||
|
||||
// 全局配置
|
||||
//
|
||||
// GlobalAuth 若配置为true,则全局生效认证机制;
|
||||
@@ -68,6 +82,20 @@ type Consumer struct {
|
||||
// https://www.rfc-editor.org/rfc/rfc7517
|
||||
JWKs string `json:"jwks"`
|
||||
|
||||
// ParsedJWKs caches parsed inline JWKS after config validation.
|
||||
ParsedJWKs *jose.JSONWebKeySet `json:"-"`
|
||||
|
||||
// RemoteJWKs specifies a remote JWKS endpoint referenced by service.
|
||||
// The service must be configured or discovered by Higress, for example via McpBridge.
|
||||
RemoteJWKs *RemoteJWKs `json:"remote_jwks,omitempty"`
|
||||
|
||||
// JWKsCacheDuration is the remote JWKS cache duration in seconds.
|
||||
// Requests are denied while the first fetch is in flight or after recent fetch failures.
|
||||
JWKsCacheDuration *int64 `json:"jwks_cache_duration,omitempty"`
|
||||
|
||||
// JWKsFetchTimeout is the remote JWKS fetch timeout in milliseconds.
|
||||
JWKsFetchTimeout *int64 `json:"jwks_fetch_timeout,omitempty"`
|
||||
|
||||
// Issuer JWT的签发者,需要和payload中的iss字段保持一致
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
@@ -102,6 +130,20 @@ type Consumer struct {
|
||||
KeepToken *bool `json:"keep_token,omitempty"`
|
||||
}
|
||||
|
||||
type RemoteJWKs struct {
|
||||
// ServiceName is the FQDN service name used to build the outbound cluster.
|
||||
ServiceName string `json:"service_name"`
|
||||
|
||||
// ServiceHost is the HTTP Host/:authority header for the JWKS request.
|
||||
ServiceHost string `json:"service_host,omitempty"`
|
||||
|
||||
// ServicePort is the service port used to build the outbound cluster. Defaults to 443.
|
||||
ServicePort *int64 `json:"service_port,omitempty"`
|
||||
|
||||
// Path is the JWKS request path, for example "/.well-known/jwks.json".
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// ClaimsToHeader 抽取JWT的payload中指定字段,设置到指定的请求头中转发给后端
|
||||
type ClaimsToHeader struct {
|
||||
// Claim JWT payload中的指定字段,要求必须是字符串或无符号整数类型
|
||||
|
||||
@@ -17,19 +17,21 @@ package config
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// RuleSet 插件是否至少在一个 domain 或 route 上生效
|
||||
var RuleSet bool
|
||||
const maxJWKsFetchTimeout = int64(10 * 1000) // milliseconds
|
||||
const maxJWKsCacheDuration = int64(7 * 24 * 3600) // seconds
|
||||
const minJWKsCacheDuration = RemoteJWKsMinRefreshIntervalSeconds
|
||||
|
||||
// ParseGlobalConfig 从wrapper提供的配置中解析并转换到插件运行时需要使用的配置。
|
||||
// 此处解析的是全局配置,域名和路由级配置由 ParseRuleConfig 负责。
|
||||
func ParseGlobalConfig(json gjson.Result, config *JWTAuthConfig, log log.Log) error {
|
||||
RuleSet = false
|
||||
config.RuleSet = len(json.Get("_rules_").Array()) > 0
|
||||
consumers := json.Get("consumers")
|
||||
if !consumers.IsArray() {
|
||||
return fmt.Errorf("failed to parse configuration for consumers: consumers is not a array")
|
||||
@@ -70,7 +72,7 @@ func ParseRuleConfig(json gjson.Result, global JWTAuthConfig, config *JWTAuthCon
|
||||
config.Allow = append(config.Allow, item.String())
|
||||
}
|
||||
|
||||
RuleSet = true
|
||||
config.RuleSet = true
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,11 +90,39 @@ func ParseConsumer(consumer gjson.Result, names map[string]struct{}) (c *Consume
|
||||
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)
|
||||
c.Issuer = strings.TrimSpace(c.Issuer)
|
||||
c.JWKs = strings.TrimSpace(c.JWKs)
|
||||
if c.RemoteJWKs != nil {
|
||||
normalizeRemoteJWKs(c.RemoteJWKs)
|
||||
}
|
||||
if c.JWKs == "" && c.RemoteJWKs == nil {
|
||||
return nil, fmt.Errorf("one of jwks and remote_jwks is required, consumer:%s", c.Name)
|
||||
}
|
||||
if c.JWKs != "" && c.RemoteJWKs != nil {
|
||||
return nil, fmt.Errorf("only one of jwks and remote_jwks can be configured, consumer:%s", c.Name)
|
||||
}
|
||||
if c.JWKs != "" {
|
||||
if c.JWKsCacheDuration != nil || c.JWKsFetchTimeout != nil {
|
||||
return nil, fmt.Errorf("jwks_cache_duration and jwks_fetch_timeout only apply to remote_jwks, consumer:%s", c.Name)
|
||||
}
|
||||
// Validate inline JWKS before accepting the consumer.
|
||||
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)
|
||||
}
|
||||
if len(jwks.Keys) == 0 {
|
||||
return nil, fmt.Errorf("jwks is empty, consumer:%s", c.Name)
|
||||
}
|
||||
c.ParsedJWKs = jwks
|
||||
}
|
||||
if c.RemoteJWKs != nil {
|
||||
if c.Issuer == "" {
|
||||
return nil, fmt.Errorf("issuer is required when remote_jwks is set, consumer:%s", c.Name)
|
||||
}
|
||||
if err := validateRemoteJWKs(c.RemoteJWKs); err != nil {
|
||||
return nil, fmt.Errorf("remote_jwks is invalid, consumer:%s, reason:%s", c.Name, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要使用默认jwt抽取来源
|
||||
@@ -132,7 +162,87 @@ func ParseConsumer(consumer gjson.Result, names map[string]struct{}) (c *Consume
|
||||
c.KeepToken = &DefaultKeepToken
|
||||
}
|
||||
|
||||
if c.RemoteJWKs != nil {
|
||||
// Fill the default remote JWKS cache duration.
|
||||
if c.JWKsCacheDuration == nil {
|
||||
v := DefaultJWKsCacheDuration
|
||||
c.JWKsCacheDuration = &v
|
||||
}
|
||||
if *c.JWKsCacheDuration <= 0 {
|
||||
return nil, fmt.Errorf("jwks_cache_duration must be positive, consumer:%s", c.Name)
|
||||
}
|
||||
if *c.JWKsCacheDuration < minJWKsCacheDuration {
|
||||
return nil, fmt.Errorf("jwks_cache_duration must be greater than or equal to %d, consumer:%s", minJWKsCacheDuration, c.Name)
|
||||
}
|
||||
if *c.JWKsCacheDuration > maxJWKsCacheDuration {
|
||||
return nil, fmt.Errorf("jwks_cache_duration must be less than or equal to %d, consumer:%s", maxJWKsCacheDuration, c.Name)
|
||||
}
|
||||
|
||||
// Fill the default remote JWKS fetch timeout.
|
||||
if c.JWKsFetchTimeout == nil {
|
||||
v := DefaultJWKsFetchTimeout
|
||||
c.JWKsFetchTimeout = &v
|
||||
}
|
||||
if *c.JWKsFetchTimeout <= 0 {
|
||||
return nil, fmt.Errorf("jwks_fetch_timeout must be positive, consumer:%s", c.Name)
|
||||
}
|
||||
if *c.JWKsFetchTimeout > maxJWKsFetchTimeout {
|
||||
return nil, fmt.Errorf("jwks_fetch_timeout must be less than or equal to %d, consumer:%s", maxJWKsFetchTimeout, c.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// consumer合法,记录consumer名称
|
||||
names[c.Name] = struct{}{}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func normalizeRemoteJWKs(remote *RemoteJWKs) {
|
||||
remote.ServiceName = strings.TrimSpace(remote.ServiceName)
|
||||
remote.ServiceHost = strings.TrimSpace(remote.ServiceHost)
|
||||
remote.Path = strings.TrimSpace(remote.Path)
|
||||
if remote.ServicePort == nil {
|
||||
v := int64(443)
|
||||
remote.ServicePort = &v
|
||||
}
|
||||
}
|
||||
|
||||
func validateRemoteJWKs(remote *RemoteJWKs) error {
|
||||
if remote.ServiceName == "" {
|
||||
return fmt.Errorf("service_name is required")
|
||||
}
|
||||
if hasInvalidRemoteJWKsFieldChar(remote.ServiceName) || strings.ContainsAny(remote.ServiceName, "|/?#@:") {
|
||||
return fmt.Errorf("service_name must not contain whitespace, control characters, or URI separators")
|
||||
}
|
||||
if remote.ServiceHost == "" {
|
||||
return fmt.Errorf("service_host is required")
|
||||
}
|
||||
if hasInvalidRemoteJWKsFieldChar(remote.ServiceHost) {
|
||||
return fmt.Errorf("service_host must not contain whitespace or control characters")
|
||||
}
|
||||
if strings.ContainsAny(remote.ServiceHost, "/?#@:") || strings.Contains(remote.ServiceHost, "://") {
|
||||
return fmt.Errorf("service_host must be a host without port")
|
||||
}
|
||||
if remote.Path == "" || !strings.HasPrefix(remote.Path, "/") {
|
||||
return fmt.Errorf("path must start with /")
|
||||
}
|
||||
if hasInvalidRemoteJWKsFieldChar(remote.Path) {
|
||||
return fmt.Errorf("path must not contain whitespace or control characters")
|
||||
}
|
||||
if *remote.ServicePort <= 0 || *remote.ServicePort > 65535 {
|
||||
return fmt.Errorf("service_port is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasInvalidRemoteJWKsFieldChar(value string) bool {
|
||||
return strings.ContainsAny(value, " \t\r\n") || hasControlChar(value)
|
||||
}
|
||||
|
||||
func hasControlChar(value string) bool {
|
||||
for _, r := range value {
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
288
plugins/wasm-go/extensions/jwt-auth/config/parser_test.go
Normal file
288
plugins/wasm-go/extensions/jwt-auth/config/parser_test.go
Normal file
@@ -0,0 +1,288 @@
|
||||
// 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 (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const testJWKs = "{\"keys\":[{\"kty\":\"EC\",\"kid\":\"p256\",\"crv\":\"P-256\",\"x\":\"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\"y\":\"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"}]}"
|
||||
|
||||
func TestParseGlobalConfigRecordsRulesExist(t *testing.T) {
|
||||
cfg := &JWTAuthConfig{}
|
||||
err := ParseGlobalConfig(gjson.Parse(`{
|
||||
"consumers": [{
|
||||
"name": "inline-consumer",
|
||||
"issuer": "higress-test",
|
||||
"jwks": `+quoteJSON(testJWKs)+`
|
||||
}],
|
||||
"_rules_": [{
|
||||
"_match_domain_": ["private.example.com"],
|
||||
"allow": ["inline-consumer"]
|
||||
}]
|
||||
}`), cfg, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseGlobalConfig returned error: %v", err)
|
||||
}
|
||||
if !cfg.RuleSet {
|
||||
t.Fatalf("expected global config to record that route/domain rules exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerCachesInlineJWKs(t *testing.T) {
|
||||
consumer, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "inline-consumer",
|
||||
"issuer": "higress-test",
|
||||
"jwks": `+quoteJSON(testJWKs)+`
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseConsumer returned error: %v", err)
|
||||
}
|
||||
if consumer.ParsedJWKs == nil || len(consumer.ParsedJWKs.Keys) != 1 {
|
||||
t.Fatalf("expected parsed inline jwks to be cached, got: %#v", consumer.ParsedJWKs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerTrimsIssuer(t *testing.T) {
|
||||
consumer, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "inline-consumer",
|
||||
"issuer": " higress-test ",
|
||||
"jwks": `+quoteJSON(testJWKs)+`
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseConsumer returned error: %v", err)
|
||||
}
|
||||
if consumer.Issuer != "higress-test" {
|
||||
t.Fatalf("expected issuer to be trimmed, got: %q", consumer.Issuer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerAcceptsRemoteJWKsService(t *testing.T) {
|
||||
consumer, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "remote-consumer",
|
||||
"issuer": "higress-test",
|
||||
"remote_jwks": {
|
||||
"service_name": "auth.example.com.dns",
|
||||
"service_host": "auth.example.com",
|
||||
"service_port": 443,
|
||||
"path": "/.well-known/jwks.json"
|
||||
}
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseConsumer returned error: %v", err)
|
||||
}
|
||||
if consumer.RemoteJWKs == nil {
|
||||
t.Fatalf("expected remote_jwks to be parsed")
|
||||
}
|
||||
if consumer.RemoteJWKs.ServiceName != "auth.example.com.dns" {
|
||||
t.Fatalf("unexpected service_name: %q", consumer.RemoteJWKs.ServiceName)
|
||||
}
|
||||
if consumer.RemoteJWKs.ServiceHost != "auth.example.com" {
|
||||
t.Fatalf("unexpected service_host: %q", consumer.RemoteJWKs.ServiceHost)
|
||||
}
|
||||
if consumer.RemoteJWKs.ServicePort == nil || *consumer.RemoteJWKs.ServicePort != 443 {
|
||||
t.Fatalf("unexpected service_port: %v", consumer.RemoteJWKs.ServicePort)
|
||||
}
|
||||
if consumer.RemoteJWKs.Path != "/.well-known/jwks.json" {
|
||||
t.Fatalf("unexpected path: %q", consumer.RemoteJWKs.Path)
|
||||
}
|
||||
if got := *consumer.JWKsCacheDuration; got != 600 {
|
||||
t.Fatalf("unexpected jwks_cache_duration: %d", got)
|
||||
}
|
||||
if got := *consumer.JWKsFetchTimeout; got != 1500 {
|
||||
t.Fatalf("unexpected jwks_fetch_timeout: %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerTrimsRemoteJWKsServiceFields(t *testing.T) {
|
||||
consumer, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "remote-consumer",
|
||||
"issuer": "higress-test",
|
||||
"remote_jwks": {
|
||||
"service_name": " auth.example.com.dns ",
|
||||
"service_host": " auth.example.com ",
|
||||
"path": " /.well-known/jwks.json "
|
||||
}
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ParseConsumer returned error: %v", err)
|
||||
}
|
||||
if consumer.RemoteJWKs.ServiceName != "auth.example.com.dns" {
|
||||
t.Fatalf("unexpected service_name: %q", consumer.RemoteJWKs.ServiceName)
|
||||
}
|
||||
if consumer.RemoteJWKs.ServiceHost != "auth.example.com" {
|
||||
t.Fatalf("unexpected service_host: %q", consumer.RemoteJWKs.ServiceHost)
|
||||
}
|
||||
if consumer.RemoteJWKs.Path != "/.well-known/jwks.json" {
|
||||
t.Fatalf("unexpected path: %q", consumer.RemoteJWKs.Path)
|
||||
}
|
||||
if consumer.RemoteJWKs.ServicePort == nil || *consumer.RemoteJWKs.ServicePort != 443 {
|
||||
t.Fatalf("expected default service_port 443, got: %v", consumer.RemoteJWKs.ServicePort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerRejectsBothInlineAndRemoteJWKs(t *testing.T) {
|
||||
_, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "remote-consumer",
|
||||
"issuer": "higress-test",
|
||||
"jwks": `+quoteJSON(testJWKs)+`,
|
||||
"remote_jwks": {"service_name": "auth.example.com.dns", "path": "/.well-known/jwks.json"}
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err == nil || !containsError(err, "only one of jwks and remote_jwks can be configured") {
|
||||
t.Fatalf("expected mutually exclusive jwks error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerRejectsMissingJWKs(t *testing.T) {
|
||||
_, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "remote-consumer",
|
||||
"issuer": "higress-test"
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err == nil || !containsError(err, "one of jwks and remote_jwks is required") {
|
||||
t.Fatalf("expected missing jwks error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerRejectsRemoteJWKsWithoutIssuer(t *testing.T) {
|
||||
_, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "remote-consumer",
|
||||
"remote_jwks": {"service_name": "auth.example.com.dns", "path": "/.well-known/jwks.json"}
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err == nil || !containsError(err, "issuer is required when remote_jwks is set") {
|
||||
t.Fatalf("expected missing issuer error for remote jwks, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerRejectsInvalidRemoteJWKsService(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteJWKs string
|
||||
}{
|
||||
{name: "missing service_name", remoteJWKs: `"path": "/.well-known/jwks.json"`},
|
||||
{name: "blank service_name", remoteJWKs: `"service_name": " ", "path": "/.well-known/jwks.json"`},
|
||||
{name: "service_name whitespace", remoteJWKs: `"service_name": "auth example", "path": "/.well-known/jwks.json"`},
|
||||
{name: "service_name cluster separator", remoteJWKs: `"service_name": "auth|example", "path": "/.well-known/jwks.json"`},
|
||||
{name: "service_name path", remoteJWKs: `"service_name": "auth.example.com/jwks", "path": "/.well-known/jwks.json"`},
|
||||
{name: "missing service_host", remoteJWKs: `"service_name": "auth.example.com.dns", "path": "/.well-known/jwks.json"`},
|
||||
{name: "service_host whitespace", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth example", "path": "/.well-known/jwks.json"`},
|
||||
{name: "service_host scheme", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "https://auth.example.com", "path": "/.well-known/jwks.json"`},
|
||||
{name: "service_host port", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com:8443", "path": "/.well-known/jwks.json"`},
|
||||
{name: "service_host path", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com/jwks", "path": "/.well-known/jwks.json"`},
|
||||
{name: "service_host userinfo", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "user@auth.example.com", "path": "/.well-known/jwks.json"`},
|
||||
{name: "path control char", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "/jwks\n.json"`},
|
||||
{name: "path whitespace", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "/jwks file.json"`},
|
||||
{name: "missing path", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com"`},
|
||||
{name: "relative path", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "jwks.json"`},
|
||||
{name: "invalid port", remoteJWKs: `"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "service_port": 99999, "path": "/.well-known/jwks.json"`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "remote-consumer",
|
||||
"issuer": "higress-test",
|
||||
"remote_jwks": {`+tt.remoteJWKs+`}
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err == nil || !containsError(err, "remote_jwks is invalid") {
|
||||
t.Fatalf("expected invalid remote_jwks error, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerRejectsTooLargeRemoteJWKsFetchTimeout(t *testing.T) {
|
||||
_, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "remote-consumer",
|
||||
"issuer": "higress-test",
|
||||
"remote_jwks": {"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "/.well-known/jwks.json"},
|
||||
"jwks_fetch_timeout": 10001
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err == nil || !containsError(err, "jwks_fetch_timeout must be less than or equal to") {
|
||||
t.Fatalf("expected invalid jwks_fetch_timeout error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerRejectsTooLargeRemoteJWKsCacheDuration(t *testing.T) {
|
||||
_, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "remote-consumer",
|
||||
"issuer": "higress-test",
|
||||
"remote_jwks": {"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "/.well-known/jwks.json"},
|
||||
"jwks_cache_duration": 604801
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err == nil || !containsError(err, "jwks_cache_duration must be less than or equal to") {
|
||||
t.Fatalf("expected invalid jwks_cache_duration error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerRejectsTooSmallRemoteJWKsCacheDuration(t *testing.T) {
|
||||
_, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "remote-consumer",
|
||||
"issuer": "higress-test",
|
||||
"remote_jwks": {"service_name": "auth.example.com.dns", "service_host": "auth.example.com", "path": "/.well-known/jwks.json"},
|
||||
"jwks_cache_duration": 29
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err == nil || !containsError(err, "jwks_cache_duration must be greater than or equal to 30") {
|
||||
t.Fatalf("expected invalid jwks_cache_duration error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerRejectsRemoteJWKsOptionsForInlineJWKs(t *testing.T) {
|
||||
_, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "inline-consumer",
|
||||
"issuer": "higress-test",
|
||||
"jwks": `+quoteJSON(testJWKs)+`,
|
||||
"jwks_cache_duration": 600
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err == nil || !containsError(err, "jwks_cache_duration and jwks_fetch_timeout only apply to remote_jwks") {
|
||||
t.Fatalf("expected inline jwks remote option error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsumerRejectsEmptyInlineJWKs(t *testing.T) {
|
||||
_, err := ParseConsumer(gjson.Parse(`{
|
||||
"name": "remote-consumer",
|
||||
"issuer": "higress-test",
|
||||
"jwks": "{\"keys\":[]}"
|
||||
}`), map[string]struct{}{})
|
||||
|
||||
if err == nil || !containsError(err, "jwks is empty") {
|
||||
t.Fatalf("expected empty jwks error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func quoteJSON(value string) string {
|
||||
return strconv.Quote(value)
|
||||
}
|
||||
|
||||
func containsError(err error, want string) bool {
|
||||
return err != nil && strings.Contains(err.Error(), want)
|
||||
}
|
||||
Reference in New Issue
Block a user