Skip to content

Commit

Permalink
refactor request reset passwod
Browse files Browse the repository at this point in the history
  • Loading branch information
erudenko committed Jul 13, 2023
1 parent 0ea482b commit 20df54b
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 114 deletions.
2 changes: 2 additions & 0 deletions l/messages_const.go

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

1 change: 1 addition & 0 deletions l/translations/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ error.invalid.phone: "invalid phone number"
error.email.empty: "email could not be empty"
error.phone.empty: "phone could not be empty"
error.username.empty: "username could not be empty"
error.login.data.empty: "Your login data is empty."
error.user.not_found: User not found.
error.user.not_found.error: User not found with error; %v.
error.request.challenge.unsupported.by.app: This login type is not supported by the app.
Expand Down
1 change: 1 addition & 0 deletions model/user_auth_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ const (
AuthIdentityTypePhone AuthIdentityType = "phone"
AuthIdentityTypeUsername AuthIdentityType = "username"
AuthIdentityTypeAnonymous AuthIdentityType = "anonymous"
AuthIdentityTypeNone AuthIdentityType = "none"

// AuthChallengeType - the challenge type we are using to auth the sure.
AuthChallengeTypePassword AuthChallengeType = "password"
Expand Down
154 changes: 40 additions & 114 deletions web/api/reset_password.go
Original file line number Diff line number Diff line change
@@ -1,158 +1,84 @@
package api

import (
"fmt"
"errors"
"net/http"
"net/url"

"github.com/madappgang/identifo/v2/l"
"github.com/madappgang/identifo/v2/model"
"github.com/madappgang/identifo/v2/web/middleware"
)

// RequestResetPassword requests password reset
// now we support reset password only with JWT reset token send to email
// if user does not have email - we could not reset the password
// we need to find user by secondary ID (phone, username, email)
// if user does not exist - we just silently return ok
// if user exists - we send reset password email
// if there is not email for user - we return error, as it's configuration error
func (ar *Router) RequestResetPassword() http.HandlerFunc {
type resetRequest struct {
login
TFACode string `json:"tfa_code,omitempty"`
Phone string `json:"phone"`
Email string `json:"email"`
Username string `json:"username"`
ResetPageURL string `json:"reset_page_url,omitempty"`
}

return func(w http.ResponseWriter, r *http.Request) {
locale := r.Header.Get("Accept-Language")
// agent := r.Header.Get("User-Agent")
app := middleware.AppFromContext(r.Context())

d := resetRequest{}
if ar.MustParseJSON(w, r, &d) != nil {
return
}

if err := d.login.validate(); err != nil {
ar.LocalizedError(w, locale, http.StatusBadRequest, l.ErrorAPIRequestBodyInvalidError, err)
return
}

if err := ar.checkSupportedWays(d.login); err != nil {
ar.LocalizedError(w, locale, http.StatusBadRequest, l.APIAPPUsernameLoginNotSupported)
return
}

user, err := ar.server.Storages().User.UserByEmail(d.Email)
if err == l.ErrorUserNotFound {
// return ok, but there is no user
ar.Logger.Printf("Trying to reset password for the user, which is not exists: %s. Sending back ok to user for security reason.", d.Email)
result := map[string]string{"result": "ok"}
ar.ServeJSON(w, locale, http.StatusOK, result)
return
} else if err != nil {
ar.LocalizedError(w, locale, http.StatusInternalServerError, l.ErrorStorageFindUserEmailError, d.Email, err)
return
idType := model.AuthIdentityTypeNone
idValue := ""
if len(d.Email) > 0 {
idType = model.AuthIdentityTypeEmail
idValue = d.Email
} else if len(d.Phone) > 0 {
idType = model.AuthIdentityTypePhone
idValue = d.Phone
} else if len(d.Username) > 0 {
idType = model.AuthIdentityTypeUsername
idValue = d.Username
}

app := middleware.AppFromContext(r.Context())
if len(app.ID) == 0 {
ar.LocalizedError(w, locale, http.StatusBadRequest, l.ErrorAPIAPPNoAPPInContext)
// nothing filled
if idType == model.AuthIdentityTypeNone {
ar.LocalizedError(w, locale, http.StatusBadRequest, l.ErrorLoginDataEmpty)
return
}

_, enabled2FA, _ := ar.check2FA(app.TFAStatus, ar.tfaType, user)

if enabled2FA && ar.tfaType != model.TFATypeEmail {
if d.TFACode != "" {
otpVerified, err := ar.verifyOTPCode(user, d.TFACode)
if err != nil {
ar.Error(w, locale, http.StatusForbidden, l.Error2FAVerifyFailError, err)
return
}

dontNeedVerification := app.DebugTFACode != "" && d.TFACode == app.DebugTFACode

if !(otpVerified || dontNeedVerification) {
ar.Error(w, locale, http.StatusUnauthorized, l.ErrorAPILoginCodeInvalid)
return
}
} else {
if err := ar.sendOTPCode(app, user); err != nil {
ar.Error(w, locale, http.StatusInternalServerError, l.ErrorServiceOtpSendError, err)
return
}
result := map[string]string{"result": "tfa-required"}
ar.ServeJSON(w, locale, http.StatusOK, result)
return
}
}

resetToken, err := ar.server.Services().Token.NewResetToken(user.ID)
if err != nil {
ar.LocalizedError(w, locale, http.StatusInternalServerError, l.ErrorTokenUnableToCreateResetTokenError, err)
respok := map[string]string{"result": "ok"}
user, err := ar.server.Storages().UC.UserBySecondaryID(r.Context(), idType, idValue)
if err != nil && !errors.Is(err, l.ErrorUserNotFound) {
ar.LocalizedError(w, locale, http.StatusInternalServerError, l.LocalizedString(err.Error()))
return
}

resetTokenString, err := ar.server.Services().Token.String(resetToken)
if err != nil {
ar.LocalizedError(w, locale, http.StatusInternalServerError, l.ErrorTokenUnableToCreateResetTokenError, err)
} else if !errors.Is(err, l.ErrorUserNotFound) {
// if not user - just report ok for security reasons
ar.ServeJSON(w, locale, http.StatusOK, respok)
return
}

query := fmt.Sprintf("appId=%s&token=%s", app.ID, resetTokenString)
u := &url.URL{
Scheme: ar.Host.Scheme,
Host: ar.Host.Host,
RawQuery: query,
}

resetPath := model.DefaultLoginWebAppSettings.ResetPasswordURL

// if app requested reset password custom page, use it.
if len(d.ResetPageURL) > 0 {
resetPath = d.ResetPageURL
} else if app.LoginAppSettings != nil && len(app.LoginAppSettings.ResetPasswordURL) > 0 {
// rewrite path for app, if app has specific web app login settings
resetPath = app.LoginAppSettings.ResetPasswordURL
}

resetPathURL, err := url.Parse(resetPath)
// we have user
_, err = ar.server.Storages().UMC.SendPasswordResetEmail(r.Context(), user.ID, app.ID)
if err != nil {
ar.LocalizedError(w, locale, http.StatusInternalServerError, l.ErrorAPPResetUrlError, resetPath, app.ID, err)
return
}

// app settings could rewrite host or just path, if path is absolute - it rewrites host as well
if resetPathURL.IsAbs() {
u.Scheme = resetPathURL.Scheme
u.Host = resetPathURL.Host
}

u.Path = resetPathURL.Path

uu := &url.URL{Scheme: u.Scheme, Host: u.Host, Path: u.Path}

resetEmailData := ResetEmailData{
User: user,
Token: resetTokenString,
URL: u.String(),
Host: uu.String(),
}

if err = ar.server.Services().Email.SendTemplateEmail(
model.EmailTemplateTypeResetPassword,
app.GetCustomEmailTemplatePath(),
"Reset Password",
d.Email,
model.EmailData{
User: user,
Data: resetEmailData,
},
); err != nil {
ar.LocalizedError(w, locale, http.StatusInternalServerError, l.ErrorServiceEmailSendError, err)
// TODO: generate proper localized error with details
ar.LocalizedError(w, locale, http.StatusInternalServerError, l.LocalizedString(err.Error()))
return
}

result := map[string]string{"result": "ok"}
ar.ServeJSON(w, locale, http.StatusOK, result)
ar.ServeJSON(w, locale, http.StatusOK, respok)
}
}

// ResetPassword handles password reset form submission (POST request).
// this method exchanges reset JWT token to access JWT token
// getting the new password and saving it in the database.
func (ar *Router) ResetPassword() http.HandlerFunc {
type newPassword struct {
Password string `json:"password,omitempty"`
Expand Down

0 comments on commit 20df54b

Please sign in to comment.