Skip to content

Commit

Permalink
Merge pull request #296 from MadAppGang/fix-phone-error
Browse files Browse the repository at this point in the history
Fix phone taken user error. Add separate 2fa phone/email and enable tfa on verify.
  • Loading branch information
erudenko authored Feb 4, 2022
2 parents 61a5c67 + 99db444 commit 97f8ddf
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 119 deletions.
2 changes: 2 additions & 0 deletions model/user_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ type TFAInfo struct {
IsEnabled bool `json:"is_enabled" bson:"is_enabled"`
HOTPCounter int `json:"hotp_counter" bson:"hotp_counter"`
HOTPExpiredAt time.Time `json:"hotp_expired_at" bson:"hotp_expired_at"`
Email string `json:"email" bson:"email"`
Phone string `json:"phone" bson:"phone"`
Secret string `json:"secret" bson:"secret"`
}

Expand Down
143 changes: 124 additions & 19 deletions web/api/2fa.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,23 @@ type ResetEmailData struct {

// EnableTFA enables two-factor authentication for the user.
func (ar *Router) EnableTFA() http.HandlerFunc {
type requestBody struct {
Email string `json:"email"`
Phone string `json:"phone"`
}

type tfaSecret struct {
AccessToken string `json:"access_token,omitempty"`
ProvisioningURI string `json:"provisioning_uri,omitempty"`
ProvisioningQR string `json:"provisioning_qr,omitempty"`
}

return func(w http.ResponseWriter, r *http.Request) {
d := requestBody{}
if ar.MustParseJSON(w, r, &d) != nil {
return
}

app := middleware.AppFromContext(r.Context())
if len(app.ID) == 0 {
ar.Error(w, ErrorAPIRequestAppIDInvalid, http.StatusBadRequest, "App is not in context.", "EnableTFA.AppFromContext")
Expand Down Expand Up @@ -69,25 +79,6 @@ func (ar *Router) EnableTFA() http.HandlerFunc {
return
}

if ar.tfaType == model.TFATypeSMS && user.Phone == "" {
ar.Error(w, ErrorAPIRequestPleaseSetPhoneForTFA, http.StatusBadRequest, "Please specify your phone number to be able to receive one-time passwords", "EnableTFA.setPhone")
return
}
if ar.tfaType == model.TFATypeEmail && user.Email == "" {
ar.Error(w, ErrorAPIRequestPleaseSetEmailForTFA, http.StatusBadRequest, "Please specify your email address to be able to receive one-time passwords", "EnableTFA.setEmail")
return
}

user.TFAInfo = model.TFAInfo{
IsEnabled: true,
Secret: gotp.RandomSecret(16),
}

if _, err := ar.server.Storages().User.UpdateUser(userID, user); err != nil {
ar.Error(w, ErrorAPIInternalServerError, http.StatusInternalServerError, err.Error(), "EnableTFA.UpdateUser")
return
}

tokenPayload, err := ar.getTokenPayloadForApp(app, user)
if err != nil {
ar.Error(w, ErrorAPIAppAccessTokenNotCreated, http.StatusInternalServerError, err.Error(), "EnableTFA.accessToken")
Expand All @@ -102,6 +93,18 @@ func (ar *Router) EnableTFA() http.HandlerFunc {

switch ar.tfaType {
case model.TFATypeApp:
// For app we just enable tfa and generate secret
user.TFAInfo = model.TFAInfo{
IsEnabled: true,
Secret: gotp.RandomSecret(16),
}

if _, err := ar.server.Storages().User.UpdateUser(userID, user); err != nil {
ar.Error(w, ErrorAPIInternalServerError, http.StatusInternalServerError, err.Error(), "EnableTFA.UpdateUser")
return
}

// Send new provising uri for authenticator
uri := gotp.NewDefaultTOTP(user.TFAInfo.Secret).ProvisioningUri(user.Username, app.Name)

var png []byte
Expand All @@ -115,6 +118,28 @@ func (ar *Router) EnableTFA() http.HandlerFunc {
ar.ServeJSON(w, http.StatusOK, &tfaSecret{ProvisioningURI: uri, ProvisioningQR: encoded, AccessToken: accessToken})
return
case model.TFATypeSMS, model.TFATypeEmail:
// If 2fa is SMS or Email we set it in TFAInfo and enable it only when it will be verified
if ar.tfaType == model.TFATypeSMS {
if d.Phone == "" {
ar.Error(w, ErrorAPIRequestPleaseSetPhoneForTFA, http.StatusBadRequest, "Please specify your phone number to be able to receive one-time passwords", "EnableTFA.setPhone")
return
}
user.TFAInfo = model.TFAInfo{Phone: d.Phone}
}
if ar.tfaType == model.TFATypeEmail {
if d.Email == "" {
ar.Error(w, ErrorAPIRequestPleaseSetEmailForTFA, http.StatusBadRequest, "Please specify your email address to be able to receive one-time passwords", "EnableTFA.setEmail")
return
}
user.TFAInfo = model.TFAInfo{Email: d.Email}
}

if _, err := ar.server.Storages().User.UpdateUser(userID, user); err != nil {
ar.Error(w, ErrorAPIInternalServerError, http.StatusInternalServerError, err.Error(), "EnableTFA.UpdateUser")
return
}

// And send OTP code for 2fa
if err := ar.sendOTPCode(user); err != nil {
ar.Error(w, ErrorAPIRequestUnableToSendOTP, http.StatusInternalServerError, err.Error(), "EnableTFA.sendOTP")
return
Expand Down Expand Up @@ -269,6 +294,19 @@ func (ar *Router) FinalizeTFA() http.HandlerFunc {
User: user,
}

// Enable TFA after verify if it not enabled
if !user.TFAInfo.IsEnabled {
user.TFAInfo = model.TFAInfo{
IsEnabled: true,
Secret: gotp.RandomSecret(16),
}

if _, err := ar.server.Storages().User.UpdateUser(userID, user); err != nil {
ar.Error(w, ErrorAPIInternalServerError, http.StatusInternalServerError, err.Error(), "EnableTFA.UpdateUser")
return
}
}

ar.server.Storages().User.UpdateLoginMetadata(user.ID)
ar.ServeJSON(w, http.StatusOK, result)
}
Expand Down Expand Up @@ -479,3 +517,70 @@ func (ar *Router) RequestTFAReset() http.HandlerFunc {
ar.ServeJSON(w, http.StatusOK, result)
}
}

// check2FA checks correspondence between app's TFAstatus and user's TFAInfo,
// and decides if we require two-factor authentication after all checks are successfully passed.
func (ar *Router) check2FA(appTFAStatus model.TFAStatus, serverTFAType model.TFAType, user model.User) (bool, bool, error) {
if appTFAStatus == model.TFAStatusMandatory && !user.TFAInfo.IsEnabled {
return true, false, errPleaseEnableTFA
}

if appTFAStatus == model.TFAStatusDisabled && user.TFAInfo.IsEnabled {
return false, true, errPleaseDisableTFA
}

// Request two-factor auth if user enabled it and app supports it.
if user.TFAInfo.IsEnabled && appTFAStatus != model.TFAStatusDisabled {
if user.TFAInfo.Phone == "" && serverTFAType == model.TFATypeSMS {
// Server required sms tfa but user phone is empty
return true, false, errPleaseSetPhoneTFA
}
if user.TFAInfo.Email == "" && serverTFAType == model.TFATypeEmail {
// Server required email tfa but user email is empty
return true, false, errPleaseSetEmailTFA
}
if user.TFAInfo.Secret == "" {
// Then admin must have enabled TFA for this user manually.
// User must obtain TFA secret, i.e send EnableTFA request.
return true, false, errPleaseEnableTFA
}
return true, true, nil
}
return false, false, nil
}

func (ar *Router) sendTFACodeInSMS(phone, otp string) error {
if phone == "" {
return errors.New("unable to send SMS OTP, user has no phone number")
}

if err := ar.server.Services().SMS.SendSMS(phone, fmt.Sprintf(smsTFACode, otp)); err != nil {
return fmt.Errorf("unable to send sms. %s", err)
}
return nil
}

func (ar *Router) sendTFACodeOnEmail(user model.User, otp string) error {
if user.TFAInfo.Email == "" {
return errors.New("unable to send email OTP, user has no email")
}

emailData := SendTFAEmailData{
User: user,
OTP: otp,
}

if err := ar.server.Services().Email.SendTemplateEmail(
model.EmailTemplateTypeTFAWithCode,
"One-time password",
user.TFAInfo.Email,
model.EmailData{
User: user,
Data: emailData,
},
); err != nil {
return fmt.Errorf("unable to send email with OTP with error: %s", err)
}

return nil
}
69 changes: 1 addition & 68 deletions web/api/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func (ar *Router) sendOTPCode(user model.User) error {
}
switch ar.tfaType {
case model.TFATypeSMS:
return ar.sendTFACodeInSMS(user.Phone, otp)
return ar.sendTFACodeInSMS(user.TFAInfo.Phone, otp)
case model.TFATypeEmail:
return ar.sendTFACodeOnEmail(user, otp)
}
Expand Down Expand Up @@ -276,73 +276,6 @@ func (ar *Router) loginUser(user model.User, scopes []string, app model.AppData,
return accessTokenString, refreshTokenString, nil
}

// check2FA checks correspondence between app's TFAstatus and user's TFAInfo,
// and decides if we require two-factor authentication after all checks are successfully passed.
func (ar *Router) check2FA(appTFAStatus model.TFAStatus, serverTFAType model.TFAType, user model.User) (bool, bool, error) {
if appTFAStatus == model.TFAStatusMandatory && !user.TFAInfo.IsEnabled {
return true, false, errPleaseEnableTFA
}

if appTFAStatus == model.TFAStatusDisabled && user.TFAInfo.IsEnabled {
return false, true, errPleaseDisableTFA
}

// Request two-factor auth if user enabled it and app supports it.
if user.TFAInfo.IsEnabled && appTFAStatus != model.TFAStatusDisabled {
if user.Phone == "" && serverTFAType == model.TFATypeSMS {
// Server required sms tfa but user phone is empty
return true, false, errPleaseSetPhoneTFA
}
if user.Email == "" && serverTFAType == model.TFATypeEmail {
// Server required email tfa but user email is empty
return true, false, errPleaseSetEmailTFA
}
if user.TFAInfo.Secret == "" {
// Then admin must have enabled TFA for this user manually.
// User must obtain TFA secret, i.e send EnableTFA request.
return true, false, errPleaseEnableTFA
}
return true, true, nil
}
return false, false, nil
}

func (ar *Router) sendTFACodeInSMS(phone, otp string) error {
if phone == "" {
return errors.New("unable to send SMS OTP, user has no phone number")
}

if err := ar.server.Services().SMS.SendSMS(phone, fmt.Sprintf(smsTFACode, otp)); err != nil {
return fmt.Errorf("unable to send sms. %s", err)
}
return nil
}

func (ar *Router) sendTFACodeOnEmail(user model.User, otp string) error {
if user.Email == "" {
return errors.New("unable to send email OTP, user has no email")
}

emailData := SendTFAEmailData{
User: user,
OTP: otp,
}

if err := ar.server.Services().Email.SendTemplateEmail(
model.EmailTemplateTypeTFAWithCode,
"One-time password",
user.Email,
model.EmailData{
User: user,
Data: emailData,
},
); err != nil {
return fmt.Errorf("unable to send email with OTP with error: %s", err)
}

return nil
}

func (ar *Router) loginFlow(app model.AppData, user model.User, requestedScopes []string) (AuthResponse, error) {
// check if the user has the scope, that allows to login to the app
// user has to have at least one scope app expecting
Expand Down
5 changes: 5 additions & 0 deletions web/api/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var messages = map[MessageID]string{
ErrorAPIUsernameTaken: "Username is taken. Try to choose another one",
ErrorAPIUsernameEmailOrPhoneTaken: "Username, email or/and phone is taken. Try to choose another one",
ErrorAPIEmailTaken: "Email is taken. Try to choose another one",
ErrorAPIPhoneTaken: "Phone is taken. Try to choose another one",
ErrorAPIInviteTokenServerError: "Unable to create invite token. Try again or contact support team",
ErrorAPIInviteUnableToInvalidate: "Unable to invalidate invite. Try again or contact support team",
ErrorAPIInviteUnableToSave: "Unable to save invite. Try again or contact support team",
Expand Down Expand Up @@ -71,6 +72,8 @@ const (
ErrorAPIUsernameEmailOrPhoneTaken = "error.api.username_phone_email.taken"
// ErrorAPIEmailTaken is when email is already taken.
ErrorAPIEmailTaken = "error.api.email.taken"
// ErrorAPIPhoneTaken is when phone is already taken.
ErrorAPIPhoneTaken = "error.api.phone.taken"
// ErrorAPIInviteTokenServerError is for invite token creation issues.
ErrorAPIInviteTokenServerError = "error.api.invite_token.server_error"
// ErrorAPIInviteUnableToInvalidate is when invite cannot be invalidated.
Expand Down Expand Up @@ -123,6 +126,8 @@ const (
ErrorAPIRequestPleaseSetPhoneForTFA = "error.api.request.2fa.set_phone"
// ErrorAPIRequestPleaseSetEmailForTFA means that user must set up their email address to be able to receive OTPs on the email.
ErrorAPIRequestPleaseSetEmailForTFA = "error.api.request.2fa.set_email"
// ErrorAPIRequestEnableTFAEmptyPhoneAndEmail means that the email and phone is empty
ErrorAPIRequestEnableTFAEmptyPhoneAndEmail = "error.api.request.enable_2fa.empty_phone_and_email"
// ErrorAPIRequestUnableToSendOTP means that there is error sending the otp code while login to user
ErrorAPIRequestUnableToSendOTP = "error.api.request.2fa.unable to send OTP code to email or sms"

Expand Down
2 changes: 1 addition & 1 deletion web/api/update_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (ar *Router) UpdateUser() http.HandlerFunc {
// Check that phone is not taken.
if d.updatePhone {
if _, err := ar.server.Storages().User.UserByPhone(d.NewPhone); err == nil {
ar.Error(w, ErrorAPIEmailTaken, http.StatusBadRequest, "", "UpdateUser.updatePhone && UserByPhone")
ar.Error(w, ErrorAPIPhoneTaken, http.StatusBadRequest, "", "UpdateUser.updatePhone && UserByPhone")
return
}
}
Expand Down
5 changes: 4 additions & 1 deletion web_apps_src/identifo.js/dist/identifo.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,10 @@ declare class API {
requestResetPassword(email: string, tfaCode?: string): Promise<SuccessResponse | TFARequiredRespopnse>;
resetPassword(password: string): Promise<SuccessResponse>;
getAppSettings(callbackUrl: string): Promise<AppSettingsResponse>;
enableTFA(): Promise<EnableTFAResponse>;
enableTFA(data: {
phone?: string;
email?: string;
}): Promise<EnableTFAResponse>;
verifyTFA(code: string, scopes: string[]): Promise<LoginResponse>;
resendTFA(): Promise<LoginResponse>;
logout(): Promise<SuccessResponse>;
Expand Down
12 changes: 5 additions & 7 deletions web_apps_src/identifo.js/dist/identifo.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion web_apps_src/identifo.js/dist/identifo.js.map

Large diffs are not rendered by default.

Loading

0 comments on commit 97f8ddf

Please sign in to comment.