diff --git a/cmd/imtestaudit/main.go b/cmd/imtestaudit/main.go index f23fe80..b1d937c 100644 --- a/cmd/imtestaudit/main.go +++ b/cmd/imtestaudit/main.go @@ -196,7 +196,7 @@ func (a *audit) run() { a.testHTTP("5", "5.qr-resolve", "二维码解析", "POST", "/v1/im/qrcodes/resolve", map[string]any{"token": a.meQR, "purpose": a.meQRPurpose}, a.tokenB, true, []int{200}, nil) } - a.testHTTP("6", "6.create", "创建群", "POST", "/v1/im/groups", map[string]any{"name": "联调群" + suffix, "description": "自动化测试群", "member_user_ids": []string{a.userB}, "avatar_file_id": ""}, a.tokenA, true, []int{200}, func(r imResp) string { + a.testHTTP("6", "6.create", "创建群", "POST", "/v1/im/groups", map[string]any{"name": "auditgroup" + suffix, "description": "audit group", "member_user_ids": []string{a.userB}, "avatar_file_id": ""}, a.tokenA, true, []int{200}, func(r imResp) string { a.groupTopic = asString(r.Data["topic"]) a.groupVer = asInt(r.Data["version"]) return fmt.Sprintf("topic=%s version=%d", a.groupTopic, a.groupVer) @@ -446,13 +446,14 @@ func (a *audit) wsRegister(username, phone, password, challenge, verification st accID := "acc-" + username secret := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) err = conn.WriteJSON(map[string]any{"acc": map[string]any{ - "id": accID, - "user": "new", - "scheme": "basic", - "username": username, - "phone": phone, - "secret": secret, - "login": true, + "id": accID, + "user": "new", + "scheme": "basic", + "username": username, + "phone": phone, + "registration_type": 1, + "secret": secret, + "login": true, "cred": []map[string]any{{ "meth": "tel", "val": phone, diff --git a/cmd/imwsaudit/main.go b/cmd/imwsaudit/main.go index fc15ad9..8c17486 100644 --- a/cmd/imwsaudit/main.go +++ b/cmd/imwsaudit/main.go @@ -637,13 +637,14 @@ func (a *audit) register(username, phone, password string) (string, string, erro id := "acc-" + username secret := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) if err = c.write(map[string]any{"acc": map[string]any{ - "id": id, - "user": "new", - "scheme": "basic", - "username": username, - "phone": phone, - "secret": secret, - "login": true, + "id": id, + "user": "new", + "scheme": "basic", + "username": username, + "phone": phone, + "registration_type": 1, + "secret": secret, + "login": true, "cred": []map[string]any{{ "meth": "tel", "val": phone, @@ -724,8 +725,8 @@ func (a *audit) createFriendship() (string, error) { func (a *audit) createGroup(suffix string) (string, error) { status, r, err := a.call("POST", "/v1/im/groups", map[string]any{ - "name": "WS协议群" + suffix, - "description": "WS audit group", + "name": "auditgroup" + suffix, + "description": "audit group", "member_user_ids": []string{a.userB}, "avatar_file_id": "", }, a.tokenA) @@ -827,9 +828,10 @@ func (a *audit) openBasicLoggedIn(name, username, password string) (*wsClient, e loginID := "login-" + strings.ToLower(name) secret := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) if err = c.write(map[string]any{"login": map[string]any{ - "id": loginID, - "scheme": "basic", - "secret": secret, + "id": loginID, + "scheme": "basic", + "login_type": 1, + "secret": secret, }}); err != nil { _ = c.conn.Close() return nil, err diff --git a/server/store/store.go b/server/store/store.go index 69a1275..20c72f5 100644 --- a/server/store/store.go +++ b/server/store/store.go @@ -4294,7 +4294,7 @@ type verificationCodeCache struct { Action string `json:"action,omitempty"` PhoneHash string `json:"phone_hash"` PhoneMask string `json:"phone_mask,omitempty"` - TargetUser types.Uid `json:"target_user,omitempty"` + TargetUser string `json:"target_user,omitempty"` CodeHash string `json:"code_hash"` VerificationTokenHash *string `json:"verification_token_hash,omitempty"` State types.VerificationState `json:"state"` @@ -4320,7 +4320,7 @@ func verificationCodeCacheFromType(vc *types.VerificationCode) verificationCodeC Action: vc.Action, PhoneHash: vc.PhoneHash, PhoneMask: vc.PhoneMask, - TargetUser: vc.TargetUser, + TargetUser: vc.TargetUser.UserId(), CodeHash: vc.CodeHash, VerificationTokenHash: vc.VerificationTokenHash, State: vc.State, @@ -4344,7 +4344,7 @@ func (vc verificationCodeCache) toType() *types.VerificationCode { Action: vc.Action, PhoneHash: vc.PhoneHash, PhoneMask: vc.PhoneMask, - TargetUser: vc.TargetUser, + TargetUser: types.ParseUserId(vc.TargetUser), CodeHash: vc.CodeHash, VerificationTokenHash: vc.VerificationTokenHash, State: vc.State, @@ -4405,6 +4405,58 @@ type UserSecurityPersistenceInterface interface { type userSecurityMapper struct{} +type actionTokenCache struct { + ID int64 `json:"id,omitempty"` + TenantID types.TenantID `json:"tenant_id"` + UserID string `json:"user_id"` + TokenID string `json:"token_id"` + Action string `json:"action"` + Target string `json:"target,omitempty"` + TokenHash string `json:"token_hash"` + State int16 `json:"state"` + ExpiresAt time.Time `json:"expires_at"` + ConsumedAt *time.Time `json:"consumed_at,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` +} + +func actionTokenCacheFromType(token *types.ActionToken) actionTokenCache { + if token == nil { + return actionTokenCache{} + } + return actionTokenCache{ + ID: token.ID, + TenantID: token.TenantID, + UserID: token.UserID.UserId(), + TokenID: token.TokenID, + Action: token.Action, + Target: token.Target, + TokenHash: token.TokenHash, + State: token.State, + ExpiresAt: token.ExpiresAt, + ConsumedAt: token.ConsumedAt, + CreatedAt: token.CreatedAt, + UpdatedAt: token.UpdatedAt, + } +} + +func (token actionTokenCache) toType() *types.ActionToken { + return &types.ActionToken{ + ID: token.ID, + TenantID: token.TenantID, + UserID: types.ParseUserId(token.UserID), + TokenID: token.TokenID, + Action: token.Action, + Target: token.Target, + TokenHash: token.TokenHash, + State: token.State, + ExpiresAt: token.ExpiresAt, + ConsumedAt: token.ConsumedAt, + CreatedAt: token.CreatedAt, + UpdatedAt: token.UpdatedAt, + } +} + // Security is a singleton for user security metadata. var Security UserSecurityPersistenceInterface @@ -4461,7 +4513,7 @@ func (userSecurityMapper) CreateActionToken(tenantID types.TenantID, token *type now := types.TimeNow() token.CreatedAt = now token.UpdatedAt = now - return CacheObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(token.UserID, token.TokenID, token.Action, token.Target), token) + return CacheObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(token.UserID, token.TokenID, token.Action, token.Target), actionTokenCacheFromType(token)) } func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types.Uid, token, action, target string, now time.Time) (*types.ActionToken, error) { @@ -4476,7 +4528,7 @@ func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types. return nil, types.ErrMalformed } cacheID := actionTokenCacheID(uid, tokenID, action, target) - var cached types.ActionToken + var cached actionTokenCache ok, err := GetCachedObject(tenantID, cacheNamespaceActionToken, cacheID, &cached) if err != nil { return nil, err @@ -4490,7 +4542,7 @@ func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types. if err = DeleteCachedObject(tenantID, cacheNamespaceActionToken, cacheID); err != nil { return nil, err } - return &cached, nil + return cached.toType(), nil } func (userSecurityMapper) GetIndependentPassword(tenantID types.TenantID, uid types.Uid, purpose string) (*types.IndependentPassword, error) { diff --git a/server/store/tenant_store_test.go b/server/store/tenant_store_test.go index 7083ade..f5e75e5 100644 --- a/server/store/tenant_store_test.go +++ b/server/store/tenant_store_test.go @@ -55,6 +55,7 @@ func TestVerificationCodeCachePreservesInternalFields(t *testing.T) { Action: "signup", PhoneHash: "phone-hash", PhoneMask: "+861****000", + TargetUser: types.ParseUserId("usrR25C_ZgHzyg"), CodeHash: "code-hash", VerificationTokenHash: &tokenHash, State: types.VerificationStateVerified, @@ -81,6 +82,7 @@ func TestVerificationCodeCachePreservesInternalFields(t *testing.T) { } if !strings.Contains(string(cachePayload), `"code_hash":"code-hash"`) || !strings.Contains(string(cachePayload), `"phone_hash":"phone-hash"`) || + !strings.Contains(string(cachePayload), `"target_user":"usrR25C_ZgHzyg"`) || !strings.Contains(string(cachePayload), `"max_attempts":5`) { t.Fatalf("cache verification JSON missing internal fields: %s", cachePayload) } @@ -91,8 +93,55 @@ func TestVerificationCodeCachePreservesInternalFields(t *testing.T) { } 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.TargetUser != vc.TargetUser || 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) } } + +func TestActionTokenCachePreservesInternalFields(t *testing.T) { + now := time.Now().UTC().Round(time.Millisecond) + consumedAt := now.Add(time.Minute) + token := &types.ActionToken{ + ID: 9, + TenantID: 7, + UserID: types.ParseUserId("usrR25C_ZgHzyg"), + TokenID: "token-id", + Action: "close_youth_mode", + Target: "youth_mode", + TokenHash: "token-hash", + State: 1, + ExpiresAt: now.Add(5 * time.Minute), + ConsumedAt: &consumedAt, + CreatedAt: now, + UpdatedAt: consumedAt, + } + + publicPayload, err := json.Marshal(token) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(publicPayload), "token-hash") || strings.Contains(string(publicPayload), "token-id") { + t.Fatalf("public action token JSON leaked internal fields: %s", publicPayload) + } + + cachePayload, err := json.Marshal(actionTokenCacheFromType(token)) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(cachePayload), `"token_hash":"token-hash"`) || + !strings.Contains(string(cachePayload), `"token_id":"token-id"`) || + !strings.Contains(string(cachePayload), `"state":1`) { + t.Fatalf("cache action token JSON missing internal fields: %s", cachePayload) + } + + var cached actionTokenCache + if err := json.Unmarshal(cachePayload, &cached); err != nil { + t.Fatal(err) + } + got := cached.toType() + if got.TokenHash != token.TokenHash || got.TokenID != token.TokenID || + got.UserID != token.UserID || got.State != token.State || got.Action != token.Action { + t.Fatalf("cache round trip lost fields: got %#v", got) + } +}