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
Now we need to navigate into the go_auth_app
and run the go mod init
command
cd go_auth_app && go mod init
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
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)
}
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
Now we have GORM
installed we need to install a compatible SQL driver.
go get -u gorm.io/driver/mysql
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
}
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)
}
}
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
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, ""
}
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
}
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)
}
}
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)