Skip to content

Commit 6dbd261

Browse files
AJ ONeallafriks
AJ ONeal
authored andcommitted
UX + Security current user password reset (#5042)
* allow current user to reset their own password * handle reset password edge cases properly and consistently * remove dangling assignment * properly label account recovery instead of reset password * remove 'Click here' from button * update English-only account-recovery templates
1 parent fdb933c commit 6dbd261

File tree

7 files changed

+100
-57
lines changed

7 files changed

+100
-57
lines changed

Diff for: models/mail.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func SendActivateAccountMail(c *macaron.Context, u *User) {
7373

7474
// SendResetPasswordMail sends a password reset mail to the user
7575
func SendResetPasswordMail(c *macaron.Context, u *User) {
76-
SendUserMail(c, u, mailAuthResetPassword, u.GenerateActivateCode(), c.Tr("mail.reset_password"), "reset password")
76+
SendUserMail(c, u, mailAuthResetPassword, u.GenerateActivateCode(), c.Tr("mail.reset_password"), "recover account")
7777
}
7878

7979
// SendActivateEmailMail sends confirmation email to confirm new email address

Diff for: options/locale/locale_en-US.ini

+9-7
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ sign_up_successful = Account was successfully created.
206206
confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process.
207207
must_change_password = Update your password
208208
allow_password_change = Require user to change password (recommended)
209-
reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the password reset process.
209+
reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the account recovery process.
210210
active_your_account = Activate Your Account
211211
account_activated = Account has been activated
212212
prohibit_login = Sign In Prohibited
@@ -215,10 +215,11 @@ resent_limit_prompt = You have already requested an activation email recently. P
215215
has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to resend a new one, please click on the button below.
216216
resend_mail = Click here to resend your activation email
217217
email_not_associate = The email address is not associated with any account.
218-
send_reset_mail = Click here to resend your password reset email
219-
reset_password = Reset Your Password
218+
send_reset_mail = Send Account Recovery Email
219+
reset_password = Account Recovery
220220
invalid_code = Your confirmation code is invalid or has expired.
221-
reset_password_helper = Click here to reset your password
221+
reset_password_helper = Recover Account
222+
reset_password_wrong_user = You are signed in as %s, but the account recovery link is for %s
222223
password_too_short = Password length cannot be less than %d characters.
223224
non_local_account = Non-local users can not update their password through the Gitea web interface.
224225
verify = Verify
@@ -241,7 +242,7 @@ openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new
241242
openid_register_title = Create new account
242243
openid_register_desc = The chosen OpenID URI is unknown. Associate it with a new account here.
243244
openid_signin_desc = Enter your OpenID URI. For example: https://anne.me, bob.openid.org.cn or gnusocial.net/carry.
244-
disable_forgot_password_mail = Password reset is disabled. Please contact your site administrator.
245+
disable_forgot_password_mail = Account recovery is disabled. Please contact your site administrator.
245246
email_domain_blacklisted = You cannot register with your email address.
246247
authorize_application = Authorize Application
247248
authroize_redirect_notice = You will be redirected to %s if you authorize this application.
@@ -250,11 +251,12 @@ authorize_application_description = If you grant the access, it will be able to
250251
authorize_title = Authorize "%s" to access your account?
251252
authorization_failed = Authorization failed
252253
authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you've tried to authorize.
254+
disable_forgot_password_mail = Account recovery is disabled. Please contact your site administrator.
253255

254256
[mail]
255257
activate_account = Please activate your account
256258
activate_email = Verify your email address
257-
reset_password = Reset your password
259+
reset_password = Recover your account
258260
register_success = Registration successful
259261
register_notify = Welcome to Gitea
260262

@@ -1691,7 +1693,7 @@ config.mail_notify = Enable Email Notifications
16911693
config.disable_key_size_check = Disable Minimum Key Size Check
16921694
config.enable_captcha = Enable CAPTCHA
16931695
config.active_code_lives = Active Code Lives
1694-
config.reset_password_code_lives = Reset Password Code Expiry Time
1696+
config.reset_password_code_lives = Recover Account Code Expiry Time
16951697
config.default_keep_email_private = Hide Email Addresses by Default
16961698
config.default_allow_create_organization = Allow Creation of Organizations by Default
16971699
config.enable_timetracking = Enable Time Tracking

Diff for: routers/routes/routes.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,6 @@ func RegisterRoutes(m *macaron.Macaron) {
273273
}, openIDSignInEnabled)
274274
m.Get("/sign_up", user.SignUp)
275275
m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost)
276-
m.Get("/reset_password", user.ResetPasswd)
277-
m.Post("/reset_password", user.ResetPasswdPost)
278276
m.Group("/oauth2", func() {
279277
m.Get("/:provider", user.SignInOAuth)
280278
m.Get("/:provider/callback", user.SignInOAuthCallback)
@@ -382,6 +380,8 @@ func RegisterRoutes(m *macaron.Macaron) {
382380
m.Any("/activate", user.Activate, reqSignIn)
383381
m.Any("/activate_email", user.ActivateEmail)
384382
m.Get("/email2user", user.Email2User)
383+
m.Get("/recover_account", user.ResetPasswd)
384+
m.Post("/recover_account", user.ResetPasswdPost)
385385
m.Get("/forgot_password", user.ForgotPasswd)
386386
m.Post("/forgot_password", user.ForgotPasswdPost)
387387
m.Get("/logout", user.SignOut)

Diff for: routers/user/auth.go

+68-43
Original file line numberDiff line numberDiff line change
@@ -893,8 +893,7 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au
893893
ctx.Redirect(setting.AppSubURL + "/user/login")
894894
}
895895

896-
// SignOut sign out from login status
897-
func SignOut(ctx *context.Context) {
896+
func handleSignOut(ctx *context.Context) {
898897
ctx.Session.Delete("uid")
899898
ctx.Session.Delete("uname")
900899
ctx.Session.Delete("socialId")
@@ -904,6 +903,11 @@ func SignOut(ctx *context.Context) {
904903
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
905904
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true)
906905
ctx.SetCookie("lang", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true) // Setting the lang cookie will trigger the middleware to reset the language ot previous state.
906+
}
907+
908+
// SignOut sign out from login status
909+
func SignOut(ctx *context.Context) {
910+
handleSignOut(ctx)
907911
ctx.Redirect(setting.AppSubURL + "/")
908912
}
909913

@@ -1174,68 +1178,89 @@ func ForgotPasswdPost(ctx *context.Context) {
11741178
ctx.HTML(200, tplForgotPassword)
11751179
}
11761180

1177-
// ResetPasswd render the reset password page
1178-
func ResetPasswd(ctx *context.Context) {
1181+
func commonResetPassword(ctx *context.Context) *models.User {
1182+
code := ctx.Query("code")
1183+
11791184
ctx.Data["Title"] = ctx.Tr("auth.reset_password")
1185+
ctx.Data["Code"] = code
1186+
1187+
if nil != ctx.User {
1188+
ctx.Data["user_signed_in"] = true
1189+
}
11801190

1181-
code := ctx.Query("code")
11821191
if len(code) == 0 {
1183-
ctx.Error(404)
1184-
return
1192+
ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
1193+
return nil
11851194
}
1186-
ctx.Data["Code"] = code
11871195

1188-
if u := models.VerifyUserActiveCode(code); u != nil {
1189-
ctx.Data["IsResetForm"] = true
1196+
// Fail early, don't frustrate the user
1197+
u := models.VerifyUserActiveCode(code)
1198+
if u == nil {
1199+
ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
1200+
return nil
1201+
}
1202+
1203+
// Show the user that they are affecting the account that they intended to
1204+
ctx.Data["user_email"] = u.Email
1205+
1206+
if nil != ctx.User && u.ID != ctx.User.ID {
1207+
ctx.Flash.Error(ctx.Tr("auth.reset_password_wrong_user", ctx.User.Email, u.Email))
1208+
return nil
11901209
}
11911210

1211+
return u
1212+
}
1213+
1214+
// ResetPasswd render the account recovery page
1215+
func ResetPasswd(ctx *context.Context) {
1216+
ctx.Data["IsResetForm"] = true
1217+
1218+
commonResetPassword(ctx)
1219+
11921220
ctx.HTML(200, tplResetPassword)
11931221
}
11941222

1195-
// ResetPasswdPost response from reset password request
1223+
// ResetPasswdPost response from account recovery request
11961224
func ResetPasswdPost(ctx *context.Context) {
1197-
ctx.Data["Title"] = ctx.Tr("auth.reset_password")
1225+
u := commonResetPassword(ctx)
11981226

1199-
code := ctx.Query("code")
1200-
if len(code) == 0 {
1201-
ctx.Error(404)
1227+
if u == nil {
1228+
// Flash error has been set
1229+
ctx.HTML(200, tplResetPassword)
12021230
return
12031231
}
1204-
ctx.Data["Code"] = code
12051232

1206-
if u := models.VerifyUserActiveCode(code); u != nil {
1207-
// Validate password length.
1208-
passwd := ctx.Query("password")
1209-
if len(passwd) < setting.MinPasswordLength {
1210-
ctx.Data["IsResetForm"] = true
1211-
ctx.Data["Err_Password"] = true
1212-
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
1213-
return
1214-
}
1233+
// Validate password length.
1234+
passwd := ctx.Query("password")
1235+
if len(passwd) < setting.MinPasswordLength {
1236+
ctx.Data["IsResetForm"] = true
1237+
ctx.Data["Err_Password"] = true
1238+
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
1239+
return
1240+
}
12151241

1216-
var err error
1217-
if u.Rands, err = models.GetUserSalt(); err != nil {
1218-
ctx.ServerError("UpdateUser", err)
1219-
return
1220-
}
1221-
if u.Salt, err = models.GetUserSalt(); err != nil {
1222-
ctx.ServerError("UpdateUser", err)
1223-
return
1224-
}
1225-
u.HashPassword(passwd)
1226-
u.MustChangePassword = false
1227-
if err := models.UpdateUserCols(u, "must_change_password", "passwd", "rands", "salt"); err != nil {
1228-
ctx.ServerError("UpdateUser", err)
1229-
return
1230-
}
1242+
var err error
1243+
if u.Rands, err = models.GetUserSalt(); err != nil {
1244+
ctx.ServerError("UpdateUser", err)
1245+
return
1246+
}
1247+
if u.Salt, err = models.GetUserSalt(); err != nil {
1248+
ctx.ServerError("UpdateUser", err)
1249+
return
1250+
}
12311251

1232-
log.Trace("User password reset: %s", u.Name)
1233-
ctx.Redirect(setting.AppSubURL + "/user/login")
1252+
u.HashPassword(passwd)
1253+
u.MustChangePassword = false
1254+
if err := models.UpdateUserCols(u, "must_change_password", "passwd", "rands", "salt"); err != nil {
1255+
ctx.ServerError("UpdateUser", err)
12341256
return
12351257
}
12361258

1259+
log.Trace("User password reset: %s", u.Name)
1260+
12371261
ctx.Data["IsResetFailed"] = true
1238-
ctx.HTML(200, tplResetPassword)
1262+
remember := len(ctx.Query("remember")) != 0
1263+
handleSignInFull(ctx, u, remember, true)
12391264
}
12401265

12411266
// MustChangePassword renders the page to change a user's password

Diff for: templates/mail/auth/register_notify.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<p>Hi <b>{{.DisplayName}}</b>, this is your registration confirmation email for {{AppName}}!</p>
1010
<p>You can now login via username: {{.Username}}.</p>
1111
<p><a href="{{AppUrl}}user/login">{{AppUrl}}user/login</a></p>
12-
<p>If this account has been created for you, please <a href="{{AppUrl}}user/forgot_password">reset your password</a> first.</p>
12+
<p>If this account has been created for you, please <a href="{{AppUrl}}user/forgot_password">set your password</a> first.</p>
1313
<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
1414
</body>
1515
</html>

Diff for: templates/mail/auth/reset_passwd.tmpl

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
<html>
33
<head>
44
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5-
<title>{{.DisplayName}}, you have requested to reset your password</title>
5+
<title>{{.DisplayName}}, you have requested to recover your account</title>
66
</head>
77

88
<body>
99
<p>Hi <b>{{.DisplayName}}</b>,</p>
10-
<p>Please click the following link to reset your password within <b>{{.ResetPwdCodeLives}}</b>:</p>
11-
<p><a href="{{AppUrl}}user/reset_password?code={{.Code}}">{{AppUrl}}user/reset_password?code={{.Code}}</a></p>
10+
<p>Please click the following link to recover your account within <b>{{.ResetPwdCodeLives}}</b>:</p>
11+
12+
<p><a href="{{AppUrl}}user/recover_account?code={{.Code}}">{{AppUrl}}user/recover_account?code={{.Code}}</a></p>
1213
<p>Not working? Try copying and pasting it to your browser.</p>
1314
<p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p>
1415
</body>

Diff for: templates/user/auth/reset_passwd.tmpl

+15
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,26 @@
1010
</h2>
1111
<div class="ui attached segment">
1212
{{template "base/alert" .}}
13+
{{if .user_email }}
14+
<div class="inline field">
15+
<label for="user_name">{{.i18n.Tr "email"}}</label>
16+
<input id="user_name" type="text" value="{{ .user_email }}" disabled>
17+
</div>
18+
{{end}}
1319
{{if .IsResetForm}}
1420
<div class="required inline field {{if .Err_Password}}error{{end}}">
1521
<label for="password">{{.i18n.Tr "password"}}</label>
1622
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" autofocus required>
1723
</div>
24+
{{if not .user_signed_in}}
25+
<div class="inline field">
26+
<label></label>
27+
<div class="ui checkbox">
28+
<label>{{.i18n.Tr "auth.remember_me"}}</label>
29+
<input name="remember" type="checkbox">
30+
</div>
31+
</div>
32+
{{end}}
1833
<div class="ui divider"></div>
1934
<div class="inline field">
2035
<label></label>

0 commit comments

Comments
 (0)