Sync telesrv commits 49f9bab and 04d9563 into the public mirror. Exclude private docs and runtime key material per sync rules.
263 lines
8.5 KiB
Go
263 lines
8.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/otpdelivery"
|
|
"telesrv/internal/store/memory"
|
|
)
|
|
|
|
type testLoginEmailStore struct {
|
|
emails map[string]string
|
|
}
|
|
|
|
func (s *testLoginEmailStore) LoginEmailByPhone(_ context.Context, phone string) (string, bool, error) {
|
|
email, ok := s.emails[domain.NormalizePhone(phone)]
|
|
return email, ok, nil
|
|
}
|
|
|
|
func (s *testLoginEmailStore) SetLoginEmail(_ context.Context, _ int64, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
type testMailSender struct {
|
|
to string
|
|
code string
|
|
}
|
|
|
|
func (s *testMailSender) Deliver(_ context.Context, req otpdelivery.Request) (otpdelivery.Result, error) {
|
|
s.to = req.Recipient
|
|
s.code = req.Code
|
|
return otpdelivery.Result{}, nil
|
|
}
|
|
|
|
func TestConfiguredEmailLoginSharesAttemptsAcrossOfficialCodeCarriers(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
authz := memory.NewAuthorizationStore()
|
|
if _, err := users.Create(ctx, domain.User{Phone: "15550009101", FirstName: "Email"}); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
emails := &testLoginEmailStore{emails: map[string]string{"15550009101": "alice@example.test"}}
|
|
sender := &testMailSender{}
|
|
appDelivery := &captureLoginCodeDelivery{}
|
|
svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345",
|
|
WithLoginCodeDelivery(appDelivery),
|
|
WithLoginEmail(LoginEmailOptions{
|
|
Enabled: true,
|
|
CodeLength: 6,
|
|
Store: emails,
|
|
Sender: sender,
|
|
}),
|
|
WithCodeMaxAttempts(2))
|
|
|
|
hash, err := svc.SendCode(ctx, "+1 555 000 9101")
|
|
if err != nil {
|
|
t.Fatalf("SendCode: %v", err)
|
|
}
|
|
if sender.to != "alice@example.test" || len(sender.code) != 6 {
|
|
t.Fatalf("sent email to/code = %q/%q, want alice@example.test/6 digits", sender.to, sender.code)
|
|
}
|
|
if len(appDelivery.requests) != 1 || appDelivery.requests[0].PhoneCodeHash != hash || appDelivery.requests[0].Code != sender.code {
|
|
t.Fatalf("App-code delivery=%+v, want same email code/hash", appDelivery.requests)
|
|
}
|
|
delivery, found, err := svc.CodeDelivery(ctx, hash)
|
|
if err != nil || !found {
|
|
t.Fatalf("CodeDelivery found=%v err=%v", found, err)
|
|
}
|
|
if delivery.Kind != domain.AuthCodeDeliveryEmail || delivery.EmailPattern != "a***e@example.test" || delivery.Length != 6 {
|
|
t.Fatalf("delivery = %+v, want email masked length 6", delivery)
|
|
}
|
|
bad1 := wrongCode(sender.code, '0')
|
|
bad2 := wrongCode(sender.code, '1')
|
|
if bad2 == bad1 {
|
|
bad2 = wrongCode(sender.code, '2')
|
|
}
|
|
if _, _, _, err := svc.SignIn(ctx, domain.Authorization{}, "+15550009101", hash, bad1); !errors.Is(err, ErrCodeInvalid) {
|
|
t.Fatalf("first bad WebK SignIn err = %v, want ErrCodeInvalid", err)
|
|
}
|
|
if _, _, _, err := svc.SignInWithEmail(ctx, domain.Authorization{}, "+15550009101", hash, bad2); !errors.Is(err, ErrCodeInvalid) {
|
|
t.Fatalf("second bad native SignInWithEmail err = %v, want ErrCodeInvalid", err)
|
|
}
|
|
if _, _, _, err := svc.SignIn(ctx, domain.Authorization{}, "+15550009101", hash, sender.code); !errors.Is(err, ErrCodeExpired) {
|
|
t.Fatalf("WebK SignIn after shared max attempts err = %v, want ErrCodeExpired", err)
|
|
}
|
|
}
|
|
|
|
func wrongCode(code string, digit byte) string {
|
|
if code == "" {
|
|
return string(digit)
|
|
}
|
|
out := make([]byte, len(code))
|
|
for i := range out {
|
|
out[i] = digit
|
|
}
|
|
if string(out) != code {
|
|
return string(out)
|
|
}
|
|
for i := range out {
|
|
out[i] = '9'
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func TestConfiguredEmailLoginAcceptsOfficialCodeCarriers(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
phone string
|
|
email string
|
|
webK bool
|
|
}{
|
|
{name: "webk_phone_code", phone: "15550009102", email: "webk@example.test", webK: true},
|
|
{name: "native_email_verification", phone: "15550009103", email: "native@example.test"},
|
|
}
|
|
|
|
for i, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
authz := memory.NewAuthorizationStore()
|
|
u, err := users.Create(ctx, domain.User{Phone: tc.phone, FirstName: "Email"})
|
|
if err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
emails := &testLoginEmailStore{emails: map[string]string{tc.phone: tc.email}}
|
|
sender := &testMailSender{}
|
|
appDelivery := &captureLoginCodeDelivery{}
|
|
var key [8]byte
|
|
key[0] = byte(0x91 + i)
|
|
svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345",
|
|
WithLoginCodeDelivery(appDelivery),
|
|
WithLoginEmail(LoginEmailOptions{
|
|
Enabled: true,
|
|
CodeLength: 6,
|
|
Store: emails,
|
|
Sender: sender,
|
|
}))
|
|
|
|
hash, err := svc.SendCode(ctx, tc.phone)
|
|
if err != nil {
|
|
t.Fatalf("SendCode: %v", err)
|
|
}
|
|
if len(appDelivery.requests) != 1 || appDelivery.requests[0].Code != sender.code {
|
|
t.Fatalf("App-code delivery=%+v, want same email code", appDelivery.requests)
|
|
}
|
|
|
|
var got domain.User
|
|
var needSignUp bool
|
|
if tc.webK {
|
|
if _, _, _, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: key}, tc.phone, hash, "12345"); !errors.Is(err, ErrCodeInvalid) {
|
|
t.Fatalf("WebK development code err=%v, want ErrCodeInvalid for random email channel", err)
|
|
}
|
|
got, _, needSignUp, err = svc.SignIn(ctx, domain.Authorization{AuthKeyID: key}, tc.phone, hash, sender.code)
|
|
} else {
|
|
got, _, needSignUp, err = svc.SignInWithEmail(ctx, domain.Authorization{AuthKeyID: key}, tc.phone, hash, sender.code)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("sign in: %v", err)
|
|
}
|
|
if needSignUp || got.ID != u.ID {
|
|
t.Fatalf("sign in got user=%d needSignUp=%v, want %d/false", got.ID, needSignUp, u.ID)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfiguredEmailLoginViaWebKStillHonorsTwoFactor(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
authz := memory.NewAuthorizationStore()
|
|
passwords := memory.NewPasswordStore()
|
|
u, err := users.Create(ctx, domain.User{Phone: "15550009104", FirstName: "Email"})
|
|
if err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
if err := passwords.Save(ctx, u.ID, domain.PasswordSettings{HasPassword: true}); err != nil {
|
|
t.Fatalf("save password settings: %v", err)
|
|
}
|
|
sender := &testMailSender{}
|
|
svc := NewService(users, authz, memory.NewCodeStore(), nil, nil, "12345",
|
|
WithPasswords(passwords),
|
|
WithLoginCodeDelivery(&captureLoginCodeDelivery{}),
|
|
WithLoginEmail(LoginEmailOptions{
|
|
Enabled: true,
|
|
CodeLength: 6,
|
|
Store: &testLoginEmailStore{emails: map[string]string{u.Phone: "2fa@example.test"}},
|
|
Sender: sender,
|
|
}))
|
|
var key [8]byte
|
|
key[0] = 0x94
|
|
|
|
hash, err := svc.SendCode(ctx, u.Phone)
|
|
if err != nil {
|
|
t.Fatalf("SendCode: %v", err)
|
|
}
|
|
got, _, _, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: key}, u.Phone, hash, sender.code)
|
|
if !errors.Is(err, domain.ErrSessionPasswordNeeded) {
|
|
t.Fatalf("WebK email SignIn err=%v, want ErrSessionPasswordNeeded", err)
|
|
}
|
|
if got.ID != u.ID {
|
|
t.Fatalf("WebK email SignIn user=%d, want pending 2FA user %d", got.ID, u.ID)
|
|
}
|
|
if bound, found, err := svc.UserID(ctx, key); err != nil || found || bound != 0 {
|
|
t.Fatalf("UserID after WebK email SignIn with 2FA=%d found=%v err=%v, want not-found", bound, found, err)
|
|
}
|
|
}
|
|
|
|
func TestConfiguredEmailLoginHasSingleConsumerAcrossOfficialCodeCarriers(t *testing.T) {
|
|
ctx := context.Background()
|
|
users := memory.NewUserStore()
|
|
u, err := users.Create(ctx, domain.User{Phone: "15550009105", FirstName: "Email"})
|
|
if err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
sender := &testMailSender{}
|
|
svc := NewService(users, memory.NewAuthorizationStore(), memory.NewCodeStore(), nil, nil, "12345",
|
|
WithLoginCodeDelivery(&captureLoginCodeDelivery{}),
|
|
WithLoginEmail(LoginEmailOptions{
|
|
Enabled: true,
|
|
CodeLength: 6,
|
|
Store: &testLoginEmailStore{emails: map[string]string{u.Phone: "race@example.test"}},
|
|
Sender: sender,
|
|
}))
|
|
hash, err := svc.SendCode(ctx, u.Phone)
|
|
if err != nil {
|
|
t.Fatalf("SendCode: %v", err)
|
|
}
|
|
|
|
start := make(chan struct{})
|
|
results := make(chan error, 2)
|
|
var webKKey, nativeKey [8]byte
|
|
webKKey[0] = 0x95
|
|
nativeKey[0] = 0x96
|
|
go func() {
|
|
<-start
|
|
_, _, _, err := svc.SignIn(ctx, domain.Authorization{AuthKeyID: webKKey}, u.Phone, hash, sender.code)
|
|
results <- err
|
|
}()
|
|
go func() {
|
|
<-start
|
|
_, _, _, err := svc.SignInWithEmail(ctx, domain.Authorization{AuthKeyID: nativeKey}, u.Phone, hash, sender.code)
|
|
results <- err
|
|
}()
|
|
close(start)
|
|
|
|
accepted, expired := 0, 0
|
|
for range 2 {
|
|
err := <-results
|
|
switch {
|
|
case err == nil:
|
|
accepted++
|
|
case errors.Is(err, ErrCodeExpired):
|
|
expired++
|
|
default:
|
|
t.Fatalf("concurrent sign in err=%v, want nil or ErrCodeExpired", err)
|
|
}
|
|
}
|
|
if accepted != 1 || expired != 1 {
|
|
t.Fatalf("concurrent results accepted=%d expired=%d, want 1/1", accepted, expired)
|
|
}
|
|
}
|