summaryrefslogtreecommitdiffstats
path: root/routers/web/auth/password.go
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /routers/web/auth/password.go
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--routers/web/auth/password.go325
1 files changed, 325 insertions, 0 deletions
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
new file mode 100644
index 0000000..363c01c
--- /dev/null
+++ b/routers/web/auth/password.go
@@ -0,0 +1,325 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package auth
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "code.gitea.io/gitea/models/auth"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/auth/password"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/mailer"
+ user_service "code.gitea.io/gitea/services/user"
+)
+
+var (
+ // tplMustChangePassword template for updating a user's password
+ tplMustChangePassword base.TplName = "user/auth/change_passwd"
+ tplForgotPassword base.TplName = "user/auth/forgot_passwd"
+ tplResetPassword base.TplName = "user/auth/reset_passwd"
+)
+
+// ForgotPasswd render the forget password page
+func ForgotPasswd(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
+
+ if setting.MailService == nil {
+ log.Warn("no mail service configured")
+ ctx.Data["IsResetDisable"] = true
+ ctx.HTML(http.StatusOK, tplForgotPassword)
+ return
+ }
+
+ ctx.Data["Email"] = ctx.FormString("email")
+
+ ctx.Data["IsResetRequest"] = true
+ ctx.HTML(http.StatusOK, tplForgotPassword)
+}
+
+// ForgotPasswdPost response for forget password request
+func ForgotPasswdPost(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
+
+ if setting.MailService == nil {
+ ctx.NotFound("ForgotPasswdPost", nil)
+ return
+ }
+ ctx.Data["IsResetRequest"] = true
+
+ email := ctx.FormString("email")
+ ctx.Data["Email"] = email
+
+ u, err := user_model.GetUserByEmail(ctx, email)
+ if err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale)
+ ctx.Data["IsResetSent"] = true
+ ctx.HTML(http.StatusOK, tplForgotPassword)
+ return
+ }
+
+ ctx.ServerError("user.ResetPasswd(check existence)", err)
+ return
+ }
+
+ if !u.IsLocal() && !u.IsOAuth2() {
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
+ return
+ }
+
+ if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
+ ctx.Data["ResendLimited"] = true
+ ctx.HTML(http.StatusOK, tplForgotPassword)
+ return
+ }
+
+ if err := mailer.SendResetPasswordMail(ctx, u); err != nil {
+ ctx.ServerError("SendResetPasswordMail", err)
+ return
+ }
+
+ if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ }
+
+ ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale)
+ ctx.Data["IsResetSent"] = true
+ ctx.HTML(http.StatusOK, tplForgotPassword)
+}
+
+func commonResetPassword(ctx *context.Context, shouldDeleteToken bool) (*user_model.User, *auth.TwoFactor) {
+ code := ctx.FormString("code")
+
+ ctx.Data["Title"] = ctx.Tr("auth.reset_password")
+ ctx.Data["Code"] = code
+
+ if nil != ctx.Doer {
+ ctx.Data["user_signed_in"] = true
+ }
+
+ if len(code) == 0 {
+ ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
+ return nil, nil
+ }
+
+ // Fail early, don't frustrate the user
+ u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.PasswordReset, shouldDeleteToken)
+ if err != nil {
+ ctx.ServerError("VerifyUserAuthorizationToken", err)
+ return nil, nil
+ }
+
+ if u == nil {
+ ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
+ return nil, nil
+ }
+
+ twofa, err := auth.GetTwoFactorByUID(ctx, u.ID)
+ if err != nil {
+ if !auth.IsErrTwoFactorNotEnrolled(err) {
+ ctx.Error(http.StatusInternalServerError, "CommonResetPassword", err.Error())
+ return nil, nil
+ }
+ } else {
+ ctx.Data["has_two_factor"] = true
+ ctx.Data["scratch_code"] = ctx.FormBool("scratch_code")
+ }
+
+ // Show the user that they are affecting the account that they intended to
+ ctx.Data["user_email"] = u.Email
+
+ if nil != ctx.Doer && u.ID != ctx.Doer.ID {
+ ctx.Flash.Error(ctx.Tr("auth.reset_password_wrong_user", ctx.Doer.Email, u.Email), true)
+ return nil, nil
+ }
+
+ return u, twofa
+}
+
+// ResetPasswd render the account recovery page
+func ResetPasswd(ctx *context.Context) {
+ ctx.Data["IsResetForm"] = true
+
+ commonResetPassword(ctx, false)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplResetPassword)
+}
+
+// ResetPasswdPost response from account recovery request
+func ResetPasswdPost(ctx *context.Context) {
+ u, twofa := commonResetPassword(ctx, true)
+ if ctx.Written() {
+ return
+ }
+
+ if u == nil {
+ // Flash error has been set
+ ctx.HTML(http.StatusOK, tplResetPassword)
+ return
+ }
+
+ // Handle two-factor
+ regenerateScratchToken := false
+ if twofa != nil {
+ if ctx.FormBool("scratch_code") {
+ if !twofa.VerifyScratchToken(ctx.FormString("token")) {
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Token"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
+ return
+ }
+ regenerateScratchToken = true
+ } else {
+ passcode := ctx.FormString("passcode")
+ ok, err := twofa.ValidateTOTP(passcode)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err.Error())
+ return
+ }
+ if !ok || twofa.LastUsedPasscode == passcode {
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Passcode"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
+ return
+ }
+
+ twofa.LastUsedPasscode = passcode
+ if err = auth.UpdateTwoFactor(ctx, twofa); err != nil {
+ ctx.ServerError("ResetPasswdPost: UpdateTwoFactor", err)
+ return
+ }
+ }
+ }
+
+ opts := &user_service.UpdateAuthOptions{
+ Password: optional.Some(ctx.FormString("password")),
+ MustChangePassword: optional.Some(false),
+ }
+ if err := user_service.UpdateAuth(ctx, u, opts); err != nil {
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Password"] = true
+ switch {
+ case errors.Is(err, password.ErrMinLength):
+ ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
+ case errors.Is(err, password.ErrComplexity):
+ ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
+ case errors.Is(err, password.ErrIsPwned):
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplResetPassword, nil)
+ case password.IsErrIsPwnedRequest(err):
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
+ default:
+ ctx.ServerError("UpdateAuth", err)
+ }
+ return
+ }
+
+ log.Trace("User password reset: %s", u.Name)
+ ctx.Data["IsResetFailed"] = true
+ remember := len(ctx.FormString("remember")) != 0
+
+ if regenerateScratchToken {
+ // Invalidate the scratch token.
+ _, err := twofa.GenerateScratchToken()
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ if err = auth.UpdateTwoFactor(ctx, twofa); err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ handleSignInFull(ctx, u, remember, false)
+ if ctx.Written() {
+ return
+ }
+ ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+ return
+ }
+
+ handleSignIn(ctx, u, remember)
+}
+
+// MustChangePassword renders the page to change a user's password
+func MustChangePassword(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
+ ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
+ ctx.Data["MustChangePassword"] = true
+ ctx.HTML(http.StatusOK, tplMustChangePassword)
+}
+
+// MustChangePasswordPost response for updating a user's password after their
+// account was created by an admin
+func MustChangePasswordPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.MustChangePasswordForm)
+ ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
+ ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplMustChangePassword)
+ return
+ }
+
+ // Make sure only requests for users who are eligible to change their password via
+ // this method passes through
+ if !ctx.Doer.MustChangePassword {
+ ctx.ServerError("MustUpdatePassword", errors.New("cannot update password. Please visit the settings page"))
+ return
+ }
+
+ if form.Password != form.Retype {
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form)
+ return
+ }
+
+ opts := &user_service.UpdateAuthOptions{
+ Password: optional.Some(form.Password),
+ MustChangePassword: optional.Some(false),
+ }
+ if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
+ switch {
+ case errors.Is(err, password.ErrMinLength):
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
+ case errors.Is(err, password.ErrComplexity):
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
+ case errors.Is(err, password.ErrIsPwned):
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplMustChangePassword, &form)
+ case password.IsErrIsPwnedRequest(err):
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
+ default:
+ ctx.ServerError("UpdateAuth", err)
+ }
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
+
+ log.Trace("User updated password: %s", ctx.Doer.Name)
+
+ redirectTo := ctx.GetSiteCookie("redirect_to")
+ if redirectTo != "" {
+ middleware.DeleteRedirectToCookie(ctx.Resp)
+ }
+ ctx.RedirectToFirst(redirectTo)
+}