parseWithHMAC_centralized_audit_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. package middleware
  2. import (
  3. "crypto/hmac"
  4. "crypto/sha256"
  5. "encoding/base64"
  6. "encoding/json"
  7. "testing"
  8. "time"
  9. "perms-system-server/internal/consts"
  10. "github.com/golang-jwt/jwt/v4"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/require"
  13. )
  14. // ---------------------------------------------------------------------------
  15. // 覆盖目标:审计 L-N1 修复 —— ParseWithHMAC 上移到 middleware 层作为唯一入口。
  16. // 本测试直接在 middleware 包内验证:
  17. // (1) 正常 HS256 token 放行
  18. // (2) alg=RS256(公钥→HMAC 共享密钥混淆)显式拒绝
  19. // (3) alg=none 拒绝
  20. // (4) 错误 secret 签名拒绝
  21. // (5) 非 Claims 结构的 claims 同样正确解析(保证函数与具体 claims 类型解耦)
  22. // ---------------------------------------------------------------------------
  23. const ln1Secret = "ln1-centralized-secret"
  24. func b64urlLN1(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) }
  25. func forgeTokenLN1(t *testing.T, alg string, claims any, signKey string) string {
  26. t.Helper()
  27. header := map[string]string{"alg": alg, "typ": "JWT"}
  28. hBytes, err := json.Marshal(header)
  29. require.NoError(t, err)
  30. pBytes, err := json.Marshal(claims)
  31. require.NoError(t, err)
  32. signingInput := b64urlLN1(hBytes) + "." + b64urlLN1(pBytes)
  33. mac := hmac.New(sha256.New, []byte(signKey))
  34. mac.Write([]byte(signingInput))
  35. return signingInput + "." + b64urlLN1(mac.Sum(nil))
  36. }
  37. func validAccessClaimsLN1() Claims {
  38. now := time.Now()
  39. return Claims{
  40. TokenType: consts.TokenTypeAccess,
  41. UserId: 42,
  42. Username: "ln1_u",
  43. ProductCode: "ln1_p",
  44. MemberType: consts.MemberTypeAdmin,
  45. TokenVersion: 0,
  46. RegisteredClaims: jwt.RegisteredClaims{
  47. ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)),
  48. IssuedAt: jwt.NewNumericDate(now),
  49. },
  50. }
  51. }
  52. // TC-1003: L-N1 —— middleware 层 ParseWithHMAC 能正确解析合法 HS256 token。
  53. func TestMiddlewareParseWithHMAC_LN1_HS256Valid(t *testing.T) {
  54. signed := jwt.NewWithClaims(jwt.SigningMethodHS256, validAccessClaimsLN1())
  55. tok, err := signed.SignedString([]byte(ln1Secret))
  56. require.NoError(t, err)
  57. parsed, err := ParseWithHMAC(tok, ln1Secret, &Claims{})
  58. require.NoError(t, err)
  59. require.True(t, parsed.Valid)
  60. claims, ok := parsed.Claims.(*Claims)
  61. require.True(t, ok)
  62. assert.Equal(t, int64(42), claims.UserId)
  63. assert.Equal(t, consts.TokenTypeAccess, claims.TokenType)
  64. }
  65. // TC-1004: L-N1 —— middleware 层 ParseWithHMAC 必须拒绝 alg=RS256 伪造(公钥→HMAC 混淆)。
  66. func TestMiddlewareParseWithHMAC_LN1_RS256HeaderRejected(t *testing.T) {
  67. forged := forgeTokenLN1(t, "RS256", validAccessClaimsLN1(), ln1Secret)
  68. _, err := ParseWithHMAC(forged, ln1Secret, &Claims{})
  69. require.Error(t, err, "L-N1:必须拒绝 alg=RS256 伪造 token")
  70. assert.Contains(t, err.Error(), "unexpected signing method",
  71. "L-N1:HMAC 断言失败必须产出可审计错误信息,方便 SOC 定位攻击尝试")
  72. }
  73. // TC-1005: L-N1 —— middleware 层 ParseWithHMAC 必须拒绝 alg=none。
  74. func TestMiddlewareParseWithHMAC_LN1_AlgNoneRejected(t *testing.T) {
  75. header := map[string]string{"alg": "none", "typ": "JWT"}
  76. hBytes, _ := json.Marshal(header)
  77. pBytes, _ := json.Marshal(validAccessClaimsLN1())
  78. forged := b64urlLN1(hBytes) + "." + b64urlLN1(pBytes) + "."
  79. _, err := ParseWithHMAC(forged, ln1Secret, &Claims{})
  80. require.Error(t, err, "L-N1:alg=none 不可通过 HMAC 唯一入口")
  81. }
  82. // TC-1006: L-N1 —— 错误 secret 签发的合法结构 HS256 token 必须被拒绝。
  83. func TestMiddlewareParseWithHMAC_LN1_WrongSecretRejected(t *testing.T) {
  84. signed := jwt.NewWithClaims(jwt.SigningMethodHS256, validAccessClaimsLN1())
  85. tok, err := signed.SignedString([]byte("attacker-guess"))
  86. require.NoError(t, err)
  87. _, err = ParseWithHMAC(tok, ln1Secret, &Claims{})
  88. require.Error(t, err, "L-N1:签名校验失败必须 fail-close")
  89. }
  90. // TC-1007: L-N1 —— ParseWithHMAC 可以为任意 jwt.Claims 结构体工作(不绑 Claims 类型),
  91. // 保证 gRPC VerifyToken、RefreshToken、HTTP 中间件等所有调用点可以共用该入口。
  92. func TestMiddlewareParseWithHMAC_LN1_ArbitraryClaimsType(t *testing.T) {
  93. type customClaims struct {
  94. Role string `json:"role"`
  95. jwt.RegisteredClaims
  96. }
  97. c := customClaims{
  98. Role: "admin",
  99. RegisteredClaims: jwt.RegisteredClaims{
  100. ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
  101. IssuedAt: jwt.NewNumericDate(time.Now()),
  102. },
  103. }
  104. signed := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
  105. tok, err := signed.SignedString([]byte(ln1Secret))
  106. require.NoError(t, err)
  107. parsed, err := ParseWithHMAC(tok, ln1Secret, &customClaims{})
  108. require.NoError(t, err)
  109. parsedClaims, ok := parsed.Claims.(*customClaims)
  110. require.True(t, ok)
  111. assert.Equal(t, "admin", parsedClaims.Role,
  112. "L-N1:唯一入口必须对任意 claims 类型解耦,保证所有调用方可以复用")
  113. }