In last section we have created users table in our database, but we are currently storing user's password in plain text. This is something we should never do, and instead we need to store hashed password with random salt. For that we will use golang/crypto library. First we need to expand our User structure:
type User struct {
ID int
Username string `binding:"required,min=5,max=30"`
Password string `pg:"-" binding:"required,min=7,max=32"`
HashedPassword []byte `json:"-"`
Salt []byte `json:"-"`
CreatedAt time.Time
ModifiedAt time.Time
}
There are few new things here. First, we added pg:"-"
for password validation. What that means? Well, this simply defines that there is no password
column in users database table. This field is still required because user needs to provide it, but it will not be saved in database. Instead of that, we will generate random Salt
and use Password
field to generate HashedPassword
. Then we will store Salt and HashedPassword in database. When authenticating user with Username and Password, we can use Salt from database to calculate HashedPassword based on provided Password and then compare it with HashedPassword stored in database. We also added json:"-"
for HashedPassword, and Salt
fields so they will not be sent to fronted in JSON response and will also be ignored if sent to backend as JSON. User struct is now ready to go, but we also need to update our migration file:
package main
import (
"fmt"
"github.com/go-pg/migrations/v8"
)
func init() {
migrations.MustRegisterTx(func(db migrations.DB) error {
fmt.Println("creating table users...")
_, err := db.Exec(`CREATE TABLE users(
id SERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
hashed_password BYTEA NOT NULL,
salt BYTEA NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
)`)
return err
}, func(db migrations.DB) error {
fmt.Println("dropping table users...")
_, err := db.Exec(`DROP TABLE users`)
return err
})
}
When this is done we are ready to update rest of our internal/store/users.go
:
package store
import (
"crypto/rand"
"time"
"golang.org/x/crypto/bcrypt"
)
type User struct {
ID int
Username string `binding:"required,min=5,max=30"`
Password string `pg:"-" binding:"required,min=7,max=32"`
HashedPassword []byte `json:"-"`
Salt []byte `json:"-"`
CreatedAt time.Time
ModifiedAt time.Time
}
func AddUser(user *User) error {
salt, err := GenerateSalt()
if err != nil {
return err
}
toHash := append([]byte(user.Password), salt...)
hashedPassword, err := bcrypt.GenerateFromPassword(toHash, bcrypt.DefaultCost)
if err != nil {
return err
}
user.Salt = salt
user.HashedPassword = hashedPassword
_, err = db.Model(user).Returning("*").Insert()
if err != nil {
return err
}
return err
}
func Authenticate(username, password string) (*User, error) {
user := new(User)
if err := db.Model(user).Where(
"username = ?", username).Select(); err != nil {
return nil, err
}
salted := append([]byte(password), user.Salt...)
if err := bcrypt.CompareHashAndPassword(user.HashedPassword, salted); err != nil {
return nil, err
}
return user, nil
}
func GenerateSalt() ([]byte, error) {
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return nil, err
}
return salt, nil
}
Since we changed our database schema, now we also need to reset our database:
cd migrations/
go run *.go reset
go run *.go up
Now let's go to our frontend and create new account again. If we check our database now, we can see that there is no more password column, but there are salt and hashed_password columns:
Top comments (0)