From 8c779e51d2f04038839967f0125aaddd0bed639b Mon Sep 17 00:00:00 2001 From: celestix <73958752+celestix@users.noreply.github.com> Date: Sun, 21 Apr 2024 23:53:27 +0530 Subject: [PATCH] feat: auth status events and retry mechanism for phone and otp input This commit adds up retry mechanism for phone number input and otp as well also with the 3 max trials like for 2FA password. This commit also brings a new type of events that are supplied by AuthConversator known as "AuthStatus" events. These events contain data for attempts left for the current event as well as a string containing the data about on going event. List of available auth status events: AuthStatusPhoneAsked = AuthStatusEvent("phone number asked") AuthStatusPhoneRetrial = AuthStatusEvent("phone number validation retrial") AuthStatusPhoneFailed = AuthStatusEvent("phone number validation failed") AuthStatusPhoneCodeAsked = AuthStatusEvent("phone otp asked") AuthStatusPhoneCodeVerified = AuthStatusEvent("phone code verified") AuthStatusPhoneCodeRetrial = AuthStatusEvent("phone code verification retrial") AuthStatusPhoneCodeFailed = AuthStatusEvent("phone code verification failed") AuthStatusPasswordAsked = AuthStatusEvent("2fa password asked") AuthStatusPasswordRetrial = AuthStatusEvent("2fa password verification retrial") AuthStatusPasswordFailed = AuthStatusEvent("2fa password verification failed") AuthStatusSuccess = AuthStatusEvent("authentification success") --- authConversation.go | 70 +++++++++++++++--- authHelper.go | 82 ++++++++++++++++++--- examples/auth-using-api-base/web/api.go | 5 ++ examples/auth-using-api-base/web/webauth.go | 24 ++++-- 4 files changed, 156 insertions(+), 25 deletions(-) diff --git a/authConversation.go b/authConversation.go index 6694163..ec6e2d4 100644 --- a/authConversation.go +++ b/authConversation.go @@ -6,6 +6,41 @@ import ( "os" ) +type ( + AuthStatusEvent string + AuthStatus struct { + Event AuthStatusEvent + AttemptsLeft int + } +) + +func SendAuthStatus(conversator AuthConversator, event AuthStatusEvent) { + conversator.AuthStatus(AuthStatus{ + Event: event, + }) +} + +func SendAuthStatusWithRetrials(conversator AuthConversator, event AuthStatusEvent, attemptsLeft int) { + conversator.AuthStatus(AuthStatus{ + Event: event, + AttemptsLeft: attemptsLeft, + }) +} + +var ( + AuthStatusPhoneAsked = AuthStatusEvent("phone number asked") + AuthStatusPhoneRetrial = AuthStatusEvent("phone number validation retrial") + AuthStatusPhoneFailed = AuthStatusEvent("phone number validation failed") + AuthStatusPhoneCodeAsked = AuthStatusEvent("phone otp asked") + AuthStatusPhoneCodeVerified = AuthStatusEvent("phone code verified") + AuthStatusPhoneCodeRetrial = AuthStatusEvent("phone code verification retrial") + AuthStatusPhoneCodeFailed = AuthStatusEvent("phone code verification failed") + AuthStatusPasswordAsked = AuthStatusEvent("2fa password asked") + AuthStatusPasswordRetrial = AuthStatusEvent("2fa password verification retrial") + AuthStatusPasswordFailed = AuthStatusEvent("2fa password verification failed") + AuthStatusSuccess = AuthStatusEvent("authentification success") +) + // AuthConversator is an interface for asking user for auth information. type AuthConversator interface { // AskPhoneNumber is called to ask user for phone number. @@ -17,36 +52,51 @@ type AuthConversator interface { // AskPassword is called to ask user for 2FA password. // 2FA password should be returned. AskPassword() (string, error) - // RetryPassword is called when the 2FA password is incorrect - // attemptsLeft is the number of attempts left. - // 2FA password should be returned. - RetryPassword(attemptsLeft int) (string, error) + // SendAuthStatus is called to inform the user about + // the status of the auth process. + // attemptsLeft is the number of attempts left for the user + // to enter the input correctly for the current auth status. + AuthStatus(authStatus AuthStatus) } func BasicConversator() AuthConversator { return &basicConservator{} } -type basicConservator struct{} +type basicConservator struct { + authStatus AuthStatus +} func (b *basicConservator) AskPhoneNumber() (string, error) { + if b.authStatus.Event == AuthStatusPhoneRetrial { + fmt.Println("The phone number you just entered seems to be incorrect,") + fmt.Println("Attempts Left:", b.authStatus.AttemptsLeft) + fmt.Println("Please try again....") + } fmt.Print("Enter Phone Number: ") return bufio.NewReader(os.Stdin).ReadString('\n') } func (b *basicConservator) AskPassword() (string, error) { + if b.authStatus.Event == AuthStatusPasswordRetrial { + fmt.Println("The 2FA password you just entered seems to be incorrect,") + fmt.Println("Attempts Left:", b.authStatus.AttemptsLeft) + fmt.Println("Please try again....") + } fmt.Print("Enter 2FA password: ") return bufio.NewReader(os.Stdin).ReadString('\n') } func (b *basicConservator) AskCode() (string, error) { + if b.authStatus.Event == AuthStatusPhoneCodeRetrial { + fmt.Println("The OTP you just entered seems to be incorrect,") + fmt.Println("Attempts Left:", b.authStatus.AttemptsLeft) + fmt.Println("Please try again....") + } fmt.Print("Enter Code: ") return bufio.NewReader(os.Stdin).ReadString('\n') } -func (b *basicConservator) RetryPassword(trialsLeft int) (string, error) { - fmt.Println("The 2FA Code you just entered seems to be incorrect,") - fmt.Println("Attempts Left:", trialsLeft) - fmt.Println("Please try again....") - return b.AskPassword() +func (b *basicConservator) AuthStatus(authStatus AuthStatus) { + b.authStatus = authStatus } diff --git a/authHelper.go b/authHelper.go index 3c0ab05..b5a4aeb 100644 --- a/authHelper.go +++ b/authHelper.go @@ -5,6 +5,7 @@ import ( "github.com/gotd/td/telegram/auth" "github.com/gotd/td/tg" + "github.com/gotd/td/tgerr" "github.com/pkg/errors" ) @@ -57,24 +58,78 @@ func authFlow(ctx context.Context, client *auth.Client, conversator AuthConversa return errors.New("no UserAuthenticator provided") } - phone, err := f.Auth.Phone(ctx) - if err != nil { - return errors.Wrap(err, "get phone") + var ( + sentCode tg.AuthSentCodeClass + err error + ) + SendAuthStatus(conversator, AuthStatusPhoneAsked) + for i := 0; i < 3; i++ { + var phone string + var err1 error + if i == 0 { + phone, err1 = f.Auth.Phone(ctx) + } else { + SendAuthStatusWithRetrials(conversator, AuthStatusPhoneAsked, 3-i) + phone, err1 = conversator.AskPhoneNumber() + } + if err1 != nil { + return errors.Wrap(err, "get phone") + } + sentCode, err = client.SendCode(ctx, phone, f.Options) + if err == nil { + SendAuthStatus(conversator, AuthStatusPhoneCodeAsked) + break + } + if tgerr.Is(err, "PHONE_NUMBER_INVALID") { + continue + } } - - sentCode, err := client.SendCode(ctx, phone, f.Options) if err != nil { + SendAuthStatus(conversator, AuthStatusPhoneFailed) return err } + + // phone, err := f.Auth.Phone(ctx) + // if err != nil { + // return errors.Wrap(err, "get phone") + // } + + // sentCode, err := client.SendCode(ctx, phone, f.Options) + // if err != nil { + // return err + // } switch s := sentCode.(type) { case *tg.AuthSentCode: hash := s.PhoneCodeHash - code, err := f.Auth.Code(ctx, s) - if err != nil { - return errors.Wrap(err, "get code") + var signInErr error + for i := 0; i < 3; i++ { + var code string + if i == 0 { + code, err = f.Auth.Code(ctx, s) + } else { + SendAuthStatusWithRetrials(conversator, AuthStatusPhoneCodeRetrial, 3-i) + code, err = conversator.AskCode() + } + if err != nil { + SendAuthStatus(conversator, AuthStatusPhoneCodeFailed) + return errors.Wrap(err, "get code") + } + _, signInErr = client.SignIn(ctx, phone, code, hash) + if signInErr == nil { + break + } + if tgerr.Is(signInErr, "PHONE_CODE_INVALID") { + continue + } } - _, signInErr := client.SignIn(ctx, phone, code, hash) + // code, err := f.Auth.Code(ctx, s) + // if err != nil { + // return errors.Wrap(err, "get code") + // } + // _, signInErr := client.SignIn(ctx, phone, code, hash) + if errors.Is(signInErr, auth.ErrPasswordAuthNeeded) { + SendAuthStatus(conversator, AuthStatusPasswordAsked) err = signInErr for i := 0; err != nil && i < 3; i++ { var password string @@ -82,14 +137,19 @@ func authFlow(ctx context.Context, client *auth.Client, conversator AuthConversa if i == 0 { password, err1 = f.Auth.Password(ctx) } else { - password, err1 = conversator.RetryPassword(3 - i) + SendAuthStatusWithRetrials(conversator, AuthStatusPasswordRetrial, 3-i) + password, err1 = conversator.AskPassword() } if err1 != nil { return errors.Wrap(err1, "get password") } _, err = client.Password(ctx, password) + if err == nil { + break + } } if err != nil { + SendAuthStatus(conversator, AuthStatusPasswordFailed) return errors.Wrap(err, "sign in with password") } return nil @@ -100,11 +160,13 @@ func authFlow(ctx context.Context, client *auth.Client, conversator AuthConversa } if signInErr != nil { + SendAuthStatus(conversator, AuthStatusPhoneCodeFailed) return errors.Wrap(signInErr, "sign in") } case *tg.AuthSentCodeSuccess: switch a := s.Authorization.(type) { case *tg.AuthAuthorization: + SendAuthStatus(conversator, AuthStatusSuccess) // Looks that we are already authorized. return nil case *tg.AuthAuthorizationSignUpRequired: diff --git a/examples/auth-using-api-base/web/api.go b/examples/auth-using-api-base/web/api.go index 563e1bb..d1d86de 100644 --- a/examples/auth-using-api-base/web/api.go +++ b/examples/auth-using-api-base/web/api.go @@ -8,9 +8,14 @@ import ( // Start a web server and wait func Start() { http.HandleFunc("/", setInfo) + http.HandleFunc("/getAuthStatus", getAuthStatus) http.ListenAndServe(":9997", nil) } +func getAuthStatus(w http.ResponseWriter, req *http.Request) { + fmt.Fprint(w, authStatus) +} + // setInfo handle user info, set phone, code or passwd func setInfo(w http.ResponseWriter, req *http.Request) { action := req.URL.Query().Get("set") diff --git a/examples/auth-using-api-base/web/webauth.go b/examples/auth-using-api-base/web/webauth.go index 1df6331..6b77edd 100644 --- a/examples/auth-using-api-base/web/webauth.go +++ b/examples/auth-using-api-base/web/webauth.go @@ -6,6 +6,8 @@ import ( "github.com/celestix/gotgproto" ) +var authStatus gotgproto.AuthStatus + type webAuth struct{} var ( @@ -19,28 +21,40 @@ func GetWebAuth() gotgproto.AuthConversator { } func (w *webAuth) AskPhoneNumber() (string, error) { + if authStatus.Event == gotgproto.AuthStatusPhoneRetrial { + fmt.Println("The phone number you just entered seems to be incorrect,") + fmt.Println("Attempts Left:", authStatus.AttemptsLeft) + fmt.Println("Please try again....") + } fmt.Println("waiting for phone...") code := <-phoneChan return code, nil } func (w *webAuth) AskCode() (string, error) { + if authStatus.Event == gotgproto.AuthStatusPhoneCodeRetrial { + fmt.Println("The OTP you just entered seems to be incorrect,") + fmt.Println("Attempts Left:", authStatus.AttemptsLeft) + fmt.Println("Please try again....") + } fmt.Println("waiting for code...") code := <-codeChan return code, nil } func (w *webAuth) AskPassword() (string, error) { + if authStatus.Event == gotgproto.AuthStatusPasswordRetrial { + fmt.Println("The 2FA password you just entered seems to be incorrect,") + fmt.Println("Attempts Left:", authStatus.AttemptsLeft) + fmt.Println("Please try again....") + } fmt.Println("waiting for 2fa password...") code := <-passwdChan return code, nil } -func (w *webAuth) RetryPassword(attemptsLeft int) (string, error) { - fmt.Println("The 2FA Code you just entered seems to be incorrect,") - fmt.Println("Attempts Left:", attemptsLeft) - fmt.Println("Please try again.... ") - return w.AskPassword() +func (w *webAuth) AuthStatus(authStatusIp gotgproto.AuthStatus) { + authStatus = authStatusIp } func ReceivePhone(phone string) {