diff --git a/server/user.go b/server/user.go index 943541e..b0e107a 100644 --- a/server/user.go +++ b/server/user.go @@ -156,6 +156,14 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) { continue } challengeID, _ := cr.Params["challenge_id"].(string) + if strings.TrimSpace(challengeID) == "" && phoneRegistration && isLegacyRegistrationSMSCode(cr.Response) { + if err := attachLegacyRegistrationSMSChallenge(s.tenantID, cr, phone, time.Now().UTC().Round(time.Millisecond)); err != nil { + s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp, + map[string]any{"creds": []string{"tel"}})) + return + } + challengeID, _ = cr.Params["challenge_id"].(string) + } if phone == "" || strings.TrimSpace(challengeID) == "" || strings.TrimSpace(cr.Response) == "" || (registrationPhone != "" && registrationPhone != phone) { s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp, @@ -400,7 +408,15 @@ var ( func validateAccountRegistrationTenant(s *Session, msg *ClientComMessage) error { if msg == nil || msg.Acc == nil || msg.Acc.RegistrationType == nil { - return errRegistrationTypeRequired + if msg != nil && msg.Acc != nil && s != nil && !strings.EqualFold(s.tenantCode, imDefaultTenantCode) { + registrationType := registrationTypeEnterprise + msg.Acc.RegistrationType = ®istrationType + if strings.TrimSpace(msg.Acc.TenantCode) == "" { + msg.Acc.TenantCode = s.tenantCode + } + } else { + return errRegistrationTypeRequired + } } tenantCode := strings.TrimSpace(msg.Acc.TenantCode) switch *msg.Acc.RegistrationType { @@ -427,6 +443,51 @@ func validateAccountRegistrationTenant(s *Session, msg *ClientComMessage) error return nil } +func isLegacyRegistrationSMSCode(value string) bool { + if len(value) != imVerificationCodeLength { + return false + } + for i := range value { + if value[i] < '0' || value[i] > '9' { + return false + } + } + return true +} + +func attachLegacyRegistrationSMSChallenge(tenantID types.TenantID, cr *MsgCredClient, phone string, now time.Time) error { + if store.Verifications == nil { + return types.ErrInternal + } + phoneHash := hashIMSecret(tenantID, "register", phone, "phone") + vc, err := store.Verifications.GetActive(tenantID, "register", phoneHash, now) + if err != nil { + return err + } + if vc.CodeHash != hashIMSecret(tenantID, vc.Scene, vc.ChallengeID, vc.PhoneHash, strings.TrimSpace(cr.Response)) { + nextState := types.VerificationStatePending + if vc.Attempts+1 >= vc.MaxAttempts { + nextState = types.VerificationStateLocked + } + _ = store.Verifications.Fail(tenantID, vc.ChallengeID, nextState) + return types.ErrInvalidResponse + } + token, err := randomIMToken(32) + if err != nil { + return err + } + tokenHash := hashIMSecret(tenantID, vc.Scene, vc.ChallengeID, vc.PhoneHash, token) + if err = store.Verifications.MarkVerified(tenantID, vc.ChallengeID, tokenHash, now); err != nil { + return err + } + if cr.Params == nil { + cr.Params = map[string]any{} + } + cr.Params["challenge_id"] = vc.ChallengeID + cr.Response = token + return nil +} + func registrationTenantErrorParams(err error) map[string]any { params := map[string]any{"what": "registration_type", "reason": "REGISTRATION_TYPE_INVALID"} switch { diff --git a/server/user_registration_test.go b/server/user_registration_test.go index daab4b3..29a17f2 100644 --- a/server/user_registration_test.go +++ b/server/user_registration_test.go @@ -2,6 +2,7 @@ package main import ( "regexp" + "strings" "testing" "time" @@ -83,6 +84,70 @@ func TestRegistrationSecretForAccount(t *testing.T) { } } +func TestValidateAccountRegistrationTenantDefaultsEnterpriseFromSession(t *testing.T) { + msg := &ClientComMessage{Acc: &MsgClientAcc{}} + s := &Session{tenantCode: "system"} + + if err := validateAccountRegistrationTenant(s, msg); err != nil { + t.Fatalf("validateAccountRegistrationTenant returned %v", err) + } + if msg.Acc.RegistrationType == nil || *msg.Acc.RegistrationType != registrationTypeEnterprise { + t.Fatalf("registration type was not defaulted to enterprise: %#v", msg.Acc.RegistrationType) + } + if msg.Acc.TenantCode != "system" { + t.Fatalf("tenant code default = %q, want system", msg.Acc.TenantCode) + } +} + +func TestValidateAccountRegistrationTenantStillRequiresPersonalType(t *testing.T) { + err := validateAccountRegistrationTenant(&Session{tenantCode: imDefaultTenantCode}, &ClientComMessage{Acc: &MsgClientAcc{}}) + if err != errRegistrationTypeRequired { + t.Fatalf("got %v, want registration type required", err) + } +} + +func TestAttachLegacyRegistrationSMSChallenge(t *testing.T) { + oldVerifications := store.Verifications + defer func() { + store.Verifications = oldVerifications + }() + + tenantID := types.TenantID(7) + phone := "+8618612345678" + code := "123456" + phoneHash := hashIMSecret(tenantID, "register", phone, "phone") + challengeID := "challenge-legacy" + store.Verifications = &testVerificationStore{active: &types.VerificationCode{ + TenantID: tenantID, + ChallengeID: challengeID, + Scene: "register", + PhoneHash: phoneHash, + CodeHash: hashIMSecret(tenantID, "register", challengeID, phoneHash, code), + State: types.VerificationStatePending, + MaxAttempts: 5, + ExpiresAt: time.Now().UTC().Add(time.Minute), + }} + + cred := &MsgCredClient{Method: "tel", Value: phone, Response: code} + if err := attachLegacyRegistrationSMSChallenge(tenantID, cred, phone, time.Now().UTC()); err != nil { + t.Fatalf("attachLegacyRegistrationSMSChallenge returned %v", err) + } + if cred.Params["challenge_id"] != challengeID { + t.Fatalf("challenge id = %#v, want %q", cred.Params["challenge_id"], challengeID) + } + if cred.Response == code || strings.TrimSpace(cred.Response) == "" { + t.Fatalf("legacy code was not replaced by verification token: %q", cred.Response) + } + verifications := store.Verifications.(*testVerificationStore) + if !verifications.verified { + t.Fatal("verification challenge was not marked verified") + } + wantHash := hashIMSecret(tenantID, "register", challengeID, phoneHash, cred.Response) + if verifications.tokenHash != wantHash { + t.Fatalf("token hash mismatch") + } +} + func TestRegistrationPublicWithUsername(t *testing.T) { if got := registrationPublicWithUsername(nil, "alice").(map[string]any)["fn"]; got != "alice" { t.Fatalf("got fn=%v", got)