DEV Community

Cover image for Building a Basic Auth System With Go and MySQL
Kinanee Samson
Kinanee Samson

Posted on

Building a Basic Auth System With Go and MySQL

Go is one of the few languages that have caught my attention in the past. So in our quest to do more, we will build a simple auth system using Go. I am starting to like writing Go because the syntax is quite elegant and helps to reduce the overall amount of code I have to write.

Go is also very fast and as such it would make so much sense for us to build a backend service with Go, in today’s post we will explore how to build a simple authentication system using Go and as such we will cover the following talking points;

  • Project Setup
  • Integration with MySQL and GORM
  • Create Account
  • Login

Project Setup

The first thing we need to do is to set up a new project and for that, we’ll need to create a new directory “go_auth_app”. We’ll navigate into and run the go init command inside the directory.

mkdir go_auth_app 
Enter fullscreen mode Exit fullscreen mode

Now we need to navigate into the go_auth_app and run the go mod init command

cd go_auth_app && go mod init
Enter fullscreen mode Exit fullscreen mode

Let’s install the dependencies that we will need for this project starting with Gorilla Mux Http amongst others.

go get -u github.com/gorilla/mux
Enter fullscreen mode Exit fullscreen mode

Now let's create a server.go file in our project root. This file will contain our server so let's create a basic server.

package main

import (
  "net/http"
  "github.com/gorilla/mux"
)

func main () {
  r := mux.NewRouter()
  http.handle("/", r)
  http.ListenAndServe(":8080", nil)
}
Enter fullscreen mode Exit fullscreen mode

Integration with MySQL and GORM

Now we need to install an ORM that will simplify the process of interacting with our database and for that, we will use GORM. Let's go ahead and add that dependency to our project.

go get -u gorm.io/gorm
Enter fullscreen mode Exit fullscreen mode

Now we have GORM installed we need to install a compatible SQL driver.

go get -u gorm.io/driver/mysql
Enter fullscreen mode Exit fullscreen mode

Now let's create a model for a user. To do that we need to create a new folder. models and inside this folder, we will create a new file user.go let's add the following code to the user.go.

// go_auth_app/models/user.go

package models

import (
  "gorm.io/gorm"
)

type User struct {
  gorm.Model
  Name string
  Age uint
  Email string
  Phone string
  Password string
}
Enter fullscreen mode Exit fullscreen mode

Create User

Now that we have our model set up, we need to create a controller package to export some functions for interacting with our data. Let's make a folder called controllers and inside we'll create a new go file user.go.


// go_auth_app/controllers/user.go

package controller

import (
  "net/http"
  "encoding/json"

  "go_auth_app/helper"

  "gorm.io/gorm"
  "golang.org/x/crypto/bcrypt"
)

func ReturnCreateUser(db *gorm.DB) func(http.ResponseWriter, *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        user, err := helper.ParseRequestBody(r)

        if len(err) > 1 {
            http.Error(w, err, http.StatusBadRequest)
            return
        }

        toHash := []byte(user.Password)
        hashedPassword, hashErr := bcrypt.GenerateFromPassword(toHash, 14)
        if hashErr != nil {
            {
                http.Error(w, "Failed to hash password", http.StatusInternalServerError)
                return
            }
        }

        createdUser := models.User{Name: user.Name, Age: user.Age, Email: user.Email, Password: string(hashedPassword)}
        result := db.Create(&createdUser)

        if result == nil {
            http.Error(w, "Failed to create user", http.StatusInternalServerError)
            return
        }
        json.NewEncoder(w).Encode(createdUser)
    }
}

Enter fullscreen mode Exit fullscreen mode

The code snippet above implements a function called ReturnCreateUser in the controller package. Here's a breakdown of what the code does: This function is designed to handle creating a new user in the system. It takes the database connection (db of type *gorm.DB) as input and returns another function that acts as the actual route handler.

Inside the route handler function, we call ParseRequestBody on the helper package. The ParseRequestBody function is used to extract user data (name, age, email, password) from the request body (likely sent as JSON data). If there's an error parsing the request body (length of err string is greater than 1), then we send a bad request (400) error response.

Next, we hash the user's Password by converting the user's password (assumed to be a string) to a byte slice ([]byte). We use the bcrypt.GenerateFromPassword function to securely hash the password. Bcrypt is a popular hashing algorithm for passwords. The function takes the password bytes and a cost factor (14 in this case) to control the hashing intensity. If there's an error during hashing (hashErr is not nil), it sends an internal server error (500) response with a message and exits. Run the following command to install the bcrypt library.

go get golang.org/x/crypto/bcrypt
Enter fullscreen mode Exit fullscreen mode

Then we create a new models.User object by populating the user object with the parsed data (name, age, email) and the hashed password converted back to a string. Then we save the user to the Database using the database connection (db). We call the Create method on the database connection, passing the createdUser object as an argument. If the Create operation fails (result is nil), it sends an internal server error (500) response with a message and exits. If successful, we use the json.NewEncoder to encode the newly created user object (including the ID generated by the database) back into JSON format and sends it as the response. Now we need to implement the helper package and the ParseRequestBody function.


// go_auth_app/helper/helper.go

package helper

import (
    "encoding/json"
    "io"
    "net/http"
)

type Payload struct {
    Name     string `json:"name"`
    Age      int    `json:"age"`
    Email    string `json:"email"`
    Password string `json:"password"`
}

func ParseRequestBody[S string, P Payload](req *http.Request) (P, S) {
    body, err := io.ReadAll(req.Body)

    var payload P
    if err != nil {
        return payload, "Error reading request body"
    }
    err = json.Unmarshal(body, &payload)

    if err != nil {
        return payload, "Invalid JSON format in the request body"
    }

    return payload, ""

}

Enter fullscreen mode Exit fullscreen mode

The code snippet above defines a package named helper containing functionalities for parsing request bodies in a Go web application. The package imports the necessary libraries:
encoding/json for working with JSON data.
io for reading data from the request body.
net/http for accessing request information.

Then we define a struct named Payload. This struct represents the expected format of the data sent in the request body. It has fields for Name, Age (int), Email, and Password. The json tag specifies the corresponding JSON field names for each struct field during marshaling and unmarshalling. This function is designed to parse the request body and extract the user data. It takes an http.Request object as input and returns two values: P An instance of the Payload struct populated with the parsed user data (name, age, email, password). S: A string representing any error message encountered during parsing.

Now let's put all of this together in our server.go file


// go_auth_app/server.go

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"

    "go_auth_app/controller"
    "go_auth_app/models"
)

func main() {
    r := mux.NewRouter()

    db, err := gorm.Open(mysql.New(mysql.Config{
        DSN: "root@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=true",
    }))

    if err != nil {
        panic("failed to connect database")
    }

    // Migrate the schema
    db.AutoMigrate(&models.User{})

    r.HandleFunc("/user", controller.ReturnCreateUser(db)).Methods("POST")
    r.HandleFunc("/login", controller.ReturnLoginUser(db)).Methods("POST")

    http.Handle("/", r)
    log.Println("server started on port 8080")
    http.ListenAndServe(":8080", nil) // Start server on port 8080
}

Enter fullscreen mode Exit fullscreen mode

Login

The code snippet in the main package ties together the previously explained controller and helper packages to create a functional web server application. Now let's add a new handler function to enable the user to log in to their account. We'll edit the controller/user file to add the handler function to enable the user login.

// go_auth_app/controller/user.go

package controller

// cont'd

func ReturnLoginUser(db *gorm.DB) func(http.ResponseWriter, *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        payload, err := helper.ParseRequestBody(r)

        if len(err) > 1 {
            http.Error(w, err, http.StatusBadRequest)
            return
        }

        var user models.User
        db.Where("email =?", payload.Email).First(&user) // Find user by email
        // if the user with that email does not exist throw an error
        if user.Email == "" {
            http.Error(w, "User not found", http.StatusNotFound)
            return
        }

        // compare their passwords
        compareErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(payload.Password))
        if compareErr != nil {
            http.Error(w, "Invalid credentials", http.StatusUnauthorized)
            return
        }
        // if the passwords match, return the user
        json.NewEncoder(w).Encode(user)
    }
}
Enter fullscreen mode Exit fullscreen mode

That's going to be all for now, I hope you found this useful and leave your thoughts on the article and building a web server with Go in the comment section below.

Top comments (0)