package server import ( "net/http" "net/url" "strings" "testing" "time" "cc.tiredsysadmin.keeporg/internal/store" "cc.tiredsysadmin.keeporg/internal/totp" ) const newAdminPassword = "a-much-better-password" func TestChangePasswordRequiresTheCurrentOne(t *testing.T) { h := newHarness(t) tok, _, _ := h.login("admin", adminPassword, "") status, body := h.call("POST", "/api/v1/auth/password", tok, map[string]string{ "current_password": "not-the-password", "new_password": newAdminPassword}) if status != http.StatusUnauthorized || body["code"] != "bad_credentials" { t.Fatalf("wrong current password: %d %v", status, body) } // And the password really did not change. if _, s, _ := h.login("admin", adminPassword, ""); s != http.StatusOK { t.Fatal("the old password stopped working after a failed change") } } func TestChangePasswordRejectsAWeakOrUnchangedPassword(t *testing.T) { h := newHarness(t) tok, _, _ := h.login("admin", adminPassword, "") for _, next := range []string{"short", adminPassword} { status, body := h.call("POST", "/api/v1/auth/password", tok, map[string]string{ "current_password": adminPassword, "new_password": next}) if status != http.StatusBadRequest { t.Fatalf("new password %q: %d %v", next, status, body) } } } func TestChangePasswordSwapsThePasswordAndRevokesOtherSessions(t *testing.T) { h := newHarness(t) keep, _, _ := h.login("admin", adminPassword, "") elsewhere, _, _ := h.login("admin", adminPassword, "") status, body := h.call("POST", "/api/v1/auth/password", keep, map[string]string{ "current_password": adminPassword, "new_password": newAdminPassword}) if status != http.StatusOK { t.Fatalf("change: %d %v", status, body) } if _, s, _ := h.login("admin", adminPassword, ""); s != http.StatusUnauthorized { t.Fatal("the old password still works") } if _, s, _ := h.login("admin", newAdminPassword, ""); s != http.StatusOK { t.Fatal("the new password does not work") } // The session that made the change survives; the other one does not. if s, _ := h.call("GET", "/api/v1/auth/whoami", keep, nil); s != http.StatusOK { t.Fatalf("the calling session was revoked: %d", s) } if s, _ := h.call("GET", "/api/v1/auth/whoami", elsewhere, nil); s != http.StatusUnauthorized { t.Fatalf("another session survived the password change: %d", s) } } func TestChangePasswordRequiresTheSecondFactorWhenEnrolled(t *testing.T) { h := newHarness(t) tok, _, _ := h.login("admin", adminPassword, "") _, enroll := h.call("POST", "/api/v1/auth/totp/enroll", tok, nil) sec := enroll["secret"].(string) code, _ := totp.Code(sec, time.Now()) if s, _ := h.call("POST", "/api/v1/auth/totp/confirm", tok, map[string]string{"code": code}); s != http.StatusOK { t.Fatal("could not enrol the second factor") } status, body := h.call("POST", "/api/v1/auth/password", tok, map[string]string{ "current_password": adminPassword, "new_password": newAdminPassword}) if status != http.StatusUnauthorized || body["code"] != "totp_required" { t.Fatalf("change without a code: %d %v", status, body) } code, _ = totp.Code(sec, time.Now()) if s, b := h.call("POST", "/api/v1/auth/password", tok, map[string]string{ "current_password": adminPassword, "new_password": newAdminPassword, "totp": code}); s != http.StatusOK { t.Fatalf("change with a code: %d %v", s, b) } } func TestWebChangesThePassword(t *testing.T) { h := newHarness(t) b := newBrowser(t, h) csrf := b.signIn() // The form is on the page, and it is guarded like the others. _, page := b.get("/web/vaults") if !strings.Contains(page, "Change your account password") { t.Fatalf("no password form on the vault page:\n%s", page) } if status, _ := b.post("/web/password", url.Values{ "current_password": {adminPassword}, "new_password": {newAdminPassword}, "new_password2": {newAdminPassword}}); status != http.StatusForbidden { t.Fatalf("a form without a CSRF token returned %d", status) } _, page = b.post("/web/password", url.Values{"csrf": {csrf}, "current_password": {adminPassword}, "new_password": {newAdminPassword}, "new_password2": {"something-else"}}) if !strings.Contains(page, "do not match") { t.Fatalf("mismatch not reported:\n%s", page) } _, page = b.post("/web/password", url.Values{"csrf": {csrf}, "current_password": {adminPassword}, "new_password": {newAdminPassword}, "new_password2": {newAdminPassword}}) if !strings.Contains(page, "Password changed") { t.Fatalf("no confirmation:\n%s", page) } if _, s, _ := h.login("admin", newAdminPassword, ""); s != http.StatusOK { t.Fatal("the new password does not work") } } // An account that exists only through single sign-on has no password to // change, and must not be able to acquire one this way. func TestChangePasswordRefusesAnSSOOnlyAccount(t *testing.T) { h := newHarness(t) if err := h.srv.Store().Update(func(st *store.State) error { st.Users["sso"] = &store.User{ID: "sso", Username: "ssouser", OIDCIssuer: "https://id.example", OIDCSubject: "s"} st.Sessions["sso-token"] = &store.Session{Token: "sso-token", UserID: "sso", ExpiresAt: time.Now().Add(time.Hour)} return nil }); err != nil { t.Fatal(err) } status, body := h.call("POST", "/api/v1/auth/password", "sso-token", map[string]string{ "current_password": "", "new_password": newAdminPassword}) if status != http.StatusUnauthorized { t.Fatalf("an account with no password set one: %d %v", status, body) } }