From e3e024876e71d01a6700a7f6fe8861d709de6de6 Mon Sep 17 00:00:00 2001 From: zeripath Date: Sun, 19 Jan 2020 23:37:28 +0000 Subject: [PATCH] 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 * Properly manage scratch_code regeneration Co-authored-by: Lauris BH Co-authored-by: Lauris BH --- routers/user/auth.go | 83 ++++++++++++++++++++++++--- templates/user/auth/reset_passwd.tmpl | 23 +++++++- 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/routers/user/auth.go b/routers/user/auth.go index 3924b0aaf..b1f926c77 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -1282,7 +1282,7 @@ func ForgotPasswdPost(ctx *context.Context) { ctx.HTML(200, tplForgotPassword) } -func commonResetPassword(ctx *context.Context) *models.User { +func commonResetPassword(ctx *context.Context) (*models.User, *models.TwoFactor) { code := ctx.Query("code") ctx.Data["Title"] = ctx.Tr("auth.reset_password") @@ -1294,14 +1294,25 @@ func commonResetPassword(ctx *context.Context) *models.User { if len(code) == 0 { ctx.Flash.Error(ctx.Tr("auth.invalid_code")) - return nil + return nil, nil } // Fail early, don't frustrate the user u := models.VerifyUserActiveCode(code) if u == nil { ctx.Flash.Error(ctx.Tr("auth.invalid_code")) - return nil + return nil, nil + } + + twofa, err := models.GetTwoFactorByUID(u.ID) + if err != nil { + if !models.IsErrTwoFactorNotEnrolled(err) { + ctx.Error(http.StatusInternalServerError, "CommonResetPassword", err.Error()) + return nil, nil + } + } else { + ctx.Data["has_two_factor"] = true + ctx.Data["scratch_code"] = ctx.QueryBool("scratch_code") } // Show the user that they are affecting the account that they intended to @@ -1309,10 +1320,10 @@ func commonResetPassword(ctx *context.Context) *models.User { if nil != ctx.User && u.ID != ctx.User.ID { ctx.Flash.Error(ctx.Tr("auth.reset_password_wrong_user", ctx.User.Email, u.Email)) - return nil + return nil, nil } - return u + return u, twofa } // ResetPasswd render the account recovery page @@ -1320,13 +1331,19 @@ func ResetPasswd(ctx *context.Context) { ctx.Data["IsResetForm"] = true commonResetPassword(ctx) + if ctx.Written() { + return + } ctx.HTML(200, tplResetPassword) } // ResetPasswdPost response from account recovery request func ResetPasswdPost(ctx *context.Context) { - u := commonResetPassword(ctx) + u, twofa := commonResetPassword(ctx) + if ctx.Written() { + return + } if u == nil { // Flash error has been set @@ -1348,6 +1365,39 @@ func ResetPasswdPost(ctx *context.Context) { return } + // Handle two-factor + regenerateScratchToken := false + if twofa != nil { + if ctx.QueryBool("scratch_code") { + if !twofa.VerifyScratchToken(ctx.Query("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.Query("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 = models.UpdateTwoFactor(twofa); err != nil { + ctx.ServerError("ResetPasswdPost: UpdateTwoFactor", err) + return + } + } + } + var err error if u.Rands, err = models.GetUserSalt(); err != nil { ctx.ServerError("UpdateUser", err) @@ -1357,7 +1407,6 @@ func ResetPasswdPost(ctx *context.Context) { ctx.ServerError("UpdateUser", err) return } - u.HashPassword(passwd) u.MustChangePassword = false if err := models.UpdateUserCols(u, "must_change_password", "passwd", "rands", "salt"); err != nil { @@ -1366,9 +1415,27 @@ func ResetPasswdPost(ctx *context.Context) { } log.Trace("User password reset: %s", u.Name) - ctx.Data["IsResetFailed"] = true remember := len(ctx.Query("remember")) != 0 + + if regenerateScratchToken { + // Invalidate the scratch token. + _, err = twofa.GenerateScratchToken() + if err != nil { + ctx.ServerError("UserSignIn", err) + return + } + if err = models.UpdateTwoFactor(twofa); err != nil { + ctx.ServerError("UserSignIn", err) + return + } + + handleSignInFull(ctx, u, remember, false) + ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used")) + ctx.Redirect(setting.AppSubURL + "/user/settings/security") + return + } + handleSignInFull(ctx, u, remember, true) } diff --git a/templates/user/auth/reset_passwd.tmpl b/templates/user/auth/reset_passwd.tmpl index e7d939294..91d5a5ef8 100644 --- a/templates/user/auth/reset_passwd.tmpl +++ b/templates/user/auth/reset_passwd.tmpl @@ -18,7 +18,7 @@ {{end}} {{if .IsResetForm}}
- +
{{if not .user_signed_in}} @@ -30,10 +30,31 @@ {{end}} + {{if .has_two_factor}} +

+ {{.i18n.Tr "twofa"}} +

+
{{.i18n.Tr "settings.twofa_is_enrolled" | Str2html }}
+ {{if .scratch_code}} +
+ + +
+ + {{else}} +
+ + +
+ {{end}} + {{end}}
+ {{if and .has_two_factor (not .scratch_code)}} + {{.i18n.Tr "auth.use_scratch_code" | Str2html}} + {{end}}
{{else}}

{{.i18n.Tr "auth.invalid_code"}}