mirror of
https://github.com/alibaba/higress.git
synced 2026-06-04 18:17:33 +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:
@@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
cfg "github.com/alibaba/higress/plugins/wasm-go/extensions/jwt-auth/config"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
@@ -41,113 +42,180 @@ func OnHTTPRequestHeaders(ctx wrapper.HttpContext, config cfg.JWTAuthConfig, log
|
||||
var (
|
||||
noAllow = len(config.Allow) == 0 // 未配置 allow 列表,表示插件在该 domain/route 未生效
|
||||
globalAuthNoSet = config.GlobalAuthCheck() == cfg.GlobalAuthNoSet
|
||||
globalAuthSetTrue = config.GlobalAuthCheck() == cfg.GlobalAuthTrue
|
||||
globalAuthSetFalse = config.GlobalAuthCheck() == cfg.GlobalAuthFalse
|
||||
)
|
||||
|
||||
// 不需要认证而直接放行的情况:
|
||||
// - global_auth == false 且 当前 domain/route 未配置该插件
|
||||
// - global_auth 未设置 且 有至少一个 domain/route 配置该插件 且 当前 domain/route 未配置该插件
|
||||
if globalAuthSetFalse || (cfg.RuleSet && globalAuthNoSet) {
|
||||
if globalAuthSetFalse || (config.RuleSet && globalAuthNoSet) {
|
||||
if noAllow {
|
||||
log.Info("authorization is not required")
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
|
||||
verifyTime := time.Now()
|
||||
decision := verifyConsumers(config, log, verifyTime)
|
||||
if decision.remoteConsumer != nil {
|
||||
return fetchRemoteJWKsAndVerify(decision.remoteConsumer, config, log, verifyTime)
|
||||
}
|
||||
return decision.action()
|
||||
}
|
||||
|
||||
func fetchRemoteJWKsAndVerify(consumer *cfg.Consumer, config cfg.JWTAuthConfig, log log.Log, verifyTime time.Time) types.Action {
|
||||
err := fetchRemoteJWKs(consumer, log, func() {
|
||||
completeAuthenticationAfterRemoteFetch(config, log, verifyTime, 1)
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("failed to dispatch remote jwks fetch, consumer:%s, reason:%s", consumer.Name, err.Error())
|
||||
return actionAfterRemoteFetch(config, log, verifyTime, 1)
|
||||
}
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
|
||||
func completeAuthenticationAfterRemoteFetch(config cfg.JWTAuthConfig, log log.Log, verifyTime time.Time, attempts int) {
|
||||
decision := decisionAfterRemoteFetch(config, log, verifyTime, attempts)
|
||||
if decision.waitingRemoteFetch {
|
||||
return
|
||||
}
|
||||
_ = decision.action()
|
||||
if decision.resume {
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
}
|
||||
|
||||
func actionAfterRemoteFetch(config cfg.JWTAuthConfig, log log.Log, verifyTime time.Time, attempts int) types.Action {
|
||||
decision := decisionAfterRemoteFetch(config, log, verifyTime, attempts)
|
||||
if decision.waitingRemoteFetch {
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
return decision.action()
|
||||
}
|
||||
|
||||
func decisionAfterRemoteFetch(config cfg.JWTAuthConfig, log log.Log, verifyTime time.Time, attempts int) authDecision {
|
||||
for {
|
||||
decision := verifyConsumers(config, log, verifyTime)
|
||||
if decision.remoteConsumer == nil {
|
||||
return decision
|
||||
}
|
||||
if attempts >= len(config.Consumers) {
|
||||
log.Warnf("remote jwks fetch chain exhausted after %d attempts", attempts)
|
||||
return authDecision{action: deniedJWTVerificationFails}
|
||||
}
|
||||
|
||||
// Chained fetches only advance after each response has populated or rejected one cache entry.
|
||||
nextAttempts := attempts + 1
|
||||
err := fetchRemoteJWKs(decision.remoteConsumer, log, func() {
|
||||
completeAuthenticationAfterRemoteFetch(config, log, verifyTime, nextAttempts)
|
||||
})
|
||||
if err == nil {
|
||||
return authDecision{waitingRemoteFetch: true}
|
||||
}
|
||||
|
||||
log.Warnf("failed to dispatch remote jwks fetch, consumer:%s, reason:%s", decision.remoteConsumer.Name, err.Error())
|
||||
attempts = nextAttempts
|
||||
}
|
||||
}
|
||||
|
||||
type authDecision struct {
|
||||
action func() types.Action
|
||||
resume bool
|
||||
remoteConsumer *cfg.Consumer
|
||||
waitingRemoteFetch bool
|
||||
}
|
||||
|
||||
func verifyConsumers(config cfg.JWTAuthConfig, log log.Log, verifyTime time.Time) authDecision {
|
||||
header := &proxywasmProvider{}
|
||||
actionMap := map[string]func() types.Action{}
|
||||
unAuthzConsumer := ""
|
||||
var firstRemoteConsumer *cfg.Consumer
|
||||
|
||||
// 匹配consumer
|
||||
for i := range config.Consumers {
|
||||
err := consumerVerify(config.Consumers[i], time.Now(), header, log)
|
||||
consumer := config.Consumers[i]
|
||||
verified, err := consumerVerify(consumer, verifyTime, header, log)
|
||||
if err != nil {
|
||||
if isRemoteJWKsCacheMiss(err) {
|
||||
if firstRemoteConsumer == nil && consumerAllowedForFetch(config, consumer.Name) {
|
||||
firstRemoteConsumer = consumer
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Warn(err.Error())
|
||||
if v, ok := err.(*ErrDenied); ok {
|
||||
actionMap[config.Consumers[i].Name] = v.denied
|
||||
actionMap[consumer.Name] = v.denied
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 全局生效:
|
||||
// - global_auth == true 且 当前 domain/route 未配置该插件
|
||||
// - global_auth 未设置 且 没有任何一个 domain/route 配置该插件
|
||||
if (globalAuthSetTrue && noAllow) || (globalAuthNoSet && !cfg.RuleSet) {
|
||||
log.Infof("consumer %q authenticated", config.Consumers[i].Name)
|
||||
return authenticated(config.Consumers[i].Name)
|
||||
action, resume := actionForVerifiedConsumer(config, consumer.Name, log)
|
||||
if resume {
|
||||
applyConsumerSideEffects(consumer, verified, header, log)
|
||||
return authDecision{action: action, resume: true}
|
||||
}
|
||||
|
||||
// 全局生效,但当前 domain/route 配置了 allow 列表
|
||||
if globalAuthSetTrue && !noAllow {
|
||||
if !contains(config.Consumers[i].Name, config.Allow) {
|
||||
log.Warnf("jwt verify failed, consumer %q not allow",
|
||||
config.Consumers[i].Name)
|
||||
actionMap[config.Consumers[i].Name] = deniedUnauthorizedConsumer
|
||||
unAuthzConsumer = config.Consumers[i].Name
|
||||
continue
|
||||
}
|
||||
log.Infof("consumer %q authenticated", config.Consumers[i].Name)
|
||||
return authenticated(config.Consumers[i].Name)
|
||||
if action != nil {
|
||||
actionMap[consumer.Name] = action
|
||||
unAuthzConsumer = consumer.Name
|
||||
continue
|
||||
}
|
||||
|
||||
// 非全局生效
|
||||
if globalAuthSetFalse || (globalAuthNoSet && cfg.RuleSet) {
|
||||
if !noAllow { // 配置了 allow 列表
|
||||
if !contains(config.Consumers[i].Name, config.Allow) {
|
||||
log.Warnf("jwt verify failed, consumer %q not allow",
|
||||
config.Consumers[i].Name)
|
||||
actionMap[config.Consumers[i].Name] = deniedUnauthorizedConsumer
|
||||
unAuthzConsumer = config.Consumers[i].Name
|
||||
continue
|
||||
}
|
||||
log.Infof("consumer %q authenticated", config.Consumers[i].Name)
|
||||
return authenticated(config.Consumers[i].Name)
|
||||
}
|
||||
}
|
||||
|
||||
// switch config.GlobalAuthCheck() {
|
||||
|
||||
// case cfg.GlobalAuthNoSet:
|
||||
// if !cfg.RuleSet {
|
||||
// log.Infof("consumer %q authenticated", config.Consumers[i].Name)
|
||||
// return authenticated(config.Consumers[i].Name)
|
||||
// }
|
||||
// case cfg.GlobalAuthTrue:
|
||||
// if len(config.Allow) == 0 {
|
||||
// log.Infof("consumer %q authenticated", config.Consumers[i].Name)
|
||||
// return authenticated(config.Consumers[i].Name)
|
||||
// }
|
||||
// fallthrough // 若 allow 列表不为空,则 fallthrough 到需要检查 allow 列表的逻辑中
|
||||
|
||||
// // 全局生效设置为 false
|
||||
// case cfg.GlobalAuthFalse:
|
||||
// if !contains(config.Consumers[i].Name, config.Allow) {
|
||||
// log.Warnf("jwt verify failed, consumer %q not allow",
|
||||
// config.Consumers[i].Name)
|
||||
// actionMap[config.Consumers[i].Name] = deniedUnauthorizedConsumer
|
||||
// unAuthzConsumer = config.Consumers[i].Name
|
||||
// continue
|
||||
// }
|
||||
// log.Infof("consumer %q authenticated", config.Consumers[i].Name)
|
||||
// return authenticated(config.Consumers[i].Name)
|
||||
// }
|
||||
}
|
||||
|
||||
if firstRemoteConsumer != nil {
|
||||
return authDecision{remoteConsumer: firstRemoteConsumer}
|
||||
}
|
||||
if len(config.Allow) == 1 {
|
||||
if unAuthzConsumer != "" {
|
||||
log.Warnf("consumer %q denied", unAuthzConsumer)
|
||||
return deniedUnauthorizedConsumer()
|
||||
return authDecision{action: deniedUnauthorizedConsumer}
|
||||
}
|
||||
if v, ok := actionMap[config.Allow[0]]; ok {
|
||||
log.Warnf("consumer %q denied", config.Allow[0])
|
||||
return v()
|
||||
return authDecision{action: v}
|
||||
}
|
||||
}
|
||||
|
||||
// 拒绝兜底
|
||||
log.Warnf("all consumers verify failed")
|
||||
return deniedNotAllow()
|
||||
return authDecision{action: deniedNotAllow}
|
||||
}
|
||||
|
||||
func actionForVerifiedConsumer(config cfg.JWTAuthConfig, name string, log log.Log) (func() types.Action, bool) {
|
||||
noAllow := len(config.Allow) == 0
|
||||
globalAuthNoSet := config.GlobalAuthCheck() == cfg.GlobalAuthNoSet
|
||||
globalAuthSetTrue := config.GlobalAuthCheck() == cfg.GlobalAuthTrue
|
||||
globalAuthSetFalse := config.GlobalAuthCheck() == cfg.GlobalAuthFalse
|
||||
|
||||
if (globalAuthSetTrue && noAllow) || (globalAuthNoSet && !config.RuleSet) {
|
||||
log.Infof("consumer %q authenticated", name)
|
||||
return func() types.Action { return authenticated(name) }, true
|
||||
}
|
||||
|
||||
if globalAuthSetTrue && !noAllow {
|
||||
if !contains(name, config.Allow) {
|
||||
log.Warnf("jwt verify failed, consumer %q not allow", name)
|
||||
return deniedUnauthorizedConsumer, false
|
||||
}
|
||||
log.Infof("consumer %q authenticated", name)
|
||||
return func() types.Action { return authenticated(name) }, true
|
||||
}
|
||||
|
||||
if globalAuthSetFalse || (globalAuthNoSet && config.RuleSet) {
|
||||
if !noAllow {
|
||||
if !contains(name, config.Allow) {
|
||||
log.Warnf("jwt verify failed, consumer %q not allow", name)
|
||||
return deniedUnauthorizedConsumer, false
|
||||
}
|
||||
log.Infof("consumer %q authenticated", name)
|
||||
return func() types.Action { return authenticated(name) }, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func consumerAllowedForFetch(config cfg.JWTAuthConfig, name string) bool {
|
||||
return len(config.Allow) == 0 || contains(name, config.Allow)
|
||||
}
|
||||
|
||||
func contains(str string, arr []string) bool {
|
||||
|
||||
Reference in New Issue
Block a user