Skip to content

Commit e3e0248

Browse files
zeripathlafriks
authored andcommitted
Ensure that 2fa is checked on reset-password (#9857) (#9877)
* 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 cebc125 commit e3e0248

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
@@ -1282,7 +1282,7 @@ func ForgotPasswdPost(ctx *context.Context) {
12821282
ctx.HTML(200, tplForgotPassword)
12831283
}
12841284

1285-
func commonResetPassword(ctx *context.Context) *models.User {
1285+
func commonResetPassword(ctx *context.Context) (*models.User, *models.TwoFactor) {
12861286
code := ctx.Query("code")
12871287

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

12951295
if len(code) == 0 {
12961296
ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
1297-
return nil
1297+
return nil, nil
12981298
}
12991299

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

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

13101321
if nil != ctx.User && u.ID != ctx.User.ID {
13111322
ctx.Flash.Error(ctx.Tr("auth.reset_password_wrong_user", ctx.User.Email, u.Email))
1312-
return nil
1323+
return nil, nil
13131324
}
13141325

1315-
return u
1326+
return u, twofa
13161327
}
13171328

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

13221333
commonResetPassword(ctx)
1334+
if ctx.Written() {
1335+
return
1336+
}
13231337

13241338
ctx.HTML(200, tplResetPassword)
13251339
}
13261340

13271341
// ResetPasswdPost response from account recovery request
13281342
func ResetPasswdPost(ctx *context.Context) {
1329-
u := commonResetPassword(ctx)
1343+
u, twofa := commonResetPassword(ctx)
1344+
if ctx.Written() {
1345+
return
1346+
}
13301347

13311348
if u == nil {
13321349
// Flash error has been set
@@ -1348,6 +1365,39 @@ func ResetPasswdPost(ctx *context.Context) {
13481365
return
13491366
}
13501367

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

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

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)