Files
im_server/server/user_verification_test.go
T

231 lines
7.6 KiB
Go

package main
import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/tinode/chat/server/auth"
"github.com/tinode/chat/server/auth/mock_auth"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/mock_store"
"github.com/tinode/chat/server/store/types"
)
func TestConsumeProductVerificationCred(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
oldUsers := store.Users
oldVerifications := store.Verifications
defer func() {
store.Users = oldUsers
store.Verifications = oldVerifications
}()
makeTestAPIKey(t)
token := "verified-token"
tokenHash := hashIMSecret(7, "register", "challenge-1", "phone-hash", token)
store.Verifications = &testVerificationStore{get: &types.VerificationCode{
TenantID: 7,
ChallengeID: "challenge-1",
Scene: "register",
PhoneHash: "phone-hash",
VerificationTokenHash: &tokenHash,
State: types.VerificationStateVerified,
ExpiresAt: time.Now().UTC().Add(time.Minute),
}}
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
users.EXPECT().UpsertCred(types.TenantID(7), gomock.AssignableToTypeOf(&types.Credential{})).
DoAndReturn(func(_ types.TenantID, cred *types.Credential) (bool, error) {
if cred.User != types.Uid(42).String() || cred.Method != "tel" || cred.Value != "+8613800138000" || !cred.Done {
t.Fatalf("unexpected credential: %#v", cred)
}
return true, nil
})
store.Users = users
value, ok, err := consumeProductVerificationCred(7, 42, &MsgCredClient{
Method: "tel",
Value: "+8613800138000",
Response: token,
Params: map[string]any{"challenge_id": "challenge-1", "countryCode": "CN"},
}, func(value string, _ map[string]any) (string, error) {
return "tel:" + value, nil
})
if err != nil || !ok || value != "+8613800138000" {
t.Fatalf("consumeProductVerificationCred = value %q ok %v err %v", value, ok, err)
}
if !store.Verifications.(*testVerificationStore).consumed {
t.Fatal("expected verification token to be consumed")
}
}
func TestConsumeProductVerificationCredRejectsBadToken(t *testing.T) {
oldUsers := store.Users
oldVerifications := store.Verifications
defer func() {
store.Users = oldUsers
store.Verifications = oldVerifications
}()
makeTestAPIKey(t)
tokenHash := hashIMSecret(7, "register", "challenge-1", "phone-hash", "good-token")
store.Verifications = &testVerificationStore{get: &types.VerificationCode{
TenantID: 7,
ChallengeID: "challenge-1",
Scene: "register",
PhoneHash: "phone-hash",
VerificationTokenHash: &tokenHash,
State: types.VerificationStateVerified,
ExpiresAt: time.Now().UTC().Add(time.Minute),
}}
_, ok, err := consumeProductVerificationCred(7, 42, &MsgCredClient{
Method: "tel",
Value: "+8613800138000",
Response: "bad-token",
Params: map[string]any{"challenge_id": "challenge-1"},
}, func(value string, _ map[string]any) (string, error) {
return "tel:" + value, nil
})
if !ok || err != types.ErrInvalidResponse {
t.Fatalf("consumeProductVerificationCred bad token ok %v err %v", ok, err)
}
if store.Verifications.(*testVerificationStore).consumed {
t.Fatal("bad token must not be consumed")
}
}
func TestServeIMPasswordResetVerify(t *testing.T) {
oldTenants := store.Tenants
oldVerifications := store.Verifications
defer func() {
store.Tenants = oldTenants
store.Verifications = oldVerifications
}()
makeTestAPIKey(t)
store.Tenants = testTenantStore{tenant: &types.Tenant{ID: 7, Code: "acme", Name: "Acme", State: types.TenantStateActive}}
store.Verifications = &testVerificationStore{get: &types.VerificationCode{
TenantID: 7,
ChallengeID: "challenge-1",
Scene: "reset_password",
PhoneHash: "phone-hash",
CodeHash: hashIMSecret(7, "reset_password", "challenge-1", "phone-hash", "123456"),
State: types.VerificationStatePending,
MaxAttempts: 5,
ExpiresAt: time.Now().UTC().Add(time.Minute),
}}
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/password-reset/challenges/challenge-1/verify",
bytes.NewBufferString(`{"code":"123456"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
serveIMPasswordResetVerify(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
resp := decodeIMHTTPResponse(t, rec)
data := resp.Data.(map[string]any)
token, _ := data["reset_token"].(string)
if !strings.HasPrefix(token, "challenge-1.") {
t.Fatalf("unexpected reset token: %#v", data)
}
}
func TestServeIMPasswordResetConfirm(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
oldTenants := store.Tenants
oldVerifications := store.Verifications
oldUsers := store.Users
oldStore := store.Store
oldSecurity := store.Security
defer func() {
store.Tenants = oldTenants
store.Verifications = oldVerifications
store.Users = oldUsers
store.Store = oldStore
store.Security = oldSecurity
}()
makeTestAPIKey(t)
token := "reset-secret"
tokenHash := hashIMSecret(7, "reset_password", "challenge-1", "phone-hash", token)
store.Tenants = testTenantStore{tenant: &types.Tenant{ID: 7, Code: "acme", Name: "Acme", State: types.TenantStateActive}}
store.Verifications = &testVerificationStore{get: &types.VerificationCode{
TenantID: 7,
ChallengeID: "challenge-1",
Scene: "reset_password",
PhoneHash: "phone-hash",
TargetUser: 42,
VerificationTokenHash: &tokenHash,
State: types.VerificationStateVerified,
ExpiresAt: time.Now().UTC().Add(time.Minute),
}}
users := mock_store.NewMockUsersPersistenceInterface(ctrl)
users.EXPECT().GetAuthRecord(types.TenantID(7), types.Uid(42), "basic").
Return("alice", auth.LevelAuth, []byte("old"), time.Time{}, nil)
store.Users = users
basicAuth := mock_auth.NewMockAuthHandler(ctrl)
basicAuth.EXPECT().IsInitialized().Return(true)
basicAuth.EXPECT().UpdateRecord(auth.AuthContext{TenantID: 7}, gomock.Any(), []byte(":new-password")).
Return(&auth.Rec{TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth}, nil)
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
mockStore.EXPECT().GetLogicalAuthHandler("basic").Return(basicAuth)
store.Store = mockStore
security := &testSecurityStore{}
store.Security = security
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/password-reset/confirm",
bytes.NewBufferString(`{"reset_token":"challenge-1.`+token+`","new_password":"new-password"}`))
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
req.Header.Set("X-Tenant-Code", "acme")
rec := httptest.NewRecorder()
serveIMPasswordResetConfirm(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
if !store.Verifications.(*testVerificationStore).consumed {
t.Fatal("expected reset token to be consumed")
}
if !security.bumped || security.bumpedUser != types.Uid(42) {
t.Fatalf("expected auth version bump, got %#v", security)
}
}
type testSecurityStore struct {
version int
err error
bumped bool
bumpedUser types.Uid
}
func (s *testSecurityStore) GetAuthVersion(types.TenantID, types.Uid) (int, error) {
if s.version == 0 {
return 1, s.err
}
return s.version, s.err
}
func (s *testSecurityStore) BumpAuthVersion(_ types.TenantID, uid types.Uid, _ time.Time) (int, error) {
s.bumped = true
s.bumpedUser = uid
if s.version == 0 {
s.version = 1
}
s.version++
return s.version, s.err
}