Files
higress/plugins/wasm-go/extensions/jwt-auth/handler/verify_test.go
2026-05-25 16:04:10 +08:00

571 lines
23 KiB
Go

package handler
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/alibaba/higress/plugins/wasm-go/extensions/jwt-auth/config"
"github.com/go-jose/go-jose/v3"
"github.com/go-jose/go-jose/v3/jwt"
"github.com/tidwall/gjson"
)
type testLogger struct {
T *testing.T
}
func (l *testLogger) Trace(msg string) {
l.T.Log(msg)
}
func (l *testLogger) Tracef(format string, args ...interface{}) {
l.T.Logf(format, args...)
}
func (l *testLogger) Debug(msg string) {
l.T.Log(msg)
}
func (l *testLogger) Debugf(format string, args ...interface{}) {
l.T.Logf(format, args...)
}
func (l *testLogger) Info(msg string) {
l.T.Log(msg)
}
func (l *testLogger) Infof(format string, args ...interface{}) {
l.T.Logf(format, args...)
}
func (l *testLogger) Warn(msg string) {
l.T.Log(msg)
}
func (l *testLogger) Warnf(format string, args ...interface{}) {
l.T.Logf(format, args...)
}
func (l *testLogger) Error(msg string) {
l.T.Log(msg)
}
func (l *testLogger) Errorf(format string, args ...interface{}) {
l.T.Logf(format, args...)
}
func (l *testLogger) Critical(msg string) {
l.T.Log(msg)
}
func (l *testLogger) Criticalf(format string, args ...interface{}) {
l.T.Logf(format, args...)
}
func (l *testLogger) ResetID(pluginID string) {}
type recordingLogger struct {
entries []string
}
func (l *recordingLogger) Warnf(format string, args ...interface{}) {
l.entries = append(l.entries, fmt.Sprintf(format, args...))
}
type testProvider struct {
headerMap map[string]string
}
func (p *testProvider) GetHttpRequestHeader(key string) (string, error) {
if v, ok := p.headerMap[key]; ok {
return v, nil
}
return "", errors.New("no found")
}
func (p *testProvider) ReplaceHttpRequestHeader(key string, value string) error {
p.headerMap[key] = value
return nil
}
func (p *testProvider) RemoveHttpRequestHeader(key string) error {
delete(p.headerMap, key)
return nil
}
const (
ES256Allow string = "eyJhbGciOiJFUzI1NiIsImtpZCI6InAyNTYiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MjAxOTY4NjQwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.hm71YWfjALshUAgyOu-r9W2WBG_zfqIZZacAbc7oIH1r7dbB0sGQn3wKMWMmOzmxX0UyaVZ0KMk-HFTA1hDnBQ"
ES256Expried string = "eyJhbGciOiJFUzI1NiIsImtpZCI6InAyNTYiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MTcwNDA2NzIwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.9AnXd2rZ6FirHZQAoabyL4xZNz0jr-3LmcV4-pFV3JrdtUT4386Mw5Qan125fUB-rZf_ZBlv0Bft2tWY149fyg"
RS256Allow string = "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MjAxOTY4NjQwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.iO0wPY91b_VNGUMZ1n-Ub-SRmEkDQMFLSi77z49tEzll3UZXwmBraP5udM_OPUAdk9ZO3dbb_fOgdcN9V1H9p5kiTr-l-pZTFTJHrPJj8wC519sYRcCk3wrZ9aXR5tNMwOsMdQb7waTBatDQLmHPWzAoTNBc8mwXkRcv1dmJLvsJgxyCl1I9CMOMPq0fYj1NBvaUDIdVSL1o7GGiriD8-0UIOmS72-I3mbaoCIyVb0h3wx7gnIW3zr0yYWaYoiIgmHLag-eEGxHp4-BjtCqcokU4QVMS91qpH7Mkl1iv2WHEkuDQRJ-nLzYGwXb7Dncx9K5tNWHJuZ-DihIU2oT0aA"
RS256Expried string = "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzYSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZm9vIiwiYmFyIl0sImV4cCI6MTcwNDA2NzIwMCwiaXNzIjoiaGlncmVzcy10ZXN0IiwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJoaWdyZXNzLXRlc3QifQ.jqzlhBPk9mmvtTT5aCYf-_5uXXSEU5bQ32fx78XeboCnjR9K1CsI4KYUIkXEX3bk66XJQUeSes7lz3gA4Yzkd-v9oADHTgpKnIxzv_5mD0_afIwEFjcalqVbSvCmro4PessQZDnmU7AIzoo3RPSqbmq8xbPVYUH9I-OO8aUu2ATd1HozgxJH1XnRU8k9KMkVW8XhvJXLKZJmnqe3Tu6pCU_tawFlBfBC4fAhMf0yX2CGE0ABAHubcdiI6JXObQmQQ9Or2a-g2a8g_Bw697PoPOsAn0YpTrHst9GcyTpkbNTAq9X8fc5EM7hiDM1FGeMYcaQTdMnOh4HBhP0p4YEhvA"
JWKs string = "{\"keys\":[{\"kty\":\"EC\",\"kid\":\"p256\",\"crv\":\"P-256\",\"x\":\"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\"y\":\"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"},{\"kty\":\"RSA\",\"kid\":\"rsa\",\"n\":\"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\"e\":\"AQAB\"}]}"
)
const (
consumers = `{
"consumers": [
{
"name": "consumer1",
"issuer": "higress-test",
"jwks": "{\n\"keys\": [\n{\n\"kty\": \"EC\",\n\"kid\": \"p256\",\n\"crv\": \"P-256\",\n\"x\": \"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\n\"y\": \"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"\n},\n{\n\"kty\": \"RSA\",\n\"kid\": \"rsa\",\n\"n\": \"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\n\"e\": \"AQAB\"\n}\n]\n}"
},
{
"name": "consumer_hedaer",
"issuer": "higress-test",
"jwks": "{\n\"keys\": [\n{\n\"kty\": \"EC\",\n\"kid\": \"p256\",\n\"crv\": \"P-256\",\n\"x\": \"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\n\"y\": \"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"\n},\n{\n\"kty\": \"RSA\",\n\"kid\": \"rsa\",\n\"n\": \"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\n\"e\": \"AQAB\"\n}\n]\n}",
"from_headers": [
{
"name": "jwt",
"value_prefix": "Bearer "
}
]
},
{
"name": "consumer_params",
"issuer": "higress-test",
"jwks": "{\n\"keys\": [\n{\n\"kty\": \"EC\",\n\"kid\": \"p256\",\n\"crv\": \"P-256\",\n\"x\": \"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\n\"y\": \"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"\n},\n{\n\"kty\": \"RSA\",\n\"kid\": \"rsa\",\n\"n\": \"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\n\"e\": \"AQAB\"\n}\n]\n}",
"from_params": [
"jwt_token"
]
},
{
"name": "consumer_cookies",
"issuer": "higress-test",
"jwks": "{\n\"keys\": [\n{\n\"kty\": \"EC\",\n\"kid\": \"p256\",\n\"crv\": \"P-256\",\n\"x\": \"GWym652nfByDbs4EzNpGXCkdjG03qFZHulNDHTo3YJU\",\n\"y\": \"5uVg_n-flqRJ5Zhf_aEKS0ow9SddTDgxGduSCgpoAZQ\"\n},\n{\n\"kty\": \"RSA\",\n\"kid\": \"rsa\",\n\"n\": \"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\n\"e\": \"AQAB\"\n}\n]\n}",
"from_cookies": [
"jwt_token"
]
}
]
}`
)
func TestConsumerVerify(t *testing.T) {
log := &testLogger{
T: t,
}
cs := []*config.Consumer{}
c := gjson.Parse(consumers).Get("consumers")
if !c.IsArray() {
t.Error("failed to parse configuration for consumers: consumers is not a array")
return
}
consumerNames := map[string]struct{}{}
for _, v := range c.Array() {
c, err := config.ParseConsumer(v, consumerNames)
if err != nil {
t.Log(err.Error())
continue
}
cs = append(cs, c)
}
if len(cs) == 0 {
t.Error("at least one consumer should be configured for a rule")
return
}
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + ES256Allow}}
_, err := consumerVerify(&config.Consumer{
Name: "consumer1",
JWKs: JWKs,
Issuer: "higress-test",
ClaimsToHeaders: &[]config.ClaimsToHeader{},
FromHeaders: &[]config.FromHeader{{Name: "jwt", ValuePrefix: "Bearer "}},
ClockSkewSeconds: &config.DefaultClockSkewSeconds,
KeepToken: &config.DefaultKeepToken,
}, time.Now(), header, log)
if err != nil {
if v, ok := err.(*ErrDenied); ok {
t.Error(v.msg)
}
}
}
func TestConsumerVerifyWithCachedRemoteJWKs(t *testing.T) {
log := &testLogger{T: t}
cacheRemoteJWKsForTest("consumer-remote", "https://auth.example.com/.well-known/jwks.json", JWKs, time.Now().Add(time.Minute))
defer clearRemoteJWKsCacheForTest()
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + ES256Allow}}
consumer := remoteJWKsVerifyConsumer("https://auth.example.com/.well-known/jwks.json")
_, err := consumerVerify(consumer, time.Now(), header, log)
if err != nil {
if v, ok := err.(*ErrDenied); ok {
t.Error(v.msg)
} else {
t.Error(err)
}
}
}
func TestConsumerVerifyWithRemoteSingleKeyJWKsAllowsMissingKid(t *testing.T) {
log := &testLogger{T: t}
uri := "https://auth.example.com/.well-known/jwks.json"
token, singleKeyJWKs := signedES256TokenWithoutKid(t)
cacheRemoteJWKsForTest("consumer-remote", uri, singleKeyJWKs, time.Now().Add(time.Minute))
defer clearRemoteJWKsCacheForTest()
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + token}}
consumer := remoteJWKsVerifyConsumer(uri)
_, err := consumerVerify(consumer, time.Now(), header, log)
if err != nil {
t.Fatalf("expected remote single-key jwks token without kid to verify, got: %v", err)
}
}
func TestConsumerVerifyWithRemoteMultiKeyJWKsRejectsMissingKidWhenEmptyKidKeyExists(t *testing.T) {
log := &testLogger{T: t}
uri := "https://auth.example.com/.well-known/jwks.json"
token, multiKeyJWKs := signedES256TokenWithoutKidAndMultiKeyJWKsWithEmptyKid(t)
cacheRemoteJWKsForTest("consumer-remote", uri, multiKeyJWKs, time.Now().Add(time.Minute))
defer clearRemoteJWKsCacheForTest()
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + token}}
consumer := remoteJWKsVerifyConsumer(uri)
_, err := consumerVerify(consumer, time.Now(), header, log)
if err == nil {
t.Fatalf("expected remote multi-key jwks token without kid to fail")
}
if !strings.Contains(err.Error(), "kid is required for multi-key remote jwks") {
t.Fatalf("expected multi-key remote jwks missing kid denial, got: %v", err)
}
if isRemoteJWKsCacheMiss(err) {
t.Fatalf("missing kid should be denied without remote jwks refresh")
}
}
func TestConsumerVerifyInlineMissingKidMatchesEmptyKeyID(t *testing.T) {
log := &testLogger{T: t}
token, multiKeyJWKs := signedES256TokenWithoutKidAndLaterEmptyKidJWKs(t)
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + token}}
_, err := consumerVerify(&config.Consumer{
Name: "consumer-inline",
JWKs: multiKeyJWKs,
Issuer: "higress-test",
FromHeaders: &[]config.FromHeader{{Name: "jwt", ValuePrefix: "Bearer "}},
ClockSkewSeconds: &config.DefaultClockSkewSeconds,
KeepToken: &config.DefaultKeepToken,
}, time.Now(), header, log)
if err != nil {
t.Fatalf("expected inline token without kid to match empty KeyID key, got: %v", err)
}
}
func TestConsumerVerifyErrorUsesLogSafeToken(t *testing.T) {
rawToken := "not-a-jwt-secret-token"
_, err := consumerVerify(&config.Consumer{
Name: "consumer1",
JWKs: JWKs,
Issuer: "higress-test",
FromHeaders: &[]config.FromHeader{{Name: "jwt", ValuePrefix: "Bearer "}},
ClockSkewSeconds: &config.DefaultClockSkewSeconds,
KeepToken: &config.DefaultKeepToken,
}, time.Now(), &testProvider{headerMap: map[string]string{"jwt": "Bearer " + rawToken}}, &testLogger{T: t})
if err == nil {
t.Fatalf("expected malformed token to fail")
}
if got, want := err.Error(), "token: "+fmt.Sprint(jwtLogValue(rawToken)); !strings.Contains(got, want) {
t.Fatalf("error should use log-safe jwt value: got %q, want substring %q", got, want)
}
}
func TestJWTLogValueUsesStableHashFormat(t *testing.T) {
rawToken := "not-a-jwt-secret-token"
if got, want := fmt.Sprint(jwtLogValue(rawToken)), "sha256:1258efc316106960"; got != want {
t.Fatalf("unexpected jwt log value: got %q, want %q", got, want)
}
}
func TestConsumerVerifyWithRemoteJWKsReturnsCacheMissOnUnknownKid(t *testing.T) {
log := &testLogger{T: t}
staleJWKs := "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"rsa\",\"n\":\"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\"e\":\"AQAB\"}]}"
cacheRemoteJWKsForTest("consumer-remote", "https://auth.example.com/.well-known/jwks.json", staleJWKs, time.Now().Add(time.Minute))
defer clearRemoteJWKsCacheForTest()
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + ES256Allow}}
consumer := remoteJWKsVerifyConsumer("https://auth.example.com/.well-known/jwks.json")
_, err := consumerVerify(consumer, time.Now(), header, log)
if !isRemoteJWKsCacheMiss(err) {
t.Fatalf("expected remote jwks cache miss for unknown kid, got: %v", err)
}
}
func TestConsumerVerifyWithRemoteJWKsRejectsMissingKid(t *testing.T) {
log := &testLogger{T: t}
uri := "https://auth.example.com/.well-known/jwks.json"
cacheRemoteJWKsForTest("consumer-remote", uri, JWKs, time.Now().Add(time.Minute))
defer clearRemoteJWKsCacheForTest()
tokenWithoutKid := jwtWithHeader(ES256Allow, `{"alg":"ES256","typ":"JWT"}`)
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + tokenWithoutKid}}
consumer := remoteJWKsVerifyConsumer(uri)
_, err := consumerVerify(consumer, time.Now(), header, log)
if err == nil {
t.Fatalf("expected remote jwks token without kid to fail")
}
if !strings.Contains(err.Error(), "kid is required for multi-key remote jwks") {
t.Fatalf("expected multi-key remote jwks missing kid denial, got: %v", err)
}
if isRemoteJWKsCacheMiss(err) {
t.Fatalf("missing kid should be denied without remote jwks refresh")
}
}
func TestConsumerVerifyWithRemoteJWKsAllowsUnknownKidRefreshAfterRecentFetch(t *testing.T) {
log := &testLogger{T: t}
uri := "https://auth.example.com/.well-known/jwks.json"
staleJWKs := "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"rsa\",\"n\":\"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\"e\":\"AQAB\"}]}"
now := time.Now()
cacheRemoteJWKsFetchedAtForTest("consumer-remote", uri, staleJWKs, now.Add(-time.Second))
defer clearRemoteJWKsCacheForTest()
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + ES256Allow}}
consumer := remoteJWKsVerifyConsumer(uri)
_, err := consumerVerify(consumer, now, header, log)
if !isRemoteJWKsCacheMiss(err) {
t.Fatalf("expected cache fetched before this request not to throttle unknown kid refresh, got: %v", err)
}
}
func TestConsumerVerifyWithRemoteJWKsRejectsUnknownKidAfterRequestRefresh(t *testing.T) {
log := &testLogger{T: t}
uri := "https://auth.example.com/.well-known/jwks.json"
staleJWKs := "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"rsa\",\"n\":\"pFKAKJ0V3vFwGTvBSHbPwrNdvPyr-zMTh7Y9IELFIMNUQfG9_d2D1wZcrX5CPvtEISHin3GdPyfqEX6NjPyqvCLFTuNh80-r5Mvld-A5CHwITZXz5krBdqY5Z0wu64smMbzst3HNxHbzLQvHUY-KS6hceOB84d9B4rhkIJEEAWxxIA7yPJYjYyIC_STpPddtJkkweVvoa0m0-_FQkDFsbRS0yGgMNG4-uc7qLIU4kSwMQWcw1Rwy39LUDP4zNzuZABbWsDDBsMlVUaszRdKIlk5AQ-Fkah3E247dYGUQjSQ0N3dFLlMDv_e62BT3IBXGLg7wvGosWFNT_LpIenIW6Q\",\"e\":\"AQAB\"}]}"
now := time.Now()
cacheRemoteJWKsFetchedAtForTest("consumer-remote", uri, staleJWKs, now.Add(-time.Second))
defer clearRemoteJWKsCacheForTest()
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + ES256Allow}}
consumer := remoteJWKsVerifyConsumer(uri)
if _, err := consumerVerify(consumer, now, header, log); !isRemoteJWKsCacheMiss(err) {
t.Fatalf("expected first unknown kid to request remote jwks refresh, got: %v", err)
}
cacheRemoteJWKsFetchedAtForTest("consumer-remote", uri, staleJWKs, now.Add(time.Millisecond))
_, err := consumerVerify(consumer, now, header, log)
if isRemoteJWKsCacheMiss(err) {
t.Fatalf("unknown kid should be denied after this request already refreshed remote jwks")
}
if err == nil {
t.Fatalf("expected unknown kid to fail")
}
}
func TestConsumerVerifyWithRemoteJWKsSkipsIssuerMismatchBeforeFetch(t *testing.T) {
log := &testLogger{T: t}
defer clearRemoteJWKsCacheForTest()
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + ES256Allow}}
consumer := remoteJWKsVerifyConsumer("https://auth.example.com/.well-known/jwks.json")
consumer.Issuer = "other-issuer"
_, err := consumerVerify(consumer, time.Now(), header, log)
if isRemoteJWKsCacheMiss(err) {
t.Fatalf("issuer mismatch should not trigger remote jwks fetch")
}
if err == nil {
t.Fatalf("expected issuer mismatch to fail")
}
}
func TestConsumerVerifyWithRemoteJWKsReportsUnsafeClaimsParseError(t *testing.T) {
log := &testLogger{T: t}
defer clearRemoteJWKsCacheForTest()
tokenWithMalformedPayload := jwtWithPayload(ES256Allow, "not-json")
header := &testProvider{headerMap: map[string]string{"jwt": "Bearer " + tokenWithMalformedPayload}}
consumer := remoteJWKsVerifyConsumer("https://auth.example.com/.well-known/jwks.json")
_, err := consumerVerify(consumer, time.Now(), header, log)
if isRemoteJWKsCacheMiss(err) {
t.Fatalf("malformed unsafe claims should not trigger remote jwks fetch")
}
if err == nil || !strings.Contains(err.Error(), "failed to parse unsafe claims") {
t.Fatalf("expected unsafe claims parse error, got: %v", err)
}
}
func remoteJWKsVerifyConsumer(uri string) *config.Consumer {
consumer := remoteJWKsTestConsumer("consumer-remote", uri)
consumer.Issuer = "higress-test"
consumer.ClaimsToHeaders = &[]config.ClaimsToHeader{}
consumer.FromHeaders = &[]config.FromHeader{{Name: "jwt", ValuePrefix: "Bearer "}}
consumer.ClockSkewSeconds = &config.DefaultClockSkewSeconds
consumer.KeepToken = &config.DefaultKeepToken
return consumer
}
func TestExtractTokenRemovesQueryParamWhenKeepTokenFalse(t *testing.T) {
header := &testProvider{headerMap: map[string]string{":path": "/resource?access_token=token-value&keep=1"}}
token := extractToken(false, &config.Consumer{
FromParams: &[]string{"access_token"},
}, header, &testLogger{T: t})
if token != "token-value" {
t.Fatalf("unexpected token: %q", token)
}
if got := header.headerMap[":path"]; got != "/resource?keep=1" {
t.Fatalf("expected token query param to be removed, got: %q", got)
}
}
func TestExtractFromParamsParseErrorUsesStaticLogMessage(t *testing.T) {
header := &testProvider{headerMap: map[string]string{":path": "%zz?access_token=secret-token"}}
log := &recordingLogger{}
token := extractFromParams(true, []string{"access_token"}, header, log)
if token != "" {
t.Fatalf("expected malformed path to return no token, got: %q", token)
}
if got, want := strings.Join(log.entries, "\n"), "failed to parse path: invalid request path"; got != want {
t.Fatalf("unexpected path parse error log: got %q, want %q", got, want)
}
}
func jwtWithHeader(token, headerJSON string) string {
parts := strings.Split(token, ".")
parts[0] = base64.RawURLEncoding.EncodeToString([]byte(headerJSON))
return strings.Join(parts, ".")
}
func jwtWithPayload(token, payload string) string {
parts := strings.Split(token, ".")
parts[1] = base64.RawURLEncoding.EncodeToString([]byte(payload))
return strings.Join(parts, ".")
}
func signedES256TokenWithoutKid(t *testing.T) (string, string) {
t.Helper()
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("failed to generate key: %v", err)
}
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.ES256,
Key: privateKey,
}, (&jose.SignerOptions{}).WithType("JWT"))
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
claims := jwt.Claims{
Issuer: "higress-test",
Subject: "higress-test",
Audience: []string{"foo", "bar"},
Expiry: jwt.NewNumericDate(time.Date(2034, 1, 1, 0, 0, 0, 0, time.UTC)),
NotBefore: jwt.NewNumericDate(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),
}
token, err := jwt.Signed(signer).Claims(claims).CompactSerialize()
if err != nil {
t.Fatalf("failed to sign token: %v", err)
}
jwks, err := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{
Key: &privateKey.PublicKey,
KeyID: "p256",
}}})
if err != nil {
t.Fatalf("failed to marshal jwks: %v", err)
}
return token, string(jwks)
}
func signedES256TokenWithoutKidAndMultiKeyJWKsWithEmptyKid(t *testing.T) (string, string) {
t.Helper()
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("failed to generate key: %v", err)
}
otherKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("failed to generate second key: %v", err)
}
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.ES256,
Key: privateKey,
}, (&jose.SignerOptions{}).WithType("JWT"))
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
claims := jwt.Claims{
Issuer: "higress-test",
Subject: "higress-test",
Audience: []string{"foo", "bar"},
Expiry: jwt.NewNumericDate(time.Date(2034, 1, 1, 0, 0, 0, 0, time.UTC)),
NotBefore: jwt.NewNumericDate(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),
}
token, err := jwt.Signed(signer).Claims(claims).CompactSerialize()
if err != nil {
t.Fatalf("failed to sign token: %v", err)
}
jwks, err := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{
{Key: &privateKey.PublicKey},
{Key: &otherKey.PublicKey, KeyID: "other"},
}})
if err != nil {
t.Fatalf("failed to marshal jwks: %v", err)
}
return token, string(jwks)
}
func signedES256TokenWithoutKidAndLaterEmptyKidJWKs(t *testing.T) (string, string) {
t.Helper()
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("failed to generate key: %v", err)
}
otherKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("failed to generate second key: %v", err)
}
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.ES256,
Key: privateKey,
}, (&jose.SignerOptions{}).WithType("JWT"))
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
claims := jwt.Claims{
Issuer: "higress-test",
Subject: "higress-test",
Audience: []string{"foo", "bar"},
Expiry: jwt.NewNumericDate(time.Date(2034, 1, 1, 0, 0, 0, 0, time.UTC)),
NotBefore: jwt.NewNumericDate(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),
}
token, err := jwt.Signed(signer).Claims(claims).CompactSerialize()
if err != nil {
t.Fatalf("failed to sign token: %v", err)
}
jwks, err := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{
{Key: &otherKey.PublicKey, KeyID: "other"},
{Key: &privateKey.PublicKey},
}})
if err != nil {
t.Fatalf("failed to marshal jwks: %v", err)
}
return token, string(jwks)
}