merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
|
|
@ -73,6 +73,32 @@ func TestWebhookPhoneLoginUsesRandomSMSCode(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWebhookPhoneLoginCanonicalizesNationalTrunkBeforeOTP(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sender := &captureOTPSender{}
|
||||
svc := NewService(
|
||||
memory.NewUserStore(),
|
||||
memory.NewAuthorizationStore(),
|
||||
memory.NewCodeStore(),
|
||||
nil,
|
||||
nil,
|
||||
"fixed-code-must-not-leak",
|
||||
WithPhoneCodeDelivery(sender, 6),
|
||||
)
|
||||
|
||||
hash, err := svc.SendCode(ctx, "+98 0998 167 9461")
|
||||
if err != nil {
|
||||
t.Fatalf("SendCode: %v", err)
|
||||
}
|
||||
if len(sender.requests) != 1 || sender.requests[0].Recipient != "989981679461" {
|
||||
t.Fatalf("OTP requests = %+v, want canonical Iran recipient", sender.requests)
|
||||
}
|
||||
_, _, needSignUp, err := svc.SignIn(ctx, domain.Authorization{}, "989981679461", hash, sender.requests[0].Code)
|
||||
if err != nil || !needSignUp {
|
||||
t.Fatalf("SignIn canonical variant needSignUp=%v err=%v", needSignUp, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookExistingAccountRejectionKeepsDurableAppCode(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
|
|
|
|||
|
|
@ -64,7 +64,10 @@ func validPhone(phone string) bool {
|
|||
}
|
||||
|
||||
func systemUserLoginForbidden(u domain.User) bool {
|
||||
return domain.IsSystemUserID(u.ID)
|
||||
// Login-facing callers deliberately use the same non-enumerating error for
|
||||
// reserved identities and irreversible tombstones. The durable authorization
|
||||
// store repeats the deleted check under the user lock to close the TOCTOU gap.
|
||||
return u.Deleted || domain.IsSystemUserID(u.ID)
|
||||
}
|
||||
|
||||
func systemLoginPhoneForbidden(phone string) bool {
|
||||
|
|
@ -270,12 +273,14 @@ func NewService(users store.UserStore, auths store.AuthorizationStore, codes sto
|
|||
}
|
||||
|
||||
// BindTempAuthKey 校验并记录 TDesktop PFS temp→perm auth key 绑定。
|
||||
func (s *Service) BindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) error {
|
||||
func (s *Service) BindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) (domain.TempAuthKeyBindingResult, error) {
|
||||
var validated domain.TempAuthKeyBindingResult
|
||||
if s.authKeys != nil {
|
||||
inner, protocolExpiresAt, err := s.validateBindTempAuthKey(ctx, sessionID, binding)
|
||||
inner, protocolExpiresAt, result, err := s.validateBindTempAuthKey(ctx, sessionID, binding)
|
||||
if err != nil {
|
||||
return err
|
||||
return domain.TempAuthKeyBindingResult{}, err
|
||||
}
|
||||
validated = result
|
||||
binding.TempSessionID = inner.TempSessionID
|
||||
// The bind request's expires_at is a signed client assertion. TDesktop
|
||||
// intentionally adds a small grace interval, while Android derives its
|
||||
|
|
@ -287,21 +292,22 @@ func (s *Service) BindTempAuthKey(ctx context.Context, sessionID int64, binding
|
|||
// The edge may admit the frame immediately before the temporary key's
|
||||
// absolute boundary and the encrypted proof may cross it. This is a temp-key
|
||||
// rotation condition, never a destructive permanent-key proof failure.
|
||||
return ErrTempAuthKeyEmpty
|
||||
return domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyEmpty
|
||||
}
|
||||
if s.tempKeys == nil {
|
||||
return nil
|
||||
return validated, nil
|
||||
}
|
||||
if err := s.tempKeys.Save(ctx, binding); err != nil {
|
||||
result, err := s.tempKeys.SaveWithState(ctx, binding)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrTempAuthKeyAlreadyBound) {
|
||||
return ErrTempAuthKeyAlreadyBound
|
||||
return domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyAlreadyBound
|
||||
}
|
||||
if errors.Is(err, store.ErrAuthKeyBindingInvalid) {
|
||||
return s.classifyBindingStoreInvalid(ctx, binding)
|
||||
return domain.TempAuthKeyBindingResult{}, s.classifyBindingStoreInvalid(ctx, binding)
|
||||
}
|
||||
return err
|
||||
return domain.TempAuthKeyBindingResult{}, err
|
||||
}
|
||||
return nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ResolveAuthKey 将已绑定的 temp auth_key 解析为对应 perm auth_key。
|
||||
|
|
@ -323,7 +329,7 @@ func (s *Service) ResolveAuthKey(ctx context.Context, authKeyID [8]byte) ([8]byt
|
|||
|
||||
// UserID 返回 auth_key 当前绑定的用户。未登录、或两步验证未完成时 found=false。
|
||||
func (s *Service) UserID(ctx context.Context, authKeyID [8]byte) (int64, bool, error) {
|
||||
if s == nil || s.auths == nil {
|
||||
if s == nil || s.auths == nil || s.users == nil {
|
||||
return 0, false, nil
|
||||
}
|
||||
a, found, err := s.auths.ByAuthKey(ctx, authKeyID)
|
||||
|
|
@ -334,7 +340,13 @@ func (s *Service) UserID(ctx context.Context, authKeyID [8]byte) (int64, bool, e
|
|||
// 两步验证未完成:业务鉴权视为未登录,仅允许 auth.checkPassword 继续。
|
||||
return 0, false, nil
|
||||
}
|
||||
if domain.IsSystemUserID(a.UserID) {
|
||||
u, userFound, err := s.users.ByID(ctx, a.UserID)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if !userFound || systemUserLoginForbidden(u) {
|
||||
// A stale row can exist only after an interrupted/legacy path. Fail closed
|
||||
// before it reaches the Router auth cache and retire it opportunistically.
|
||||
_ = s.auths.Delete(ctx, authKeyID)
|
||||
return 0, false, nil
|
||||
}
|
||||
|
|
@ -344,14 +356,18 @@ func (s *Service) UserID(ctx context.Context, authKeyID [8]byte) (int64, bool, e
|
|||
// PendingPasswordUserID 返回处于"待两步验证"状态的 auth_key 对应的用户。
|
||||
// UserID 对 password_pending 的 auth_key 返回未登录,auth.checkPassword 借此仍能定位待验证用户。
|
||||
func (s *Service) PendingPasswordUserID(ctx context.Context, authKeyID [8]byte) (int64, bool, error) {
|
||||
if s == nil || s.auths == nil {
|
||||
if s == nil || s.auths == nil || s.users == nil {
|
||||
return 0, false, nil
|
||||
}
|
||||
a, found, err := s.auths.ByAuthKey(ctx, authKeyID)
|
||||
if err != nil || !found || !a.PasswordPending {
|
||||
return 0, false, err
|
||||
}
|
||||
if domain.IsSystemUserID(a.UserID) {
|
||||
u, userFound, err := s.users.ByID(ctx, a.UserID)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if !userFound || systemUserLoginForbidden(u) {
|
||||
_ = s.auths.Delete(ctx, authKeyID)
|
||||
return 0, false, nil
|
||||
}
|
||||
|
|
@ -359,13 +375,24 @@ func (s *Service) PendingPasswordUserID(ctx context.Context, authKeyID [8]byte)
|
|||
}
|
||||
|
||||
// CompletePasswordSignIn 在两步验证通过后清除 password_pending,使 auth_key 转为完全授权。
|
||||
func (s *Service) CompletePasswordSignIn(ctx context.Context, authKeyID [8]byte) error {
|
||||
func (s *Service) CompletePasswordSignIn(ctx context.Context, authKeyID [8]byte, expectedUserID int64) error {
|
||||
if s == nil || s.auths == nil {
|
||||
return nil
|
||||
}
|
||||
if err := s.auths.MarkPasswordPassed(ctx, authKeyID); err != nil {
|
||||
if expectedUserID == 0 {
|
||||
return store.ErrAuthorizationStateChanged
|
||||
}
|
||||
if err := s.auths.MarkPasswordPassed(ctx, authKeyID, expectedUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
// Revalidate the active account after the CAS promotion before Router caches
|
||||
// or binds the session. Account deletion can still linearize immediately
|
||||
// after the update and must leave the caller unauthorized.
|
||||
if userID, found, err := s.UserID(ctx, authKeyID); err != nil {
|
||||
return err
|
||||
} else if !found || userID != expectedUserID {
|
||||
return ErrSystemUserLoginForbidden
|
||||
}
|
||||
// This is where a 2FA account's sign-in actually finishes — finishSignIn
|
||||
// deliberately skipped the welcome message while password_pending.
|
||||
if a, found, err := s.auths.ByAuthKey(ctx, authKeyID); err == nil && found {
|
||||
|
|
@ -1423,7 +1450,11 @@ func (s *Service) AuthKeyClientInfo(ctx context.Context, authKeyID [8]byte) (dom
|
|||
if s == nil || s.authKeys == nil || authKeyID == ([8]byte{}) {
|
||||
return domain.AuthKeyClientInfo{}, false, nil
|
||||
}
|
||||
key, found, err := s.authKeys.Get(ctx, authKeyID)
|
||||
// Client metadata is a read-only projection. The physical connection's
|
||||
// first-frame Get and active-key heartbeat already own the durable orphan
|
||||
// lease, so this path must not turn every init/profile read into another
|
||||
// last_used_at write.
|
||||
key, found, err := s.authKeys.Revalidate(ctx, authKeyID)
|
||||
if err != nil || !found {
|
||||
return domain.AuthKeyClientInfo{}, found, err
|
||||
}
|
||||
|
|
@ -1499,6 +1530,16 @@ func (s *Service) ResetAuthorizations(ctx context.Context, userID int64, keepAut
|
|||
}
|
||||
|
||||
func (s *Service) bind(ctx context.Context, auth domain.Authorization, userID int64) error {
|
||||
if s == nil || s.users == nil || s.auths == nil || userID == 0 {
|
||||
return ErrSystemUserLoginForbidden
|
||||
}
|
||||
u, found, err := s.users.ByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || systemUserLoginForbidden(u) {
|
||||
return ErrSystemUserLoginForbidden
|
||||
}
|
||||
if s.authKeys != nil {
|
||||
key, found, err := s.authKeys.Get(ctx, auth.AuthKeyID)
|
||||
if err != nil {
|
||||
|
|
@ -1519,6 +1560,9 @@ func (s *Service) bind(ctx context.Context, auth domain.Authorization, userID in
|
|||
if errors.Is(err, store.ErrAuthKeyNotPermanent) {
|
||||
return ErrAuthKeyPermEmpty
|
||||
}
|
||||
if errors.Is(err, domain.ErrAccountDeleted) || errors.Is(err, domain.ErrUserNotFound) {
|
||||
return ErrSystemUserLoginForbidden
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
@ -1535,17 +1579,19 @@ func (s *Service) passwordNeeded(ctx context.Context, userID int64) (bool, error
|
|||
return found && settings.HasPassword, nil
|
||||
}
|
||||
|
||||
const loginMessageTpl = `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `!
|
||||
func loginMessageTemplate() string {
|
||||
return `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `!
|
||||
|
||||
This code can be used to log in to your ` + branding.ProductName + ` account. We never ask it for anything else.
|
||||
|
||||
If you didn't request this code by trying to log in on another device, simply ignore this message.`
|
||||
}
|
||||
|
||||
func (s *Service) recordLoginMessage(ctx context.Context, userID int64, code string) (domain.Message, error) {
|
||||
if s.messages == nil || s.dialogs == nil {
|
||||
return domain.Message{}, nil
|
||||
}
|
||||
body := fmt.Sprintf(loginMessageTpl, code)
|
||||
body := fmt.Sprintf(loginMessageTemplate(), code)
|
||||
codeOffset := len("Login code: ")
|
||||
msg, err := s.messages.Create(ctx, domain.Message{
|
||||
OwnerUserID: userID,
|
||||
|
|
@ -1595,53 +1641,58 @@ func (s *Service) recordWelcomeMessage(ctx context.Context, u domain.User) {
|
|||
})
|
||||
}
|
||||
|
||||
func (s *Service) validateBindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) (mtcrypto.BindAuthKeyInner, int, error) {
|
||||
func (s *Service) validateBindTempAuthKey(ctx context.Context, sessionID int64, binding domain.TempAuthKeyBinding) (mtcrypto.BindAuthKeyInner, int, domain.TempAuthKeyBindingResult, error) {
|
||||
if binding.ExpiresAt <= int(time.Now().Unix()) {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrExpiresAtInvalid
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrExpiresAtInvalid
|
||||
}
|
||||
temp, found, err := s.authKeys.Get(ctx, binding.TempAuthKeyID)
|
||||
permID := authKeyIDFromInt64(binding.PermAuthKeyID)
|
||||
pair, err := s.authKeys.LoadBindingKeys(ctx, binding.TempAuthKeyID, permID)
|
||||
if err != nil {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, err
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, err
|
||||
}
|
||||
// expires_at in auth.bindTempAuthKey is client-supplied and must only attest
|
||||
// to a still-live binding. It may never create or reclassify a protocol key;
|
||||
// the caller normalizes durable retention to this handshake-authoritative
|
||||
// temp.ExpiresAt instead of trusting the client value.
|
||||
if !found || temp.ExpiresAt <= int(time.Now().Unix()) {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrTempAuthKeyEmpty
|
||||
if !pair.TemporaryFound || pair.Temporary.ExpiresAt <= int(time.Now().Unix()) {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyEmpty
|
||||
}
|
||||
|
||||
permID := authKeyIDFromInt64(binding.PermAuthKeyID)
|
||||
perm, found, err := s.authKeys.Get(ctx, permID)
|
||||
if err != nil {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, err
|
||||
}
|
||||
if !found || perm.ExpiresAt != 0 {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrEncryptedMessageInvalid
|
||||
if !pair.PermanentFound || pair.Permanent.ExpiresAt != 0 {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrEncryptedMessageInvalid
|
||||
}
|
||||
|
||||
inner, err := decryptBindAuthKeyInner(perm, binding.EncryptedMessage)
|
||||
inner, err := decryptBindAuthKeyInner(pair.Permanent, binding.EncryptedMessage)
|
||||
if err != nil {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrEncryptedMessageInvalid
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrEncryptedMessageInvalid
|
||||
}
|
||||
if inner.Nonce != binding.Nonce ||
|
||||
inner.TempAuthKeyID != authKeyIDInt64(binding.TempAuthKeyID) ||
|
||||
inner.PermAuthKeyID != binding.PermAuthKeyID ||
|
||||
inner.TempSessionID != sessionID ||
|
||||
inner.ExpiresAt != binding.ExpiresAt {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrEncryptedMessageInvalid
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrEncryptedMessageInvalid
|
||||
}
|
||||
if temp.ExpiresAt <= int(time.Now().Unix()) {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, ErrTempAuthKeyEmpty
|
||||
if pair.Temporary.ExpiresAt <= int(time.Now().Unix()) {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, ErrTempAuthKeyEmpty
|
||||
}
|
||||
return inner, temp.ExpiresAt, nil
|
||||
layer, observationID, err := store.MergeAuthKeyLayerObservations(
|
||||
pair.Temporary.Layer, pair.Temporary.LayerObservationID,
|
||||
pair.Permanent.Layer, pair.Permanent.LayerObservationID,
|
||||
)
|
||||
if err != nil {
|
||||
return mtcrypto.BindAuthKeyInner{}, 0, domain.TempAuthKeyBindingResult{}, err
|
||||
}
|
||||
return inner, pair.Temporary.ExpiresAt, domain.TempAuthKeyBindingResult{
|
||||
Layer: layer, LayerObservationID: observationID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) classifyBindingStoreInvalid(ctx context.Context, binding domain.TempAuthKeyBinding) error {
|
||||
if s == nil || s.authKeys == nil {
|
||||
return ErrEncryptedMessageInvalid
|
||||
}
|
||||
temp, found, err := s.authKeys.Get(ctx, binding.TempAuthKeyID)
|
||||
temp, found, err := s.authKeys.Revalidate(ctx, binding.TempAuthKeyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ func TestBindTempAuthKeyValidatesEncryptedMessage(t *testing.T) {
|
|||
t.Fatalf("encrypt bind message: %v", err)
|
||||
}
|
||||
|
||||
err = svc.BindTempAuthKey(ctx, sessionID, domain.TempAuthKeyBinding{
|
||||
_, err = svc.BindTempAuthKey(ctx, sessionID, domain.TempAuthKeyBinding{
|
||||
TempAuthKeyID: tempKey.ID,
|
||||
PermAuthKeyID: permKey.IntID(),
|
||||
Nonce: nonce,
|
||||
|
|
@ -59,7 +59,7 @@ func TestBindTempAuthKeyValidatesEncryptedMessage(t *testing.T) {
|
|||
t.Fatalf("BindTempAuthKey valid message: %v", err)
|
||||
}
|
||||
|
||||
err = svc.BindTempAuthKey(ctx, sessionID+1, domain.TempAuthKeyBinding{
|
||||
_, err = svc.BindTempAuthKey(ctx, sessionID+1, domain.TempAuthKeyBinding{
|
||||
TempAuthKeyID: tempKey.ID,
|
||||
PermAuthKeyID: permKey.IntID(),
|
||||
Nonce: nonce,
|
||||
|
|
@ -89,7 +89,7 @@ func TestBindTempAuthKeyValidatesEncryptedMessage(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("encrypt extended bind message: %v", err)
|
||||
}
|
||||
err = svc.BindTempAuthKey(ctx, sessionID, domain.TempAuthKeyBinding{
|
||||
_, err = svc.BindTempAuthKey(ctx, sessionID, domain.TempAuthKeyBinding{
|
||||
TempAuthKeyID: tempKey.ID,
|
||||
PermAuthKeyID: permKey.IntID(),
|
||||
Nonce: nonce,
|
||||
|
|
@ -120,17 +120,17 @@ func TestBindTempAuthKeyClassifiesExpiryWithoutDestroyingPermanentKey(t *testing
|
|||
PermAuthKeyID: permKey.IntID(),
|
||||
ExpiresAt: int(time.Now().Add(time.Hour).Unix()),
|
||||
}
|
||||
if err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrTempAuthKeyEmpty) {
|
||||
if _, err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrTempAuthKeyEmpty) {
|
||||
t.Fatalf("expired protocol temp key err = %v, want ErrTempAuthKeyEmpty", err)
|
||||
}
|
||||
|
||||
request.TempAuthKeyID = testAuthKey(0x33).ID
|
||||
if err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrTempAuthKeyEmpty) {
|
||||
if _, err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrTempAuthKeyEmpty) {
|
||||
t.Fatalf("missing protocol temp key err = %v, want ErrTempAuthKeyEmpty", err)
|
||||
}
|
||||
|
||||
request.ExpiresAt = int(time.Now().Add(-time.Second).Unix())
|
||||
if err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrExpiresAtInvalid) {
|
||||
if _, err := svc.BindTempAuthKey(ctx, 1001, request); !errors.Is(err, ErrExpiresAtInvalid) {
|
||||
t.Fatalf("expired request proof err = %v, want ErrExpiresAtInvalid", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -346,6 +346,59 @@ func TestAuthorizationBindRejectsTemporaryProtocolKey(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDeletedUserCannotCrossAuthorizationBoundaries(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
authz := memory.NewAuthorizationStore()
|
||||
deleted, err := users.Create(ctx, domain.User{
|
||||
Deleted: true,
|
||||
DeletedAt: time.Now().Unix(),
|
||||
DeletionSource: domain.AccountDeletionManual,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create deleted user: %v", err)
|
||||
}
|
||||
svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345")
|
||||
|
||||
passkeyAuthKeyID := [8]byte{0x91}
|
||||
if _, err := svc.BindVerifiedLogin(ctx, domain.Authorization{AuthKeyID: passkeyAuthKeyID}, deleted.ID); !errors.Is(err, ErrSystemUserLoginForbidden) {
|
||||
t.Fatalf("BindVerifiedLogin deleted user err = %v, want ErrSystemUserLoginForbidden", err)
|
||||
}
|
||||
if _, found, err := authz.ByAuthKey(ctx, passkeyAuthKeyID); err != nil || found {
|
||||
t.Fatalf("deleted passkey authorization found=%v err=%v, want absent", found, err)
|
||||
}
|
||||
|
||||
qrAuthKeyID := [8]byte{0x92}
|
||||
if _, err := svc.AcceptLoginToken(ctx, domain.Authorization{AuthKeyID: qrAuthKeyID}, deleted.ID); !errors.Is(err, ErrSystemUserLoginForbidden) {
|
||||
t.Fatalf("AcceptLoginToken deleted user err = %v, want ErrSystemUserLoginForbidden", err)
|
||||
}
|
||||
if _, found, err := authz.ByAuthKey(ctx, qrAuthKeyID); err != nil || found {
|
||||
t.Fatalf("deleted QR authorization found=%v err=%v, want absent", found, err)
|
||||
}
|
||||
|
||||
staleAuthKeyID := [8]byte{0x93}
|
||||
if err := authz.Bind(ctx, domain.Authorization{AuthKeyID: staleAuthKeyID, UserID: deleted.ID}); err != nil {
|
||||
t.Fatalf("seed stale authorization: %v", err)
|
||||
}
|
||||
if userID, found, err := svc.UserID(ctx, staleAuthKeyID); err != nil || found || userID != 0 {
|
||||
t.Fatalf("UserID stale tombstone = %d found=%v err=%v, want unauthorized", userID, found, err)
|
||||
}
|
||||
if _, found, err := authz.ByAuthKey(ctx, staleAuthKeyID); err != nil || found {
|
||||
t.Fatalf("stale tombstone authorization found=%v err=%v, want retired", found, err)
|
||||
}
|
||||
|
||||
pendingAuthKeyID := [8]byte{0x94}
|
||||
if err := authz.Bind(ctx, domain.Authorization{AuthKeyID: pendingAuthKeyID, UserID: deleted.ID, PasswordPending: true}); err != nil {
|
||||
t.Fatalf("seed stale pending authorization: %v", err)
|
||||
}
|
||||
if err := svc.CompletePasswordSignIn(ctx, pendingAuthKeyID, deleted.ID); !errors.Is(err, ErrSystemUserLoginForbidden) {
|
||||
t.Fatalf("CompletePasswordSignIn deleted user err = %v, want ErrSystemUserLoginForbidden", err)
|
||||
}
|
||||
if _, found, err := authz.ByAuthKey(ctx, pendingAuthKeyID); err != nil || found {
|
||||
t.Fatalf("stale pending tombstone authorization found=%v err=%v, want retired", found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhoneCodeAcceptsTDesktopDigitsOnlySignIn(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
|
|
@ -377,6 +430,100 @@ func TestPhoneCodeAcceptsTDesktopDigitsOnlySignIn(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestVirtual888PhoneRegistersAndSignsIn(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
authz := memory.NewAuthorizationStore()
|
||||
delivery := &captureLoginCodeDelivery{}
|
||||
svc := NewService(
|
||||
users,
|
||||
authz,
|
||||
memory.NewCodeStore(),
|
||||
nil,
|
||||
nil,
|
||||
"12345",
|
||||
WithLoginCodeDelivery(delivery),
|
||||
)
|
||||
const (
|
||||
formatted = "+888 12-34"
|
||||
canonical = "8881234"
|
||||
)
|
||||
|
||||
firstHash, err := svc.SendCode(ctx, formatted)
|
||||
if err != nil {
|
||||
t.Fatalf("SendCode virtual phone: %v", err)
|
||||
}
|
||||
verifyCodeForSignUp(t, svc, canonical, firstHash, "12345")
|
||||
created, _, err := svc.SignUp(ctx, domain.Authorization{AuthKeyID: [8]byte{0x54}}, formatted, firstHash, "Virtual", "User")
|
||||
if err != nil {
|
||||
t.Fatalf("SignUp virtual phone: %v", err)
|
||||
}
|
||||
if created.Phone != canonical {
|
||||
t.Fatalf("created phone = %q, want %q", created.Phone, canonical)
|
||||
}
|
||||
|
||||
secondHash, err := svc.SendCode(ctx, canonical)
|
||||
if err != nil {
|
||||
t.Fatalf("SendCode existing virtual phone: %v", err)
|
||||
}
|
||||
signedIn, _, needSignUp, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: [8]byte{0x55}}, formatted, secondHash, "12345")
|
||||
if err != nil {
|
||||
t.Fatalf("SignIn virtual phone: %v", err)
|
||||
}
|
||||
if needSignUp || signedIn.ID != created.ID {
|
||||
t.Fatalf("SignIn user=%d needSignUp=%v, want existing user %d", signedIn.ID, needSignUp, created.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIranNationalTrunkVariantsShareOneAccountIdentity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
authz := memory.NewAuthorizationStore()
|
||||
delivery := &captureLoginCodeDelivery{}
|
||||
svc := NewService(
|
||||
users,
|
||||
authz,
|
||||
memory.NewCodeStore(),
|
||||
nil,
|
||||
nil,
|
||||
"12345",
|
||||
WithLoginCodeDelivery(delivery),
|
||||
)
|
||||
const (
|
||||
withNationalTrunk = "+98 0998 167 9461"
|
||||
international = "989981679461"
|
||||
canonical = "989981679461"
|
||||
)
|
||||
|
||||
firstHash, err := svc.SendCode(ctx, withNationalTrunk)
|
||||
if err != nil {
|
||||
t.Fatalf("SendCode trunk variant: %v", err)
|
||||
}
|
||||
verifyCodeForSignUp(t, svc, international, firstHash, "12345")
|
||||
created, _, err := svc.SignUp(ctx, domain.Authorization{AuthKeyID: [8]byte{1}}, international, firstHash, "Iran", "User")
|
||||
if err != nil {
|
||||
t.Fatalf("SignUp international variant: %v", err)
|
||||
}
|
||||
if created.Phone != canonical {
|
||||
t.Fatalf("created phone = %q, want %q", created.Phone, canonical)
|
||||
}
|
||||
|
||||
secondHash, err := svc.SendCode(ctx, international)
|
||||
if err != nil {
|
||||
t.Fatalf("SendCode existing international variant: %v", err)
|
||||
}
|
||||
signedIn, _, needSignUp, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: [8]byte{2}}, withNationalTrunk, secondHash, "12345")
|
||||
if err != nil {
|
||||
t.Fatalf("SignIn trunk variant: %v", err)
|
||||
}
|
||||
if needSignUp || signedIn.ID != created.ID {
|
||||
t.Fatalf("SignIn user=%d needSignUp=%v, want existing user %d", signedIn.ID, needSignUp, created.ID)
|
||||
}
|
||||
if got, found, err := users.ByPhone(ctx, canonical); err != nil || !found || got.ID != created.ID {
|
||||
t.Fatalf("canonical lookup user=%+v found=%v err=%v", got, found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyCodeForSignUp(t *testing.T, svc *Service, phone, hash, code string) {
|
||||
t.Helper()
|
||||
got, msg, needSignUp, err := svc.SignIn(context.Background(), domain.Authorization{}, phone, hash, code)
|
||||
|
|
@ -809,7 +956,10 @@ func TestSignInExistingTwoFactorAccountNeedsPassword(t *testing.T) {
|
|||
t.Fatalf("PendingPasswordUserID = %d pending=%v err=%v, want %d", pendingUID, pending, err, u.ID)
|
||||
}
|
||||
// 两步验证通过后转为完全授权。
|
||||
if err := svc.CompletePasswordSignIn(ctx, key); err != nil {
|
||||
if err := svc.CompletePasswordSignIn(ctx, key, 0); !errors.Is(err, store.ErrAuthorizationStateChanged) {
|
||||
t.Fatalf("CompletePasswordSignIn without expected user err=%v, want authorization state changed", err)
|
||||
}
|
||||
if err := svc.CompletePasswordSignIn(ctx, key, u.ID); err != nil {
|
||||
t.Fatalf("CompletePasswordSignIn: %v", err)
|
||||
}
|
||||
bound, found, err = svc.UserID(ctx, key)
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ func TestTwoFactorSignInDefersWelcomeMessageUntilPasswordCompletes(t *testing.T)
|
|||
t.Fatalf("welcome message fired before password check completed: %+v", pending.Messages[0])
|
||||
}
|
||||
|
||||
if err := svc.CompletePasswordSignIn(ctx, key); err != nil {
|
||||
if err := svc.CompletePasswordSignIn(ctx, key, u.ID); err != nil {
|
||||
t.Fatalf("CompletePasswordSignIn: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue