fix verification cache for redis-backed challenges

This commit is contained in:
richjohnrambo
2026-09-23 09:02:56 +08:00
parent e37b52c3c0
commit 137d6fb6da
4 changed files with 134 additions and 6 deletions
+2 -2
View File
@@ -379,7 +379,7 @@ func (a *audit) run() {
func (a *audit) register(chapter, id, username, phone, password string, primary bool) {
var challenge, verification string
a.testHTTP(chapter, id+"-code", "注册发码 "+username, "POST", "/v1/im/auth/verification-codes", map[string]any{"scene": "register", "phone": phone}, "", true, []int{200}, func(r imResp) string {
a.testHTTP(chapter, id+"-code", "注册发码 "+username, "POST", "/v1/im/auth/verification-codes", map[string]any{"scene": "register", "registration_type": 1, "phone": phone}, "", true, []int{200}, func(r imResp) string {
challenge = asString(r.Data["challenge_id"])
return "challenge_id=" + mask(challenge)
})
@@ -404,7 +404,7 @@ func (a *audit) register(chapter, id, username, phone, password string, primary
func (a *audit) registerThird(chapter, id, username, phone, password string) {
var challenge, verification string
a.testHTTP(chapter, id+"-code", "入群用户注册发码 "+username, "POST", "/v1/im/auth/verification-codes", map[string]any{"scene": "register", "phone": phone}, "", true, []int{200}, func(r imResp) string {
a.testHTTP(chapter, id+"-code", "入群用户注册发码 "+username, "POST", "/v1/im/auth/verification-codes", map[string]any{"scene": "register", "registration_type": 1, "phone": phone}, "", true, []int{200}, func(r imResp) string {
challenge = asString(r.Data["challenge_id"])
return "challenge_id=" + mask(challenge)
})
+1 -1
View File
@@ -667,7 +667,7 @@ func (a *audit) register(username, phone, password string) (string, string, erro
}
func (a *audit) verificationCode(phone string) (string, error) {
status, r, err := a.call("POST", "/v1/im/auth/verification-codes", map[string]any{"scene": "register", "phone": phone}, "")
status, r, err := a.call("POST", "/v1/im/auth/verification-codes", map[string]any{"scene": "register", "registration_type": 1, "phone": phone}, "")
if err != nil {
return "", err
}
+76 -3
View File
@@ -4286,6 +4286,79 @@ type VerificationPersistenceInterface interface {
type verificationMapper struct{}
type verificationCodeCache struct {
ID int64 `json:"id,omitempty"`
TenantID types.TenantID `json:"tenant_id"`
ChallengeID string `json:"challenge_id"`
Scene string `json:"scene"`
Action string `json:"action,omitempty"`
PhoneHash string `json:"phone_hash"`
PhoneMask string `json:"phone_mask,omitempty"`
TargetUser types.Uid `json:"target_user,omitempty"`
CodeHash string `json:"code_hash"`
VerificationTokenHash *string `json:"verification_token_hash,omitempty"`
State types.VerificationState `json:"state"`
Attempts int `json:"attempts"`
MaxAttempts int `json:"max_attempts"`
ExpiresAt time.Time `json:"expires_at"`
ResendAfter time.Time `json:"resend_after"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
ConsumedAt *time.Time `json:"consumed_at,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
func verificationCodeCacheFromType(vc *types.VerificationCode) verificationCodeCache {
if vc == nil {
return verificationCodeCache{}
}
return verificationCodeCache{
ID: vc.ID,
TenantID: vc.TenantID,
ChallengeID: vc.ChallengeID,
Scene: vc.Scene,
Action: vc.Action,
PhoneHash: vc.PhoneHash,
PhoneMask: vc.PhoneMask,
TargetUser: vc.TargetUser,
CodeHash: vc.CodeHash,
VerificationTokenHash: vc.VerificationTokenHash,
State: vc.State,
Attempts: vc.Attempts,
MaxAttempts: vc.MaxAttempts,
ExpiresAt: vc.ExpiresAt,
ResendAfter: vc.ResendAfter,
VerifiedAt: vc.VerifiedAt,
ConsumedAt: vc.ConsumedAt,
CreatedAt: vc.CreatedAt,
UpdatedAt: vc.UpdatedAt,
}
}
func (vc verificationCodeCache) toType() *types.VerificationCode {
return &types.VerificationCode{
ID: vc.ID,
TenantID: vc.TenantID,
ChallengeID: vc.ChallengeID,
Scene: vc.Scene,
Action: vc.Action,
PhoneHash: vc.PhoneHash,
PhoneMask: vc.PhoneMask,
TargetUser: vc.TargetUser,
CodeHash: vc.CodeHash,
VerificationTokenHash: vc.VerificationTokenHash,
State: vc.State,
Attempts: vc.Attempts,
MaxAttempts: vc.MaxAttempts,
ExpiresAt: vc.ExpiresAt,
ResendAfter: vc.ResendAfter,
VerifiedAt: vc.VerifiedAt,
ConsumedAt: vc.ConsumedAt,
CreatedAt: vc.CreatedAt,
UpdatedAt: vc.UpdatedAt,
}
}
// Verifications is a singleton for product verification challenges.
var Verifications VerificationPersistenceInterface
@@ -4537,7 +4610,7 @@ func (verificationMapper) Create(tenantID types.TenantID, vc *types.Verification
}
func (verificationMapper) Get(tenantID types.TenantID, challengeID string) (*types.VerificationCode, error) {
var vc types.VerificationCode
var vc verificationCodeCache
ok, err := GetCachedObject(tenantID, cacheNamespaceVerification, challengeID, &vc)
if err != nil {
return nil, err
@@ -4545,7 +4618,7 @@ func (verificationMapper) Get(tenantID types.TenantID, challengeID string) (*typ
if !ok {
return nil, types.ErrNotFound
}
return &vc, nil
return vc.toType(), nil
}
func (verificationMapper) GetActive(tenantID types.TenantID, scene, phoneHash string, now time.Time) (*types.VerificationCode, error) {
@@ -4611,7 +4684,7 @@ func cacheVerificationCode(tenantID types.TenantID, vc *types.VerificationCode)
if vc == nil {
return nil
}
if err := CacheObject(tenantID, cacheNamespaceVerification, vc.ChallengeID, vc); err != nil {
if err := CacheObject(tenantID, cacheNamespaceVerification, vc.ChallengeID, verificationCodeCacheFromType(vc)); err != nil {
return err
}
if vc.State == types.VerificationStatePending {
+55
View File
@@ -2,7 +2,9 @@ package store
import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/tinode/chat/server/store/types"
)
@@ -41,3 +43,56 @@ func TestValidateMessageHeadersProductContract(t *testing.T) {
}
}
}
func TestVerificationCodeCachePreservesInternalFields(t *testing.T) {
tokenHash := "verify-token-hash"
now := time.Now().UTC().Round(time.Millisecond)
vc := &types.VerificationCode{
ID: 11,
TenantID: 7,
ChallengeID: "challenge-1",
Scene: "register",
Action: "signup",
PhoneHash: "phone-hash",
PhoneMask: "+861****000",
CodeHash: "code-hash",
VerificationTokenHash: &tokenHash,
State: types.VerificationStateVerified,
Attempts: 2,
MaxAttempts: 5,
ExpiresAt: now.Add(5 * time.Minute),
ResendAfter: now.Add(time.Minute),
VerifiedAt: &now,
CreatedAt: now,
UpdatedAt: now,
}
publicPayload, err := json.Marshal(vc)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(publicPayload), "code-hash") || strings.Contains(string(publicPayload), "phone-hash") {
t.Fatalf("public verification JSON leaked internal fields: %s", publicPayload)
}
cachePayload, err := json.Marshal(verificationCodeCacheFromType(vc))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(cachePayload), `"code_hash":"code-hash"`) ||
!strings.Contains(string(cachePayload), `"phone_hash":"phone-hash"`) ||
!strings.Contains(string(cachePayload), `"max_attempts":5`) {
t.Fatalf("cache verification JSON missing internal fields: %s", cachePayload)
}
var cached verificationCodeCache
if err := json.Unmarshal(cachePayload, &cached); err != nil {
t.Fatal(err)
}
got := cached.toType()
if got.CodeHash != vc.CodeHash || got.PhoneHash != vc.PhoneHash ||
got.State != vc.State || got.Attempts != vc.Attempts || got.MaxAttempts != vc.MaxAttempts ||
got.VerificationTokenHash == nil || *got.VerificationTokenHash != tokenHash {
t.Fatalf("cache round trip lost fields: got %#v", got)
}
}