diff --git a/cmd/imtestaudit/main.go b/cmd/imtestaudit/main.go index 1638316..f23fe80 100644 --- a/cmd/imtestaudit/main.go +++ b/cmd/imtestaudit/main.go @@ -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) }) diff --git a/cmd/imwsaudit/main.go b/cmd/imwsaudit/main.go index 8f6f37f..fc15ad9 100644 --- a/cmd/imwsaudit/main.go +++ b/cmd/imwsaudit/main.go @@ -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 } diff --git a/server/store/store.go b/server/store/store.go index 93fc923..69a1275 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -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 { diff --git a/server/store/tenant_store_test.go b/server/store/tenant_store_test.go index a4f9170..7083ade 100644 --- a/server/store/tenant_store_test.go +++ b/server/store/tenant_store_test.go @@ -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) + } +}