diff --git a/models/fixtures/TestTwoFactorWithPasswordChange/user.yml b/models/fixtures/TestTwoFactorWithPasswordChange/user.yml new file mode 100644 index 0000000000..72fb301e67 --- /dev/null +++ b/models/fixtures/TestTwoFactorWithPasswordChange/user.yml @@ -0,0 +1,37 @@ +- + id: 2001 + lower_name: user2001 + name: user2001 + full_name: "user2001" + email: user2001@example.com + keep_email_private: false + email_notifications_preference: onmention + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: true + login_source: 0 + login_name: user2001 + type: 0 + salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: "" + avatar_email: user2001@example.com + use_custom_avatar: true + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false + created_unix: 1759086716 diff --git a/routers/web/web.go b/routers/web/web.go index 20d5376cfe..162055ef0b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -167,12 +167,14 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont return } } else if ctx.Req.URL.Path == "/user/settings/change_password" { + if ctx.Doer.MustHaveTwoFactor() { + ctx.Redirect(setting.AppSubURL + "/user/settings/security") + return + } // make sure that the form cannot be accessed by users who don't need this ctx.Redirect(setting.AppSubURL + "/") return - } - - if ctx.Doer.MustHaveTwoFactor() && !strings.HasPrefix(ctx.Req.URL.Path, "/user/settings/security") { + } else if ctx.Doer.MustHaveTwoFactor() && !strings.HasPrefix(ctx.Req.URL.Path, "/user/settings/security") { hasTwoFactor, err := auth_model.HasTwoFactorByUID(ctx, ctx.Doer.ID) if err != nil { log.Error("Error getting 2fa: %s", err) diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index 5e8dead5b7..77539bb754 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -17,6 +17,7 @@ import ( "forgejo.org/modules/setting" "forgejo.org/modules/test" "forgejo.org/modules/translation" + "forgejo.org/services/forms" "forgejo.org/tests" "github.com/stretchr/testify/assert" @@ -276,3 +277,64 @@ func TestGlobalTwoFactorRequirement(t *testing.T) { }) }) } + +func TestTwoFactorWithPasswordChange(t *testing.T) { + defer unittest.OverrideFixtures("models/fixtures/TestTwoFactorWithPasswordChange")() + + normalUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + changePasswordUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{MustChangePassword: true}) + + runTest := func(t *testing.T, user *user_model.User, requireTOTP bool) { + t.Helper() + defer unittest.AssertSuccessfulDelete(t, &auth.TwoFactor{UID: user.ID}) + + session := loginUser(t, user.Name) + + if user.MustChangePassword { + req := NewRequest(t, "GET", fmt.Sprintf("/%s", user.Name)) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/user/settings/change_password", resp.Header().Get("Location")) + + req = NewRequest(t, "GET", "/user/settings/security") + resp = session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/user/settings/change_password", resp.Header().Get("Location")) + + req = NewRequestWithJSON(t, "POST", "/user/settings/change_password", forms.MustChangePasswordForm{ + Password: "password", + Retype: "password", + }) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "/user/settings/security", resp.Header().Get("Location")) + } + + if requireTOTP { + req := NewRequest(t, "GET", fmt.Sprintf("/%s", user.Name)) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/user/settings/security", resp.Header().Get("Location")) + + req = NewRequest(t, "GET", "/user/settings/change_password") + resp = session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/user/settings/security", resp.Header().Get("Location")) + + session.EnrollTOTP(t) + } + + req := NewRequest(t, "GET", fmt.Sprintf("/%s", user.Name)) + session.MakeRequest(t, req, http.StatusOK) + } + + t.Run("Don't require TwoFactor", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + runTest(t, normalUser, false) + runTest(t, changePasswordUser, false) + }) + + t.Run("Require TwoFactor", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.GlobalTwoFactorRequirement, setting.AllTwoFactorRequirement)() + + runTest(t, normalUser, true) + runTest(t, changePasswordUser, true) + }) +}