2022-01-02 16:12:35 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package auth
import (
"errors"
"net/http"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
"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/routers/utils"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
)
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 ( ctx . Tr ( "auth.disable_forgot_password_mail_admin" ) )
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 ( email )
if err != nil {
if user_model . IsErrUserNotExist ( err ) {
ctx . Data [ "ResetPwdCodeLives" ] = timeutil . MinutesToFriendly ( setting . Service . ResetPwdCodeLives , ctx . Locale . Language ( ) )
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
}
mailer . SendResetPasswordMail ( u )
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 . Language ( ) )
ctx . Data [ "IsResetSent" ] = true
ctx . HTML ( http . StatusOK , tplForgotPassword )
}
func commonResetPassword ( ctx * context . Context ) ( * user_model . User , * auth . TwoFactor ) {
code := ctx . FormString ( "code" )
ctx . Data [ "Title" ] = ctx . Tr ( "auth.reset_password" )
ctx . Data [ "Code" ] = code
2022-03-22 10:03:22 +03:00
if nil != ctx . Doer {
2022-01-02 16:12:35 +03:00
ctx . Data [ "user_signed_in" ] = true
}
if len ( code ) == 0 {
ctx . Flash . Error ( ctx . Tr ( "auth.invalid_code" ) )
return nil , nil
}
// Fail early, don't frustrate the user
u := user_model . VerifyUserActiveCode ( code )
if u == nil {
ctx . Flash . Error ( ctx . Tr ( "auth.invalid_code" ) )
return nil , nil
}
twofa , err := auth . GetTwoFactorByUID ( 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
2022-03-22 10:03:22 +03:00
if nil != ctx . Doer && u . ID != ctx . Doer . ID {
ctx . Flash . Error ( ctx . Tr ( "auth.reset_password_wrong_user" , ctx . Doer . Email , u . Email ) )
2022-01-02 16:12:35 +03:00
return nil , nil
}
return u , twofa
}
// ResetPasswd render the account recovery page
func ResetPasswd ( ctx * context . Context ) {
ctx . Data [ "IsResetForm" ] = true
commonResetPassword ( ctx )
if ctx . Written ( ) {
return
}
ctx . HTML ( http . StatusOK , tplResetPassword )
}
// ResetPasswdPost response from account recovery request
func ResetPasswdPost ( ctx * context . Context ) {
u , twofa := commonResetPassword ( ctx )
if ctx . Written ( ) {
return
}
if u == nil {
// Flash error has been set
ctx . HTML ( http . StatusOK , tplResetPassword )
return
}
// Validate password length.
passwd := ctx . FormString ( "password" )
if len ( passwd ) < setting . MinPasswordLength {
ctx . Data [ "IsResetForm" ] = true
ctx . Data [ "Err_Password" ] = true
ctx . RenderWithErr ( ctx . Tr ( "auth.password_too_short" , setting . MinPasswordLength ) , tplResetPassword , nil )
return
} else if ! password . IsComplexEnough ( passwd ) {
ctx . Data [ "IsResetForm" ] = true
ctx . Data [ "Err_Password" ] = true
ctx . RenderWithErr ( password . BuildComplexityError ( ctx ) , tplResetPassword , nil )
return
} else if pwned , err := password . IsPwned ( ctx , passwd ) ; pwned || err != nil {
errMsg := ctx . Tr ( "auth.password_pwned" )
if err != nil {
log . Error ( err . Error ( ) )
errMsg = ctx . Tr ( "auth.password_pwned_err" )
}
ctx . Data [ "IsResetForm" ] = true
ctx . Data [ "Err_Password" ] = true
ctx . RenderWithErr ( errMsg , tplResetPassword , nil )
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 ( twofa ) ; err != nil {
ctx . ServerError ( "ResetPasswdPost: UpdateTwoFactor" , err )
return
}
}
}
var err error
if u . Rands , err = user_model . GetUserSalt ( ) ; err != nil {
ctx . ServerError ( "UpdateUser" , err )
return
}
if err = u . SetPassword ( passwd ) ; err != nil {
ctx . ServerError ( "UpdateUser" , err )
return
}
u . MustChangePassword = false
2022-03-22 18:22:54 +03:00
if err := user_model . UpdateUserCols ( ctx , u , "must_change_password" , "passwd" , "passwd_hash_algo" , "rands" , "salt" ) ; err != nil {
2022-01-02 16:12:35 +03:00
ctx . ServerError ( "UpdateUser" , 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 ( 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 his/her
// 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
}
2022-03-22 10:03:22 +03:00
u := ctx . Doer
2022-01-02 16:12:35 +03:00
// Make sure only requests for users who are eligible to change their password via
// this method passes through
if ! u . 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
}
if len ( form . Password ) < setting . MinPasswordLength {
ctx . Data [ "Err_Password" ] = true
ctx . RenderWithErr ( ctx . Tr ( "auth.password_too_short" , setting . MinPasswordLength ) , tplMustChangePassword , & form )
return
}
if ! password . IsComplexEnough ( form . Password ) {
ctx . Data [ "Err_Password" ] = true
ctx . RenderWithErr ( password . BuildComplexityError ( ctx ) , tplMustChangePassword , & form )
return
}
pwned , err := password . IsPwned ( ctx , form . Password )
if pwned {
ctx . Data [ "Err_Password" ] = true
errMsg := ctx . Tr ( "auth.password_pwned" )
if err != nil {
log . Error ( err . Error ( ) )
errMsg = ctx . Tr ( "auth.password_pwned_err" )
}
ctx . RenderWithErr ( errMsg , tplMustChangePassword , & form )
return
}
if err = u . SetPassword ( form . Password ) ; err != nil {
ctx . ServerError ( "UpdateUser" , err )
return
}
u . MustChangePassword = false
2022-03-22 18:22:54 +03:00
if err := user_model . UpdateUserCols ( ctx , u , "must_change_password" , "passwd" , "passwd_hash_algo" , "salt" ) ; err != nil {
2022-01-02 16:12:35 +03:00
ctx . ServerError ( "UpdateUser" , err )
return
}
ctx . Flash . Success ( ctx . Tr ( "settings.change_password_success" ) )
log . Trace ( "User updated password: %s" , u . Name )
if redirectTo := ctx . GetCookie ( "redirect_to" ) ; len ( redirectTo ) > 0 && ! utils . IsExternalURL ( redirectTo ) {
middleware . DeleteRedirectToCookie ( ctx . Resp )
ctx . RedirectToFirst ( redirectTo )
return
}
ctx . Redirect ( setting . AppSubURL + "/" )
}