diff --git a/src/profile.go b/src/profile.go index 234800c..3bcc096 100644 --- a/src/profile.go +++ b/src/profile.go @@ -132,6 +132,13 @@ func (a *app) editProfile(w http.ResponseWriter, r *http.Request) { } if newPassword := r.FormValue("new_password"); newPassword != "" { + // A typo here would lock them out of an account they can still reach right now, and the + // only way back is an admin reset. + if newPassword != r.FormValue("new_password_repeat") { + a.flash(w, "Uudet salasanat eivät täsmää.") + http.Redirect(w, r, "/profile", http.StatusSeeOther) + return + } if !a.changePassword(w, r, me.ID, r.FormValue("current_password"), newPassword) { return } diff --git a/src/profile_test.go b/src/profile_test.go new file mode 100644 index 0000000..a297a8d --- /dev/null +++ b/src/profile_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "net/http" + "net/url" + "testing" + + "golang.org/x/crypto/bcrypt" +) + +// The repeat field is the only guard against a typo in something nobody can read back: the account +// is reachable right now, and a mistyped new password makes it reachable only through an admin. +func TestPasswordChangeNeedsMatchingRepeat(t *testing.T) { + a := testApp(t) + ctx := context.Background() + mux := a.withMember(a.memberMux()) + + id := a.seedMember(t, "esa@example.com") + hash, err := bcrypt.GenerateFromPassword([]byte("vanha1"), bcrypt.MinCost) + if err != nil { + t.Fatal(err) + } + if _, err := a.db.ExecContext(ctx, + `update users set password_hash = $2 where id = $1`, id, string(hash)); err != nil { + t.Fatal(err) + } + tok, _, err := a.startSession(ctx, id, false) + if err != nil { + t.Fatal(err) + } + + form := func(repeat string) url.Values { + return url.Values{ + "name": {"Esa"}, "email": {"esa@example.com"}, + "current_password": {"vanha1"}, + "new_password": {"uusi1"}, "new_password_repeat": {repeat}, + } + } + current := func() string { + var h string + if err := a.db.QueryRowContext(ctx, + `select password_hash from users where id = $1`, id).Scan(&h); err != nil { + t.Fatal(err) + } + return h + } + + if w := postAs(t, mux, "/profile", form("uusi2"), tok); w.Code != http.StatusSeeOther { + t.Fatalf("mismatch: status = %d, want 303", w.Code) + } + if bcrypt.CompareHashAndPassword([]byte(current()), []byte("vanha1")) != nil { + t.Fatal("a mismatched repeat still changed the password") + } + + if w := postAs(t, mux, "/profile", form("uusi1"), tok); w.Code != http.StatusSeeOther { + t.Fatalf("match: status = %d, want 303", w.Code) + } + if bcrypt.CompareHashAndPassword([]byte(current()), []byte("uusi1")) != nil { + t.Fatal("a matching repeat did not change the password") + } +} diff --git a/src/templates/profile.html b/src/templates/profile.html index 89c9670..2afbc57 100644 --- a/src/templates/profile.html +++ b/src/templates/profile.html @@ -50,6 +50,8 @@ + +
Salasanan vaihto vaatii nykyisen salasanan ja kirjaa ulos muut laitteesi.