Hashing password
Tags: go, postgres
Published: 2021-06-24 17:20:49

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](https://github.com/golang/crypto) library. First we need to expand our User structure:

 ```go
 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:

```go
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`:

```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:

```bash
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:

![Database entry with hashed password](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnakj5i72inyw6niqu8p.png)