Implement a ‘Change Password’ feature
Your goal in this exercise is to add the facility for an authenticated user to change their password, using a form which looks similar to this:

During this exercise you should make sure to:
- Ask the user for their current password and verify that it matches the hashed password in the
users
table (to confirm it is actually them making the request). - Hash their new password before updating the
users
table.
Step 1
Create two new routes and handlers:
GET /account/password/update
which maps to a newaccountPasswordUpdate
handler.POST /account/password/update
which maps to a newaccountPasswordUpdatePost
handler.
Both routes should be restricted to authenticated users only.
Step 2
Create a new ui/html/pages/password.tmpl
file which contains the change password form. This form should:
- Have three fields:
currentPassword
,newPassword
andnewPasswordConfirmation
. POST
the form data to/account/password/update
when submitted.- Display errors for each of the fields in the event of a validation error.
- Not re-display passwords in the event of a validation error.
Hint: You might want to use the work we did on the user signup form as a guide here.
Then update the cmd/web/handlers.go
file to include a new accountPasswordUpdateForm
struct that you can parse the form data into, and update the accountPasswordUpdate
handler to display this empty form.
When you visit https://localhost:4000/account/password/update
as an authenticated user it should look similar to this:

Step 3
Update the accountPasswordUpdatePost
handler to carry out the following form validation checks, and re-display the form with the relevant error messages in the event of any failures.
- All three fields are required.
- The
newPassword
value must be at least 8 characters long. - The
newPassword
andnewPasswordConfirmation
values must match.

Step 4
In your internal/models/users.go
file create a new UserModel.PasswordUpdate()
method with the following signature:
func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error
In this method:
- Retrieve the user details for the user with the ID given by the
id
parameter from the database. - Check that the
currentPassword
value matches the hashed password for the user. If it doesn’t match, return anErrInvalidCredentials
error. - Otherwise, hash the
newPassword
value and update thehashed_password
column in theusers
table for the relevant user.
Also update the UserModelInterface
interface type to include the PasswordUpdate()
method that you’ve just created.
Step 5
Update the accountPasswordUpdatePost
handler so that if the form is valid, it calls the UserModel.PasswordUpdate()
method (remember, the user’s ID should be in the session data).
In the event of a models.ErrInvalidCredentials
error, inform the user that they have entered the wrong value in the currentPassword
form field. Otherwise, add a flash message to the user’s session saying that their password has been successfully changed and redirect them to their account page.
Step 6
Update the account to include a link to the change password form, similar to this:

Step 7
Try running the tests for the application. You should get a failure because the mocks.UserModel
type no longer satisfies the interface specified in the models.UserModelInterface
struct. Fix this by adding the appropriate PasswordUpdate()
method to the mock and make sure that the tests pass.
Suggested code
Suggested code for step 1
package main ... func (app *application) accountPasswordUpdate(w http.ResponseWriter, r *http.Request) { // Some code will go here later... } func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) { // Some code will go here later... }
package main ... func (app *application) routes() http.Handler { mux := http.NewServeMux() mux.Handle("GET /static/", http.FileServerFS(ui.Files)) mux.HandleFunc("GET /ping", ping) dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate) mux.Handle("GET /{$}", dynamic.ThenFunc(app.home)) mux.Handle("GET /about", dynamic.ThenFunc(app.about)) mux.Handle("GET /snippet/view/{id}", dynamic.ThenFunc(app.snippetView)) mux.Handle("GET /user/signup", dynamic.ThenFunc(app.userSignup)) mux.Handle("POST /user/signup", dynamic.ThenFunc(app.userSignupPost)) mux.Handle("GET /user/login", dynamic.ThenFunc(app.userLogin)) mux.Handle("POST /user/login", dynamic.ThenFunc(app.userLoginPost)) protected := dynamic.Append(app.requireAuthentication) mux.Handle("GET /snippet/create", protected.ThenFunc(app.snippetCreate)) mux.Handle("POST /snippet/create", protected.ThenFunc(app.snippetCreatePost)) mux.Handle("GET /account/view", protected.ThenFunc(app.accountView)) // Add the two new routes, restricted to authenticated users only. mux.Handle("GET /account/password/update", protected.ThenFunc(app.accountPasswordUpdate)) mux.Handle("POST /account/password/update", protected.ThenFunc(app.accountPasswordUpdatePost)) mux.Handle("POST /user/logout", protected.ThenFunc(app.userLogoutPost)) standard := alice.New(app.recoverPanic, app.logRequest, commonHeaders) return standard.Then(mux) }
Suggested code for step 2
{{define "title"}}Change Password{{end}} {{define "main"}} <h2>Change Password</h2> <form action='/account/password/update' method='POST' novalidate> <input type='hidden' name='csrf_token' value='{{.CSRFToken}}'> <div> <label>Current password:</label> {{with .Form.FieldErrors.currentPassword}} <label class='error'>{{.}}</label> {{end}} <input type='password' name='currentPassword'> </div> <div> <label>New password:</label> {{with .Form.FieldErrors.newPassword}} <label class='error'>{{.}}</label> {{end}} <input type='password' name='newPassword'> </div> <div> <label>Confirm new password:</label> {{with .Form.FieldErrors.newPasswordConfirmation}} <label class='error'>{{.}}</label> {{end}} <input type='password' name='newPasswordConfirmation'> </div> <div> <input type='submit' value='Change password'> </div> </form> {{end}}
package main ... type accountPasswordUpdateForm struct { CurrentPassword string `form:"currentPassword"` NewPassword string `form:"newPassword"` NewPasswordConfirmation string `form:"newPasswordConfirmation"` validator.Validator `form:"-"` } func (app *application) accountPasswordUpdate(w http.ResponseWriter, r *http.Request) { data := app.newTemplateData(r) data.Form = accountPasswordUpdateForm{} app.render(w, r, http.StatusOK, "password.tmpl", data) } ...
Suggested code for step 3
package main ... func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) { var form accountPasswordUpdateForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } form.CheckField(validator.NotBlank(form.CurrentPassword), "currentPassword", "This field cannot be blank") form.CheckField(validator.NotBlank(form.NewPassword), "newPassword", "This field cannot be blank") form.CheckField(validator.MinChars(form.NewPassword, 8), "newPassword", "This field must be at least 8 characters long") form.CheckField(validator.NotBlank(form.NewPasswordConfirmation), "newPasswordConfirmation", "This field cannot be blank") form.CheckField(form.NewPassword == form.NewPasswordConfirmation, "newPasswordConfirmation", "Passwords do not match") if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "password.tmpl", data) return } }
Suggested code for step 4
package models ... type UserModelInterface interface { Insert(name, email, password string) error Authenticate(email, password string) (int, error) Exists(id int) (bool, error) Get(id int) (User, error) PasswordUpdate(id int, currentPassword, newPassword string) error } ... func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error { var currentHashedPassword []byte stmt := "SELECT hashed_password FROM users WHERE id = ?" err := m.DB.QueryRow(stmt, id).Scan(¤tHashedPassword) if err != nil { return err } err = bcrypt.CompareHashAndPassword(currentHashedPassword, []byte(currentPassword)) if err != nil { if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { return ErrInvalidCredentials } else { return err } } newHashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), 12) if err != nil { return err } stmt = "UPDATE users SET hashed_password = ? WHERE id = ?" _, err = m.DB.Exec(stmt, string(newHashedPassword), id) return err }
Suggested code for step 5
package main ... func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) { var form accountPasswordUpdateForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } form.CheckField(validator.NotBlank(form.CurrentPassword), "currentPassword", "This field cannot be blank") form.CheckField(validator.NotBlank(form.NewPassword), "newPassword", "This field cannot be blank") form.CheckField(validator.MinChars(form.NewPassword, 8), "newPassword", "This field must be at least 8 characters long") form.CheckField(validator.NotBlank(form.NewPasswordConfirmation), "newPasswordConfirmation", "This field cannot be blank") form.CheckField(form.NewPassword == form.NewPasswordConfirmation, "newPasswordConfirmation", "Passwords do not match") if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "password.tmpl", data) return } userID := app.sessionManager.GetInt(r.Context(), "authenticatedUserID") err = app.users.PasswordUpdate(userID, form.CurrentPassword, form.NewPassword) if err != nil { if errors.Is(err, models.ErrInvalidCredentials) { form.AddFieldError("currentPassword", "Current password is incorrect") data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "password.tmpl", data) } else { app.serverError(w, r, err) } return } app.sessionManager.Put(r.Context(), "flash", "Your password has been updated!") http.Redirect(w, r, "/account/view", http.StatusSeeOther) }
Suggested code for step 6
{{define "title"}}Your Account{{end}} {{define "main"}} <h2>Your Account</h2> {{with .User}} <table> <tr> <th>Name</th> <td>{{.Name}}</td> </tr> <tr> <th>Email</th> <td>{{.Email}}</td> </tr> <tr> <th>Joined</th> <td>{{humanDate .Created}}</td> </tr> <tr> <!-- Add a link to the change password form --> <th>Password</th> <td><a href="/account/password/update">Change password</a></td> </tr> </table> {{end }} {{end}}
Suggested code for step 7
$ go test ./... # snippetbox.alexedwards.net/cmd/web [snippetbox.alexedwards.net/cmd/web.test] cmd/web/testutils_test.go:48:19: cannot use &mocks.UserModel{} (value of type *mocks.UserModel) as type models.UserModelInterface in struct literal: *mocks.UserModel does not implement models.UserModelInterface (missing PasswordUpdate method) FAIL snippetbox.alexedwards.net/cmd/web [build failed] ok snippetbox.alexedwards.net/internal/models 1.099s ? snippetbox.alexedwards.net/internal/models/mocks [no test files] ? snippetbox.alexedwards.net/internal/validator [no test files] ? snippetbox.alexedwards.net/ui [no test files] FAIL
package mocks ... func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error { if id == 1 { if currentPassword != "pa$$word" { return models.ErrInvalidCredentials } return nil } return models.ErrNoRecord }
$ go test ./... ok snippetbox.alexedwards.net/cmd/web 0.026s ok snippetbox.alexedwards.net/internal/models (cached) ? snippetbox.alexedwards.net/internal/models/mocks [no test files] ? snippetbox.alexedwards.net/internal/validator [no test files] ? snippetbox.alexedwards.net/ui [no test files]