DEV Community

Matija Krajnik
Matija Krajnik

Posted on • Updated on • Originally published at letscode.blog

Hashing password

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
}
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Since we changed our database schema, now we also need to reset our database:

cd migrations/
go run *.go reset
go run *.go up
Enter fullscreen mode Exit fullscreen mode

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

Latest comments (0)