You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
138 lines
4.7 KiB
138 lines
4.7 KiB
package mcp
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// 签发在 user 模块、校验在 mcp 模块,两边隔着一个进程,
|
|
// 写错了不会编译报错,只会在真机上静默拒绝(或更糟:静默放行)。
|
|
// 这组用例把两边的口径钉在一起。
|
|
|
|
const testMcpKey = "test-mcp-key"
|
|
|
|
// signLikeUserModule 复刻 user/api_getmcptoken.go 的签发方式。
|
|
// ⚠️ 改那边的 claims 结构时,这里要一起改,否则这组用例就失去意义了。
|
|
func signLikeUserModule(t *testing.T, uid, audience string, ttl time.Duration, key string) string {
|
|
t.Helper()
|
|
claims := &jwt.RegisteredClaims{
|
|
ID: uid,
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
}
|
|
if audience != "" {
|
|
claims.Audience = jwt.ClaimStrings{audience}
|
|
}
|
|
s, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(key))
|
|
if err != nil {
|
|
t.Fatalf("签发失败: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestParseUIDWithAudience(t *testing.T) {
|
|
const uid = "2095078768153460736"
|
|
|
|
t.Run("正常令牌能解出uid", func(t *testing.T) {
|
|
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, testMcpKey)
|
|
got, err := parseUIDWithAudience(tok, []byte(testMcpKey), mcpTokenAudience)
|
|
if err != nil {
|
|
t.Fatalf("不该失败: %v", err)
|
|
}
|
|
if got != uid {
|
|
t.Fatalf("uid 不对: got=%s want=%s", got, uid)
|
|
}
|
|
})
|
|
|
|
t.Run("换了密钥必须拒", func(t *testing.T) {
|
|
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, "another-key")
|
|
if _, err := parseUIDWithAudience(tok, []byte(testMcpKey), mcpTokenAudience); err == nil {
|
|
t.Fatal("别人签的令牌居然通过了")
|
|
}
|
|
})
|
|
|
|
t.Run("用途不符必须拒", func(t *testing.T) {
|
|
// 这条守的是「将来若有别处也用 McpTokenKey 签东西,别被 MCP 一并接受」
|
|
tok := signLikeUserModule(t, uid, "someother", time.Hour, testMcpKey)
|
|
if _, err := parseUIDWithAudience(tok, []byte(testMcpKey), mcpTokenAudience); err == nil {
|
|
t.Fatal("用途不符的令牌居然通过了")
|
|
}
|
|
})
|
|
|
|
t.Run("没有用途标记必须拒", func(t *testing.T) {
|
|
tok := signLikeUserModule(t, uid, "", time.Hour, testMcpKey)
|
|
if _, err := parseUIDWithAudience(tok, []byte(testMcpKey), mcpTokenAudience); err == nil {
|
|
t.Fatal("没有 aud 的令牌居然通过了")
|
|
}
|
|
})
|
|
|
|
t.Run("过期必须拒", func(t *testing.T) {
|
|
tok := signLikeUserModule(t, uid, mcpTokenAudience, -time.Minute, testMcpKey)
|
|
if _, err := parseUIDWithAudience(tok, []byte(testMcpKey), mcpTokenAudience); err == nil {
|
|
t.Fatal("过期令牌居然通过了")
|
|
}
|
|
})
|
|
|
|
t.Run("没有过期时间必须拒", func(t *testing.T) {
|
|
// 没有 exp 的令牌等同长期有效,这套「短时效」的前提就没了
|
|
s, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &jwt.RegisteredClaims{
|
|
ID: uid,
|
|
Audience: jwt.ClaimStrings{mcpTokenAudience},
|
|
}).SignedString([]byte(testMcpKey))
|
|
if err != nil {
|
|
t.Fatalf("签发失败: %v", err)
|
|
}
|
|
if _, err := parseUIDWithAudience(s, []byte(testMcpKey), mcpTokenAudience); err == nil {
|
|
t.Fatal("没有 exp 的令牌居然通过了")
|
|
}
|
|
})
|
|
|
|
t.Run("Bearer前缀要能兼容", func(t *testing.T) {
|
|
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, testMcpKey)
|
|
if _, err := parseUIDWithAudience("Bearer "+tok, []byte(testMcpKey), mcpTokenAudience); err != nil {
|
|
t.Fatalf("带 Bearer 前缀不该失败: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestResolveUIDWithToken(t *testing.T) {
|
|
const uid = "2095078768153460736"
|
|
m := &Mcp{}
|
|
m.options = &Options{McpTokenKey: testMcpKey}
|
|
|
|
t.Run("参数令牌可作为身份来源", func(t *testing.T) {
|
|
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, testMcpKey)
|
|
got, err := m.ResolveUIDWithToken(t.Context(), "", tok)
|
|
if err != nil {
|
|
t.Fatalf("不该失败: %v", err)
|
|
}
|
|
if got != uid {
|
|
t.Fatalf("uid 不对: %s", got)
|
|
}
|
|
})
|
|
|
|
t.Run("参数uid与令牌不符必须拒", func(t *testing.T) {
|
|
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, testMcpKey)
|
|
if _, err := m.ResolveUIDWithToken(t.Context(), "9999", tok); err == nil {
|
|
t.Fatal("冒充别人居然通过了")
|
|
}
|
|
})
|
|
|
|
t.Run("只给裸uid不给令牌必须拒", func(t *testing.T) {
|
|
// 这条是整层鉴权的底线:裸 uid 谁都能填,绝不能作为身份来源
|
|
if _, err := m.ResolveUIDWithToken(t.Context(), uid, ""); err == nil {
|
|
t.Fatal("只凭一个 uid 参数就放行了")
|
|
}
|
|
})
|
|
|
|
t.Run("没配密钥时带令牌也要拒", func(t *testing.T) {
|
|
empty := &Mcp{}
|
|
empty.options = &Options{}
|
|
tok := signLikeUserModule(t, uid, mcpTokenAudience, time.Hour, testMcpKey)
|
|
if _, err := empty.ResolveUIDWithToken(t.Context(), "", tok); err == nil {
|
|
t.Fatal("密钥没配却放行了")
|
|
}
|
|
})
|
|
}
|
|
|