User login
In this chapter were going to focus on creating the user login page for our application.
Before we get into the main part of this work, let’s quickly revisit the internal/validator
package that we made earlier and update it to support validation errors which aren’t associated with one specific form field.
We’ll use this later in the chapter to show the user a generic “your email address or password is wrong” message if their login fails, as this considered more secure than explicitly indicating why the login failed.
Please go ahead and update your internal/validator/validator.go
file like so:
package validator ... // Add a new NonFieldErrors []string field to the struct, which we will use to // hold any validation errors which are not related to a specific form field. type Validator struct { NonFieldErrors []string FieldErrors map[string]string } // Update the Valid() method to also check that the NonFieldErrors slice is // empty. func (v *Validator) Valid() bool { return len(v.FieldErrors) == 0 && len(v.NonFieldErrors) == 0 } // Create an AddNonFieldError() helper for adding error messages to the new // NonFieldErrors slice. func (v *Validator) AddNonFieldError(message string) { v.NonFieldErrors = append(v.NonFieldErrors, message) } ...
Next let’s create a new ui/html/pages/login.tmpl
template containing the markup for our login page. We’ll follow the same pattern for showing validation errors and re-displaying data that we used for our signup page.
$ touch ui/html/pages/login.tmpl
{{define "title"}}Login{{end}} {{define "main"}} <form action='/user/login' method='POST' novalidate> <!-- Notice that here we are looping over the NonFieldErrors and displaying them, if any exist --> {{range .Form.NonFieldErrors}} <div class='error'>{{.}}</div> {{end}} <div> <label>Email:</label> {{with .Form.FieldErrors.email}} <label class='error'>{{.}}</label> {{end}} <input type='email' name='email' value='{{.Form.Email}}'> </div> <div> <label>Password:</label> {{with .Form.FieldErrors.password}} <label class='error'>{{.}}</label> {{end}} <input type='password' name='password'> </div> <div> <input type='submit' value='Login'> </div> </form> {{end}}
Then let’s head to our cmd/web/handlers.go
file and create a new userLoginForm
struct (to represent and hold the form data), and adapt our userLogin
handler to render the login page.
Like so:
package main ... // Create a new userLoginForm struct. type userLoginForm struct { Email string `form:"email"` Password string `form:"password"` validator.Validator `form:"-"` } // Update the handler so it displays the login page. func (app *application) userLogin(w http.ResponseWriter, r *http.Request) { data := app.newTemplateData(r) data.Form = userLoginForm{} app.render(w, r, http.StatusOK, "login.tmpl", data) } ...
If you run the application and visit https://localhost:4000/user/login
, you should now see the login page looking like this:

Verifying the user details
The next step is the interesting part: how do we verify that the email and password submitted by a user are correct?
The core part of this verification logic will take place in the UserModel.Authenticate()
method of our user model. Specifically, we’ll need it to do two things:
First it should retrieve the hashed password associated with the email address from our MySQL
users
table. If the email doesn’t exist in the database, we will return theErrInvalidCredentials
error that we made earlier.Otherwise, we want to compare the hashed password from the
users
table with the plain-text password that the user provided when logging in. If they don’t match, we want to return theErrInvalidCredentials
error again. But if they do match, we want to return the user’sid
value from the database.
Let’s do exactly that. Go ahead and add the following code to your internal/models/users.go
file:
package models ... func (m *UserModel) Authenticate(email, password string) (int, error) { // Retrieve the id and hashed password associated with the given email. If // no matching email exists we return the ErrInvalidCredentials error. var id int var hashedPassword []byte stmt := "SELECT id, hashed_password FROM users WHERE email = ?" err := m.DB.QueryRow(stmt, email).Scan(&id, &hashedPassword) if err != nil { if errors.Is(err, sql.ErrNoRows) { return 0, ErrInvalidCredentials } else { return 0, err } } // Check whether the hashed password and plain-text password provided match. // If they don't, we return the ErrInvalidCredentials error. err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)) if err != nil { if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { return 0, ErrInvalidCredentials } else { return 0, err } } // Otherwise, the password is correct. Return the user ID. return id, nil }
Our next step involves updating the userLoginPost
handler so that it parses the submitted login form data and calls this UserModel.Authenticate()
method.
If the login details are valid, we then want to add the user’s id
to their session data so that — for future requests — we know that they have authenticated successfully and which user they are.
Head over to your handlers.go
file and update it as follows:
package main ... func (app *application) userLoginPost(w http.ResponseWriter, r *http.Request) { // Decode the form data into the userLoginForm struct. var form userLoginForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } // Do some validation checks on the form. We check that both email and // password are provided, and also check the format of the email address as // a UX-nicety (in case the user makes a typo). form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank") form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address") form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank") if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "login.tmpl", data) return } // Check whether the credentials are valid. If they're not, add a generic // non-field error message and re-display the login page. id, err := app.users.Authenticate(form.Email, form.Password) if err != nil { if errors.Is(err, models.ErrInvalidCredentials) { form.AddNonFieldError("Email or password is incorrect") data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "login.tmpl", data) } else { app.serverError(w, r, err) } return } // Use the RenewToken() method on the current session to change the session // ID. It's good practice to generate a new session ID when the // authentication state or privilege levels changes for the user (e.g. login // and logout operations). err = app.sessionManager.RenewToken(r.Context()) if err != nil { app.serverError(w, r, err) return } // Add the ID of the current user to the session, so that they are now // 'logged in'. app.sessionManager.Put(r.Context(), "authenticatedUserID", id) // Redirect the user to the create snippet page. http.Redirect(w, r, "/snippet/create", http.StatusSeeOther) } ...
Alright, let’s give this a try!
Restart the application and try submitting some invalid user credentials…

You should get a non-field validation error message which looks like this:

But when you input some correct credentials (use the email address and password for the user that you created in the previous chapter), the application should log you in and redirect you to the create snippet page, like so:


We’ve covered a lot of ground in the last two chapters, so let’s quickly take stock of where things are at.
Users can now register with the site using the
GET /user/signup
form. We store the details of registered users (including a hashed version of their password) in theusers
table of our database.Registered users can then authenticate by using the
GET /user/login
form to provide their email address and password. If these match the details of a registered user, we deem them to have authenticated successfully and add the relevant"authenticatedUserID"
value to their session data.