User signup and password encryption
Before we can log in any users to our Snippetbox application, we need a way for them to sign up for an account. We’ll cover how to do that in this chapter.
Go ahead and create a new ui/html/pages/signup.tmpl
file containing the following markup for the signup form.
$ touch ui/html/pages/signup.tmpl
{{define "title"}}Signup{{end}} {{define "main"}} <form action='/user/signup' method='POST' novalidate> <div> <label>Name:</label> {{with .Form.FieldErrors.name}} <label class='error'>{{.}}</label> {{end}} <input type='text' name='name' value='{{.Form.Name}}'> </div> <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='Signup'> </div> </form> {{end}}
Hopefully this should feel familiar so far. For the signup form we’re using exactly the same form structure that we used earlier in the book, with three fields: name
, email
and password
(which use the relevant HTML5 input types).
Then let’s update our cmd/web/handlers.go
file to include a new userSignupForm
struct (which will represent and hold the form data), and hook it up to the userSignup
handler.
Like so:
package main ... // Create a new userSignupForm struct. type userSignupForm struct { Name string `form:"name"` Email string `form:"email"` Password string `form:"password"` validator.Validator `form:"-"` } // Update the handler so it displays the signup page. func (app *application) userSignup(w http.ResponseWriter, r *http.Request) { data := app.newTemplateData(r) data.Form = userSignupForm{} app.render(w, r, http.StatusOK, "signup.tmpl", data) } ...
If you run the application and visit https://localhost:4000/user/signup
, you should now see a page which looks like this:

Validating the user input
When this form is submitted the data will end up being posted to the userSignupPost
handler that we made earlier.
The first task of this handler will be to validate the data to make sure that it is sane and sensible before we insert it into the database. Specifically, we want to do four things:
- Check that the provided name, email address and password are not blank.
- Sanity check the format of the email address.
- Ensure that the password is at least 8 characters long.
- Make sure that the email address isn’t already in use.
We can cover the first three checks by heading back to our internal/validator/validator.go
file and creating two helper new methods — MinChars()
and Matches()
— along with a regular expression for sanity checking an email address.
Like this:
package validator import ( "regexp" // New import "slices" "strings" "unicode/utf8" ) // Use the regexp.MustCompile() function to parse a regular expression pattern // for sanity checking the format of an email address. This returns a pointer to // a 'compiled' regexp.Regexp type, or panics in the event of an error. Parsing // this pattern once at startup and storing the compiled *regexp.Regexp in a // variable is more performant than re-parsing the pattern each time we need it. var EmailRX = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") ... // MinChars() returns true if a value contains at least n characters. func MinChars(value string, n int) bool { return utf8.RuneCountInString(value) >= n } // Matches() returns true if a value matches a provided compiled regular // expression pattern. func Matches(value string, rx *regexp.Regexp) bool { return rx.MatchString(value) }
There are a couple of things about the EmailRX
regular expression pattern I want to quickly mention:
The pattern we’re using is the one currently recommended by the W3C and Web Hypertext Application Technology Working Group for validating email addresses. For more information about this pattern, see here. If you’re reading this book in PDF format or on a narrow device, and can’t see the entire line, then here it is broken up into multiple lines:
"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? (?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
In your code, this regexp pattern should all be on a single line with no whitespace. If there’s an alternative pattern that you prefer to use for email address sanity checking, then feel free to swap it in instead.
Because the
EmailRX
regexp pattern is written as an interpreted string literal, we need to double-escape special characters in the regexp with\\
for it to work correctly (we can’t use a raw string literal because the pattern contains a back quote character). If you’re not familiar with the difference between string literal forms, then this section of the Go spec is worth a read.
But anyway, I’m digressing. Let’s get back to the task at hand.
Head over to your handlers.go
file and add some code to process the form and run the validation checks like so:
package main ... func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) { // Declare an zero-valued instance of our userSignupForm struct. var form userSignupForm // Parse the form data into the userSignupForm struct. err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } // Validate the form contents using our helper functions. form.CheckField(validator.NotBlank(form.Name), "name", "This field cannot be blank") 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") form.CheckField(validator.MinChars(form.Password, 8), "password", "This field must be at least 8 characters long") // If there are any errors, redisplay the signup form along with a 422 // status code. if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "signup.tmpl", data) return } // Otherwise send the placeholder response (for now!). fmt.Fprintln(w, "Create a new user...") } ...
Try running the application now and putting some invalid data into the signup form, like this:

And if you try to submit it, you should see the appropriate validation failures returned like so:

All that remains now is the fourth validation check: make sure that the email address isn’t already in use. This is a bit trickier to deal with.
Because we’ve got a UNIQUE
constraint on the email
field of our users
table, it’s already guaranteed that we won’t end up with two users in our database who have the same email address. So from a business logic and data integrity point of view we are already OK. But the question remains about how we communicate any email already in use problem to a user. We’ll tackle this at the end of the chapter.
A brief introduction to bcrypt
If your database is ever compromised by an attacker, it’s hugely important that it doesn’t contain the plain-text versions of your users’ passwords.
It’s good practice — well, essential, really — to store a one-way hash of the password, derived with a computationally expensive key-derivation function such as Argon2, scrypt or bcrypt. Go has implementations of all 3 algorithms in the golang.org/x/crypto
package.
However a plus-point of the bcrypt implementation specifically is that it includes helper functions specifically designed for hashing and checking passwords, and that’s what we’ll use here.
If you’re following along, please go ahead and download the latest version of the golang.org/x/crypto/bcrypt
package:
$ go get golang.org/x/crypto/bcrypt@latest go: downloading golang.org/x/crypto v0.26.0 go get: added golang.org/x/crypto v0.26.0
There are two functions that we’ll use in this book. The first is the bcrypt.GenerateFromPassword()
function which lets us create a hash of a given plain-text password like so:
hash, err := bcrypt.GenerateFromPassword([]byte("my plain text password"), 12)
This function will return a 60-character long hash which looks a bit like this:
$2a$12$NuTjWXm3KKntReFwyBVHyuf/to.HEwTy.eS206TNfkGfr6HzGJSWG
The second parameter that we pass to bcrypt.GenerateFromPassword()
indicates the cost, which is represented by an integer between 4 and 31. The example above uses a cost of 12, which means that 4096 (2^12) bcrypt iterations will be used to generate the password hash.
The higher the cost, the more expensive the hash will be for an attacker to crack (which is a good thing). But a higher cost also means that our application needs to do more work to create the password hash when a user signs up — and that means increased resource use by your application and additional latency for the end user. So choosing an appropriate cost value is a balancing act. A cost of 12 is a reasonable minimum, but if possible you should carry out load testing, and if you can set the cost higher without adversely affecting user experience then you should.
On the flip side, we can check that a plain-text password matches a particular hash using the bcrypt.CompareHashAndPassword()
function like so:
hash := []byte("$2a$12$NuTjWXm3KKntReFwyBVHyuf/to.HEwTy.eS206TNfkGfr6GzGJSWG") err := bcrypt.CompareHashAndPassword(hash, []byte("my plain text password"))
The bcrypt.CompareHashAndPassword()
function will return nil
if the plain-text password matches a particular hash, or an error if they don’t match.
Storing the user details
The next stage of our build is to update the UserModel.Insert()
method so that it creates a new record in our users
table containing the validated name, email and hashed password.
This will be interesting for two reasons: first we want to store the bcrypt hash of the password (not the password itself) and second, we also need to manage the potential error caused by a duplicate email violating the UNIQUE
constraint that we added to the table.
All errors returned by MySQL have a particular code, which we can use to triage what has caused the error (a full list of the MySQL error codes and descriptions can be found here). In the case of a duplicate email, the error code used will be 1062 (ER_DUP_ENTRY)
.
Open the internal/models/users.go
file and update it to include the following code:
package models import ( "database/sql" "errors" // New import "strings" // New import "time" "github.com/go-sql-driver/mysql" // New import "golang.org/x/crypto/bcrypt" // New import ) ... type UserModel struct { DB *sql.DB } func (m *UserModel) Insert(name, email, password string) error { // Create a bcrypt hash of the plain-text password. hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12) if err != nil { return err } stmt := `INSERT INTO users (name, email, hashed_password, created) VALUES(?, ?, ?, UTC_TIMESTAMP())` // Use the Exec() method to insert the user details and hashed password // into the users table. _, err = m.DB.Exec(stmt, name, email, string(hashedPassword)) if err != nil { // If this returns an error, we use the errors.As() function to check // whether the error has the type *mysql.MySQLError. If it does, the // error will be assigned to the mySQLError variable. We can then check // whether or not the error relates to our users_uc_email key by // checking if the error code equals 1062 and the contents of the error // message string. If it does, we return an ErrDuplicateEmail error. var mySQLError *mysql.MySQLError if errors.As(err, &mySQLError) { if mySQLError.Number == 1062 && strings.Contains(mySQLError.Message, "users_uc_email") { return ErrDuplicateEmail } } return err } return nil } ...
We can then finish this all off by updating the userSignup
handler like so:
package main ... func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) { var form userSignupForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } form.CheckField(validator.NotBlank(form.Name), "name", "This field cannot be blank") 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") form.CheckField(validator.MinChars(form.Password, 8), "password", "This field must be at least 8 characters long") if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "signup.tmpl", data) return } // Try to create a new user record in the database. If the email already // exists then add an error message to the form and re-display it. err = app.users.Insert(form.Name, form.Email, form.Password) if err != nil { if errors.Is(err, models.ErrDuplicateEmail) { form.AddFieldError("email", "Email address is already in use") data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "signup.tmpl", data) } else { app.serverError(w, r, err) } return } // Otherwise add a confirmation flash message to the session confirming that // their signup worked. app.sessionManager.Put(r.Context(), "flash", "Your signup was successful. Please log in.") // And redirect the user to the login page. http.Redirect(w, r, "/user/login", http.StatusSeeOther) } ...
Save the files, restart the application and try signing up for an account. Make sure to remember the email address and password that you use… you’ll need them in the next chapter!

If everything works correctly, you should find that your browser redirects you to https://localhost:4000/user/login
after you submit the form.

At this point it’s worth opening your MySQL database and looking at the contents of the users
table. You should see a new record with the details you just used to sign up and a bcrypt hash of the password.
mysql> SELECT * FROM users; +----+-----------+-----------------+--------------------------------------------------------------+---------------------+ | id | name | email | hashed_password | created | +----+-----------+-----------------+--------------------------------------------------------------+---------------------+ | 1 | Bob Jones | bob@example.com | $2a$12$mNXQrOwVWp/TqAzCCyDoyegtpV40EXwrzVLnbFpHPpWdvnmIoZ.Q. | 2024-03-18 11:29:23 | +----+-----------+-----------------+--------------------------------------------------------------+---------------------+ 1 row in set (0.01 sec)
If you like, try heading back to the signup form and adding another account with the same email address. You should get a validation failure like so:

Additional information
Using database bcrypt implementations
Some databases provide built-in functions that you can use for password hashing and verification instead of implementing your own in Go, like we have in the code above.
But it’s probably a good idea to avoid using these for two reasons:
- They tend to be vulnerable to side-channel timing attacks due to string comparison time not being constant, at least in PostgreSQL and MySQL.
- Unless you’re very careful, sending a plain-text password to your database risks the password being accidentally recorded in one of your database logs. A couple of high-profile examples of passwords being accidently recorded in logs were the GitHub and Twitter incidents in 2018.
Alternatives for checking email duplicates
I understand that the code in our UserModel.Insert()
method isn’t very pretty, and that checking the error returned by MySQL feels a bit flaky. What if future versions of MySQL change their error numbers? Or the format of their error messages?
An alternative (but also imperfect) option would be to add an UserModel.EmailTaken()
method to our model which checks to see if a user with a specific email already exists. We could call this before we try to insert a new record, and add a validation error message to the form as appropriate.
However, this would introduce a race condition to our application. If two users try to sign up with the same email address at exactly the same time, both submissions will pass the validation check but ultimately only one INSERT
into the MySQL database will succeed. The other will violate our UNIQUE
constraint and the user would end up receiving a 500 Internal Server Error
response.
The outcome of this particular race condition is fairly benign, and some people would advise you to simply not worry about it. But thinking critically about your application logic and writing code which avoids race conditions is a good habit to get into, and where there’s a viable alternative — like there is in this case — it’s better to avoid shipping with known race conditions in your codebase.