Ask for the new password twice
Changing your own password had one input for it. Nothing can be read back, and unlike the current password it has never been typed before, so a typo saves silently and locks you out of an account you could reach a moment earlier. The only way back is an admin reset. The admin's reset of someone else's password is untouched: that is the recovery path, not a self-service change.
This commit is contained in:
@@ -132,6 +132,13 @@ func (a *app) editProfile(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if newPassword := r.FormValue("new_password"); newPassword != "" {
|
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) {
|
if !a.changePassword(w, r, me.ID, r.FormValue("current_password"), newPassword) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, "[email protected]")
|
||||||
|
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": {"[email protected]"},
|
||||||
|
"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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,8 @@
|
|||||||
<label>Kuva <input type="file" name="avatar" accept="image/*"></label>
|
<label>Kuva <input type="file" name="avatar" accept="image/*"></label>
|
||||||
<label>Nykyinen salasana <input type="password" name="current_password" autocomplete="current-password"></label>
|
<label>Nykyinen salasana <input type="password" name="current_password" autocomplete="current-password"></label>
|
||||||
<label>Uusi salasana <input type="password" name="new_password" autocomplete="new-password"></label>
|
<label>Uusi salasana <input type="password" name="new_password" autocomplete="new-password"></label>
|
||||||
|
<!-- Neither field can be read back, and the new password has never been typed before. -->
|
||||||
|
<label>Toista uusi salasana <input type="password" name="new_password_repeat" autocomplete="new-password"></label>
|
||||||
<button type="submit">Tallenna</button>
|
<button type="submit">Tallenna</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="muted small">Salasanan vaihto vaatii nykyisen salasanan ja kirjaa ulos muut laitteesi.</p>
|
<p class="muted small">Salasanan vaihto vaatii nykyisen salasanan ja kirjaa ulos muut laitteesi.</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user