fix verification cache for redis-backed challenges
This commit is contained in:
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user