Skip to content

Commit e878d74

Browse files
zeripathlafriks
andauthored
Ensure that 2fa is checked on reset-password (#9857) (#9876)
* Ensure that 2fa is checked on reset-password * Apply suggestions from code review Co-Authored-By: Lauris BH <[email protected]> * Properly manage scratch_code regeneration Co-authored-by: Lauris BH <[email protected]> Co-authored-by: Lauris BH <[email protected]>
1 parent 3fa14d8 commit e878d74

File tree

2 files changed

+97
-9
lines changed

2 files changed

+97
-9
lines changed

routers/user/auth.go

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,7 +1284,7 @@ func ForgotPasswdPost(ctx *context.Context) {
12841284
ctx.HTML(200, tplForgotPassword)
12851285
}
12861286

1287-
func commonResetPassword(ctx *context.Context) *models.User {
1287+
func commonResetPassword(ctx *context.Context) (*models.User, *models.TwoFactor) {
12881288
code := ctx.Query("code")
12891289

12901290
ctx.Data["Title"] = ctx.Tr("auth.reset_password")
@@ -1296,39 +1296,56 @@ func commonResetPassword(ctx *context.Context) *models.User {
12961296

12971297
if len(code) == 0 {
12981298
ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
1299-
return nil
1299+
return nil, nil
13001300
}
13011301

13021302
// Fail early, don't frustrate the user
13031303
u := models.VerifyUserActiveCode(code)
13041304
if u == nil {
13051305
ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
1306-
return nil
1306+
return nil, nil
1307+
}
1308+
1309+
twofa, err := models.GetTwoFactorByUID(u.ID)
1310+
if err != nil {
1311+
if !models.IsErrTwoFactorNotEnrolled(err) {
1312+
ctx.Error(http.StatusInternalServerError, "CommonResetPassword", err.Error())
1313+
return nil, nil
1314+
}
1315+
} else {
1316+
ctx.Data["has_two_factor"] = true
1317+
ctx.Data["scratch_code"] = ctx.QueryBool("scratch_code")
13071318
}
13081319

13091320
// Show the user that they are affecting the account that they intended to
13101321
ctx.Data["user_email"] = u.Email
13111322

13121323
if nil != ctx.User && u.ID != ctx.User.ID {
13131324
ctx.Flash.Error(ctx.Tr("auth.reset_password_wrong_user", ctx.User.Email, u.Email))
1314-
return nil
1325+
return nil, nil
13151326
}
13161327

1317-
return u
1328+
return u, twofa
13181329
}
13191330

13201331
// ResetPasswd render the account recovery page
13211332
func ResetPasswd(ctx *context.Context) {
13221333
ctx.Data["IsResetForm"] = true
13231334

13241335
commonResetPassword(ctx)
1336+
if ctx.Written() {
1337+
return
1338+
}
13251339

13261340
ctx.HTML(200, tplResetPassword)
13271341
}
13281342

13291343
// ResetPasswdPost response from account recovery request
13301344
func ResetPasswdPost(ctx *context.Context) {
1331-
u := commonResetPassword(ctx)
1345+
u, twofa := commonResetPassword(ctx)
1346+
if ctx.Written() {
1347+
return
1348+
}
13321349

13331350
if u == nil {
13341351
// Flash error has been set
@@ -1350,6 +1367,39 @@ func ResetPasswdPost(ctx *context.Context) {
13501367
return
13511368
}
13521369

1370+
// Handle two-factor
1371+
regenerateScratchToken := false
1372+
if twofa != nil {
1373+
if ctx.QueryBool("scratch_code") {
1374+
if !twofa.VerifyScratchToken(ctx.Query("token")) {
1375+
ctx.Data["IsResetForm"] = true
1376+
ctx.Data["Err_Token"] = true
1377+
ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
1378+
return
1379+
}
1380+
regenerateScratchToken = true
1381+
} else {
1382+
passcode := ctx.Query("passcode")
1383+
ok, err := twofa.ValidateTOTP(passcode)
1384+
if err != nil {
1385+
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err.Error())
1386+
return
1387+
}
1388+
if !ok || twofa.LastUsedPasscode == passcode {
1389+
ctx.Data["IsResetForm"] = true
1390+
ctx.Data["Err_Passcode"] = true
1391+
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
1392+
return
1393+
}
1394+
1395+
twofa.LastUsedPasscode = passcode
1396+
if err = models.UpdateTwoFactor(twofa); err != nil {
1397+
ctx.ServerError("ResetPasswdPost: UpdateTwoFactor", err)
1398+
return
1399+
}
1400+
}
1401+
}
1402+
13531403
var err error
13541404
if u.Rands, err = models.GetUserSalt(); err != nil {
13551405
ctx.ServerError("UpdateUser", err)
@@ -1359,7 +1409,6 @@ func ResetPasswdPost(ctx *context.Context) {
13591409
ctx.ServerError("UpdateUser", err)
13601410
return
13611411
}
1362-
13631412
u.HashPassword(passwd)
13641413
u.MustChangePassword = false
13651414
if err := models.UpdateUserCols(u, "must_change_password", "passwd", "rands", "salt"); err != nil {
@@ -1368,9 +1417,27 @@ func ResetPasswdPost(ctx *context.Context) {
13681417
}
13691418

13701419
log.Trace("User password reset: %s", u.Name)
1371-
13721420
ctx.Data["IsResetFailed"] = true
13731421
remember := len(ctx.Query("remember")) != 0
1422+
1423+
if regenerateScratchToken {
1424+
// Invalidate the scratch token.
1425+
_, err = twofa.GenerateScratchToken()
1426+
if err != nil {
1427+
ctx.ServerError("UserSignIn", err)
1428+
return
1429+
}
1430+
if err = models.UpdateTwoFactor(twofa); err != nil {
1431+
ctx.ServerError("UserSignIn", err)
1432+
return
1433+
}
1434+
1435+
handleSignInFull(ctx, u, remember, false)
1436+
ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
1437+
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
1438+
return
1439+
}
1440+
13741441
handleSignInFull(ctx, u, remember, true)
13751442
}
13761443

templates/user/auth/reset_passwd.tmpl

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
{{end}}
1919
{{if .IsResetForm}}
2020
<div class="required inline field {{if .Err_Password}}error{{end}}">
21-
<label for="password">{{.i18n.Tr "password"}}</label>
21+
<label for="password">{{.i18n.Tr "settings.new_password"}}</label>
2222
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" autofocus required>
2323
</div>
2424
{{if not .user_signed_in}}
@@ -30,10 +30,31 @@
3030
</div>
3131
</div>
3232
{{end}}
33+
{{if .has_two_factor}}
34+
<h4 class="ui dividing header">
35+
{{.i18n.Tr "twofa"}}
36+
</h4>
37+
<div class="ui warning visible message">{{.i18n.Tr "settings.twofa_is_enrolled" | Str2html }}</div>
38+
{{if .scratch_code}}
39+
<div class="required inline field {{if .Err_Token}}error{{end}}">
40+
<label for="token">{{.i18n.Tr "auth.scratch_code"}}</label>
41+
<input id="token" name="token" type="text" autocomplete="off" autofocus required>
42+
</div>
43+
<input type="hidden" name="scratch_code" value="true">
44+
{{else}}
45+
<div class="required inline field {{if .Err_Passcode}}error{{end}}">
46+
<label for="passcode">{{.i18n.Tr "passcode"}}</label>
47+
<input id="passcode" name="passcode" type="number" autocomplete="off" autofocus required>
48+
</div>
49+
{{end}}
50+
{{end}}
3351
<div class="ui divider"></div>
3452
<div class="inline field">
3553
<label></label>
3654
<button class="ui blue button">{{.i18n.Tr "auth.reset_password_helper"}}</button>
55+
{{if and .has_two_factor (not .scratch_code)}}
56+
<a href="{{.Link}}?code={{.Code}}&amp;scratch_code=true">{{.i18n.Tr "auth.use_scratch_code" | Str2html}}</a>
57+
{{end}}
3758
</div>
3859
{{else}}
3960
<p class="center">{{.i18n.Tr "auth.invalid_code"}}</p>

0 commit comments

Comments
 (0)