diff options
author | zeripath <art27@cantab.net> | 2020-01-19 23:37:28 +0000 |
---|---|---|
committer | techknowlogick <techknowlogick@gitea.io> | 2020-01-19 18:37:28 -0500 |
commit | e3e024876e71d01a6700a7f6fe8861d709de6de6 (patch) | |
tree | d30cdacce8e56338eefd8ddb3c5357589526a344 | |
parent | cebc125f7f08696dc00dd4e22e2177d142ae1aff (diff) | |
download | gitea-e3e024876e71d01a6700a7f6fe8861d709de6de6.tar.gz gitea-e3e024876e71d01a6700a7f6fe8861d709de6de6.zip |
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 <lauris@nix.lv>
* Properly manage scratch_code regeneration
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: Lauris BH <lauris@nix.lv>
-rw-r--r-- | routers/user/auth.go | 83 | ||||
-rw-r--r-- | 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 3924b0aaf1..b1f926c77f 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 e7d939294e..91d5a5ef88 100644 --- a/templates/user/auth/reset_passwd.tmpl +++ b/templates/user/auth/reset_passwd.tmpl @@ -18,7 +18,7 @@ {{end}} {{if .IsResetForm}} <div class="required inline field {{if .Err_Password}}error{{end}}"> - <label for="password">{{.i18n.Tr "password"}}</label> + <label for="password">{{.i18n.Tr "settings.new_password"}}</label> <input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" autofocus required> </div> {{if not .user_signed_in}} @@ -30,10 +30,31 @@ </div> </div> {{end}} + {{if .has_two_factor}} + <h4 class="ui dividing header"> + {{.i18n.Tr "twofa"}} + </h4> + <div class="ui warning visible message">{{.i18n.Tr "settings.twofa_is_enrolled" | Str2html }}</div> + {{if .scratch_code}} + <div class="required inline field {{if .Err_Token}}error{{end}}"> + <label for="token">{{.i18n.Tr "auth.scratch_code"}}</label> + <input id="token" name="token" type="text" autocomplete="off" autofocus required> + </div> + <input type="hidden" name="scratch_code" value="true"> + {{else}} + <div class="required inline field {{if .Err_Passcode}}error{{end}}"> + <label for="passcode">{{.i18n.Tr "passcode"}}</label> + <input id="passcode" name="passcode" type="number" autocomplete="off" autofocus required> + </div> + {{end}} + {{end}} <div class="ui divider"></div> <div class="inline field"> <label></label> <button class="ui blue button">{{.i18n.Tr "auth.reset_password_helper"}}</button> + {{if and .has_two_factor (not .scratch_code)}} + <a href="{{.Link}}?code={{.Code}}&scratch_code=true">{{.i18n.Tr "auth.use_scratch_code" | Str2html}}</a> + {{end}} </div> {{else}} <p class="center">{{.i18n.Tr "auth.invalid_code"}}</p> |