This commit is contained in:
onysd 2026-07-31 16:31:06 +03:00
parent 58ca71f0b7
commit 87a2a2b0e2
3 changed files with 24 additions and 10 deletions

View file

@ -867,6 +867,15 @@ func (s *Service) CancelCodeForAuthKey(ctx context.Context, authKeyID [8]byte, p
// email-login hash previously issued for this phone owner. Possession of only // email-login hash previously issued for this phone owner. Possession of only
// a phone number is never sufficient to remove an authentication factor. // a phone number is never sufficient to remove an authentication factor.
func (s *Service) ConsumeLoginEmailReset(ctx context.Context, phone, phoneCodeHash string) (int64, error) { func (s *Service) ConsumeLoginEmailReset(ctx context.Context, phone, phoneCodeHash string) (int64, error) {
// This flow exists to fall back to an SMS code when the login email is
// unreachable. Without a real phoneCodeSender configured, that "SMS code"
// is always the well-known TELESRV_DEV_AUTH_CODE (see createPhoneCode),
// so anyone who can call sendCode for a phone (no email access required)
// could strip the login-email requirement with a publicly known code.
// Refuse up front, before ClearLoginEmail runs, so nothing is mutated.
if s.phoneCodeSender == nil {
return 0, ErrCodeInvalid
}
phone = normalizePhone(phone) phone = normalizePhone(phone)
rec, found, err := s.codes.Get(ctx, phoneCodeHash) rec, found, err := s.codes.Get(ctx, phoneCodeHash)
if err != nil { if err != nil {

View file

@ -405,7 +405,8 @@ func TestConsumeLoginEmailResetRequiresExactIssuedHash(t *testing.T) {
users := &switchablePhoneOwnerStore{UserStore: baseUsers} users := &switchablePhoneOwnerStore{UserStore: baseUsers}
codes := memory.NewCodeStore() codes := memory.NewCodeStore()
delivery := &captureLoginCodeDelivery{} delivery := &captureLoginCodeDelivery{}
svc := NewService(users, memory.NewAuthorizationStore(), codes, nil, nil, "12345", WithLoginCodeDelivery(delivery)) svc := NewService(users, memory.NewAuthorizationStore(), codes, nil, nil, "12345",
WithLoginCodeDelivery(delivery), WithPhoneCodeDelivery(&captureOTPSender{}, 5))
seed := func(hash, channel string) { seed := func(hash, channel string) {
t.Helper() t.Helper()
if err := codes.Set(ctx, hash, store.PhoneCode{ if err := codes.Set(ctx, hash, store.PhoneCode{
@ -457,7 +458,7 @@ func TestConsumeLoginEmailResetRequiresExactIssuedHash(t *testing.T) {
if len(delivery.requests) != 1 || delivery.requests[0].UserID != owner.ID || delivery.requests[0].PhoneCodeHash != replacementHash { if len(delivery.requests) != 1 || delivery.requests[0].UserID != owner.ID || delivery.requests[0].PhoneCodeHash != replacementHash {
t.Fatalf("replacement delivery=%+v", delivery.requests) t.Fatalf("replacement delivery=%+v", delivery.requests)
} }
if rec, found, err := codes.Get(ctx, replacementHash); err != nil || !found || rec.Version != store.PhoneCodeVersionCurrent || rec.IssuedUserID != owner.ID || rec.Channel != codeChannelPhone { if rec, found, err := codes.Get(ctx, replacementHash); err != nil || !found || rec.Version != store.PhoneCodeVersionCurrent || rec.IssuedUserID != owner.ID || rec.Channel != codeChannelSMS {
t.Fatalf("replacement code=%+v found=%v err=%v", rec, found, err) t.Fatalf("replacement code=%+v found=%v err=%v", rec, found, err)
} }
} }
@ -481,7 +482,8 @@ func TestConcurrentLoginEmailResetHasSingleConsumer(t *testing.T) {
}, time.Minute); err != nil { }, time.Minute); err != nil {
t.Fatalf("seed code: %v", err) t.Fatalf("seed code: %v", err)
} }
svc := NewService(users, memory.NewAuthorizationStore(), codes, nil, nil, "12345") svc := NewService(users, memory.NewAuthorizationStore(), codes, nil, nil, "12345",
WithPhoneCodeDelivery(&captureOTPSender{}, 5))
const workers = 24 const workers = 24
start := make(chan struct{}) start := make(chan struct{})
errs := make(chan error, workers) errs := make(chan error, workers)
@ -542,7 +544,8 @@ func TestLoginEmailResetLocksUserAcrossOwnerTransfer(t *testing.T) {
t.Fatalf("seed reset code: %v", err) t.Fatalf("seed reset code: %v", err)
} }
delivery := &captureLoginCodeDelivery{} delivery := &captureLoginCodeDelivery{}
authSvc := NewService(users, memory.NewAuthorizationStore(), codes, nil, nil, "12345", WithLoginCodeDelivery(delivery)) authSvc := NewService(users, memory.NewAuthorizationStore(), codes, nil, nil, "12345",
WithLoginCodeDelivery(delivery), WithPhoneCodeDelivery(&captureOTPSender{}, 5))
resetUserID, err := authSvc.ConsumeLoginEmailReset(ctx, ownerA.Phone, hash) resetUserID, err := authSvc.ConsumeLoginEmailReset(ctx, ownerA.Phone, hash)
if err != nil || resetUserID != ownerA.ID { if err != nil || resetUserID != ownerA.ID {
t.Fatalf("ConsumeLoginEmailReset uid=%d err=%v", resetUserID, err) t.Fatalf("ConsumeLoginEmailReset uid=%d err=%v", resetUserID, err)

View file

@ -76,6 +76,7 @@ func TestLoginEmailEndToEnd(t *testing.T) {
messageStore := memory.NewMessageStore(dialogStore) messageStore := memory.NewMessageStore(dialogStore)
updateEventStore := memory.NewUpdateEventStore() updateEventStore := memory.NewUpdateEventStore()
emailSender := &loginEmailTestSender{} emailSender := &loginEmailTestSender{}
phoneSender := &loginEmailTestSender{}
accountService := account.NewService(passwordStore, accountService := account.NewService(passwordStore,
account.WithUsers(userStore), account.WithUsers(userStore),
account.WithLoginEmailVerification(codeStore, emailSender, 5*time.Minute, 5, 6)) account.WithLoginEmailVerification(codeStore, emailSender, 5*time.Minute, 5, 6))
@ -88,7 +89,8 @@ func TestLoginEmailEndToEnd(t *testing.T) {
CodeLength: 6, CodeLength: 6,
Store: accountService, Store: accountService,
Sender: emailSender, Sender: emailSender,
})) }),
auth.WithPhoneCodeDelivery(phoneSender, 5))
deps := rpc.Deps{ deps := rpc.Deps{
Auth: authService, Auth: authService,
@ -135,7 +137,7 @@ func TestLoginEmailEndToEnd(t *testing.T) {
return err return err
} }
hash := sent.(*tg.AuthSentCode).PhoneCodeHash hash := sent.(*tg.AuthSentCode).PhoneCodeHash
if _, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{PhoneNumber: phone, PhoneCodeHash: hash, PhoneCode: code}); err != nil { if _, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{PhoneNumber: phone, PhoneCodeHash: hash, PhoneCode: phoneSender.code}); err != nil {
return err return err
} }
if _, err := raw.AuthSignUp(ctx, &tg.AuthSignUpRequest{PhoneNumber: phone, PhoneCodeHash: hash, FirstName: "Owner"}); err != nil { if _, err := raw.AuthSignUp(ctx, &tg.AuthSignUpRequest{PhoneNumber: phone, PhoneCodeHash: hash, FirstName: "Owner"}); err != nil {
@ -247,12 +249,12 @@ func TestLoginEmailEndToEnd(t *testing.T) {
if !ok { if !ok {
return fmt.Errorf("resetLoginEmail result = %T, want *tg.AuthSentCode", resetRes) return fmt.Errorf("resetLoginEmail result = %T, want *tg.AuthSentCode", resetRes)
} }
if _, ok := resetSent.Type.(*tg.AuthSentCodeTypeApp); !ok { if _, ok := resetSent.Type.(*tg.AuthSentCodeTypeSMS); !ok {
return fmt.Errorf("resetLoginEmail sentCode type = %T, want *tg.AuthSentCodeTypeApp (back to phone)", resetSent.Type) return fmt.Errorf("resetLoginEmail sentCode type = %T, want *tg.AuthSentCodeTypeSMS (back to phone, real sender configured)", resetSent.Type)
} }
// 用手机验证码完成登录 // 用手机验证码完成登录(真实投递的随机码,不是固定 dev code
signInRes, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{PhoneNumber: phone, PhoneCodeHash: resetSent.PhoneCodeHash, PhoneCode: code}) signInRes, err := raw.AuthSignIn(ctx, &tg.AuthSignInRequest{PhoneNumber: phone, PhoneCodeHash: resetSent.PhoneCodeHash, PhoneCode: phoneSender.code})
if err != nil { if err != nil {
return err return err
} }