# Step-by-Step Guide to Secure Authentication in Go using JWT and Fiber

# Introduction

* After the last series on Go, I've been learning some new ideas, like Secure Authentication. I discovered JWT authentication and how it helps secure routes.
    
* In this article, I'll explain how we can use JWT to authenticate users and secure routes.
    
* So, let's jump right in.
    

---

# Creating a Base Folder Structure

* We'll start by setting up a project structure to clearly see how everything fits together.
    
* First, create a directory and open your favorite code editor. I prefer VS Code.
    

```bash
bmkdir go-auth
cd go-auth
code .
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736891030948/06a8af76-f508-4a1f-9e37-31fce7059cf4.png align="center")

* So this is our project structure :
    

**cmd/server/main.go**

* This is the entry point of the application
    

**controllers/auth\_controller.go**

* This is where we place our controllers/handlers. In our case, it will be **auth\_controller**. Here we will implement all the functions related to authentication like Register, Login, and Logout.
    

**database/db.go**

* In this database, setup is done and the connection is made. We will be using **MySQL** as our database.
    

**middleware/middleware.go**

* Middleware is simply a function that wraps around our HTTP handlers to add extra features. For example, once a user is logged in, we use middleware to secure other routes like /dashboard and /cart. We will learn about middleware in this blog.
    

**models/user.go**

* The model represents the data structure of the application’s data. So for example, if you are building an application related to a library, models can be, a book, author, etc. In this project which we are building, we need `user` a model as we are dealing with the user’s authentication.
    

**routes/routes.go**

* We define all the HTTP routes here. We are using `fiber` a package for managing the application routes. If you are familiar with express in Node.js then this package is inspired by it and provides flexible ways and functionalities to manage routes.
    

**types/types.go**

* In this, we define our own custom struct types which we require in our application.
    

**.env**

* This is an environmental file where we put all the secure stuff.
    

---

# Load `.env` File

* First of all, let’s load all the environmental variables in our application.
    
* For that, we are using a package called `gotdotenv`. This helps us achieve this.
    

```go
go get github.com/joho/godotenv
```

* Let’s load this env file inside our application. Go to `main.go` and paste below code
    

```go
import "github.com/joho/godotenv"

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}
}
```

* We place it at the top of the main function because we need all the variables loaded before the server starts.
    

---

# Connect Database

* We are using MySQL to work with the database. I was going through some queries and everything as It’s been a while since I wrote any. And then I came across the term ORM (Object-Relation Mapping). And I found it super convenient as we can use objects oriented approach to execute the queries.
    
* So an **ORM** is a programming technique that allows us to interact with a relational database like MySQL using object-oriented programming principles, instead of writing raw SQL queries. It serves as a bridge between the object-oriented language and the database, mapping database tables to objects in the application.
    
* And researching more about it I found this package called `gorm` for Go. So we will be using this to work with our database.
    
* Let’s install it and also we need to install the mysql driver for it separately.
    

```go
go get -u gorm.io/gorm
go get gorm.io/driver/mysql
```

* Let’s now create `Connect` function and write the logic to connect our application with the database.
    

```go
package database

import (
	"fmt"
	"os"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func Connect() error {
	dsn := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/go_auth?charset=utf8mb4&parseTime=True&loc=Local", os.Getenv("DB_USERNAME"), os.Getenv("DB_PASSWORD"))
}
```

* Inside `Connect()` we first initilized `dsn` (Data Source Name). `dsn` is nothing but a string that provides all the information to connect the database. It includes information like database type, server location, username password, database name, etc.
    
* Here we used `fmt.Sprintf` which returns a formatted string. We used two string `%s` placeholders for username and password. And we are getting those values from the `.env` file.
    
* Let’s first add those values to our `.env`
    

```go
// .env
DB_USERNAME=root
DB_PASSWORD=root123
```

```go
func Connect() error {
	dsn := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/go_auth?charset=utf8mb4&parseTime=True&loc=Local", os.Getenv("DB_USERNAME"), os.Getenv("DB_PASSWORD"))
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		return err
	}
}
```

* After that, we open the connection to the MySQL database using Gorm's `Open()` function. We use the `mysql` driver's `Open` function to connect with our `dsn` string. The second parameter is for gorm configuration if you have any.
    
* This `gorm.Open` function returns two things: `db` instance and `error`. So what we need to do is, first we need to create a global `gorm.DB` variable that our application can use from anywhere inside the project.
    

```go
var DB *gorm.DB

func Connect() error {
    // ...
}
```

* And that’s why we create `var DB *gorm.DB` .It’s a pointer because we need whatever changes we made in this variable, it reflects the original one which is `db` inside the `Connect` function.
    

```go
func Connect() error {
	dsn := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/go_auth?charset=utf8mb4&parseTime=True&loc=Local", os.Getenv("DB_USERNAME"), os.Getenv("DB_PASSWORD"))
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		return err
	}

	log.Info("Database connected successfully")
	DB = db

	db.AutoMigrate(&models.User{})
	return nil
}
```

* And that is why we assigned `db` to global `DB` variable.
    
* Then we have this `db.AutoMigrate(&User{})` function. What this AutoMirgrate function does is, it creates the same table with all the values defined inside this model `User` inside the database.
    
* This means that GORM will ensure that the table corresponding to the `User` model in the database matches the structure defined in the model, creating or updating columns as necessary.
    
* And so when you run the application for the first time, it will create a table for you automatically.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736931374997/8838aeb1-23a8-48ed-a0fb-11caa339f022.png align="center")

* We haven’t created our User model yet so let’s create it first.
    

---

# User Model

```go
package models

type User struct {
	Id       
	Name    
	Email    
	Password
}
```

* Here we’ve created a simple User struct, which contains basic information like ID, Name Email, and Password.
    

---

* Now that we have the database file ready we can use the Connect function inside our main function to connect the database.
    

```go
func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	if err = database.Connect(); err != nil {
		log.Fatalf("Could not connect to the database, %v", err)
	}
}
```

* Now let’s run the project to see if it’s working
    

```go
go run cmd/server/main.go
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736895776659/89b1ca4e-edd0-445a-9f93-4883d4439220.png align="center")

---

# Setting Up the Router

* Okay so for route management, as we discussed earlier we are using `fiber` package. To install run the following command inside your terminal
    

```go
go get github.com/gofiber/fiber/v2
```

* Let’s create an instance of fiber inside `main.go` to use it inside our application
    

```go
func main() {
	//...

	app := fiber.New()
}
```

* Using this `app` we can now create our routes.
    
* To make things look clean and separate we will implement all the routes inside `routes.go`.
    

```go
// routes.go
func SetupRoutes(app *fiber.App) {
	app.Post("/api/register", controllers.Register)
	app.Post("/api/login", controllers.Login)
	app.Post("/api/logout", controllers.Logout)
}
```

* Here we have created `SetupRoutes` a function that takes a `app` variable of type `*fiber.App`.
    
* Inside this, we created three routes, `/register`, `login/` and `/logout`. (Don’t worry if you are seeing an error saying these functions are not found. We are going to implement them in a bit)
    
* Now we call this function in `main.go` like this
    

```go
func main() {
	//...
    
	routes.SetupRoutes(app)
}
```

* Now let’s implement these handler functions inside controllers.
    

---

# Implementing Route Handlers

## Register

```go
// controllers/auth_controller.go
func Register(c *fiber.Ctx) error {
	var regReq types.RegisterRequest
}
```

* Since we receive the submitted data in an HTTP request, we need to create a custom struct type to convert all that data into a format that Go can understand.
    
* Let’s create a RegisterRequest struct for the Register handler
    

```go
// types/types.go

type RegisterRequest struct {
	Name    
	Email   
	Password
}
```

* Let’s now parse the request body of the register to this type
    

```go
func Register(c *fiber.Ctx) error {
	var regReq types.RegisterRequest

	if err := c.BodyParser(&regReq); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "invalid request format",
		})
	}
}
```

* Function `BodyParser` helps bind the body fields to a struct field. It returns an `error` of type `ErrUnprocessableEntity` which means if BodyParser fails to bind the fields it will throw this error.
    
* So we handled it by returning an error message “invalid request format“ with a status code 500 (StatusInternalServerError)
    
* Now before we create our user and send it to the database to store, we can’t just send the `password` as it is. We need to secure it by converting it into some hash or something.
    
* Go has this package called `bcrypt` which helps us achieve that. First, let’s install the package
    

```go
go get golang.org/x/crypto/bcrypt
```

* `bcrypt` has a function `GenerateFromPassword` that takes a byte of `password` string as the first parameter and `cost` as the second parameter (`cost` determines the computational complexity of the hashing process. It is a measure of how long it takes to hash a password, directly affecting the time it takes to verify the hash later)
    
* Let’s hash our password and then create a user object
    

```go
func Register(c *fiber.Ctx) error {
	//...

	password, _ := bcrypt.GenerateFromPassword([]byte(regReq.Password), 10)

	user := models.User{
		Name:     regReq.Name,
		Email:    regReq.Email,
		Password: password,
	}
}
```

* This bcrypt function returns two values: encrypted password and error.
    
* And after that, we can construct our user model by using `regReq` struct and encrypted `password`.
    
* Now before we send this to the database, we need to first check whether this user already exists in the database or not.
    

```go

func Register(c *fiber.Ctx) error {
    // ...   
 
    database.DB.Where("email = ?", user.Email).First(&user)
}
```

* We can do this using the `Where` function of the MySQL database. Since each email is unique in our application, we wrote a query that returns a user if they exist.
    
* Now we check if we found anything or not like this:
    

```go

func Register(c *fiber.Ctx) error {
    // ...   
 
    database.DB.Where("email = ?", user.Email).First(&user)

	if user.Id != 0 {
		return c.Status(fiber.StatusConflict).JSON(fiber.Map{
			"message": "user already exists",
		})
	}
}
```

* If `user.Id` is not `0`, it means we found a user, so we return an error message saying "user already exists."
    
* If we didn't find anything, we create a new user in the database using the `Create()` function of `DB`.
    

```go
func Register(c *fiber.Ctx) error {
    // ...

    database.DB.Create(&user)
}
```

* And once everything is done, we send a message back to the API response saying "user created successfully" along with the user information.
    

```go
func Register(c *fiber.Ctx) error {
    // ...

    return c.JSON(fiber.Map{
	    	"message": "user created successfully",
		    "data":    user,
    	})
}
```

* That was pretty straightforward, wasn’t it?
    

# Running the Server

* To run the server, we use the `app`'s `Listen` function and specify the port number like this:
    

```go
// main.go

func main() {
    //...
    
	log.Println("Starting server at: ", 3000)
	err = app.Listen(":3000")
	if err != nil {
		log.Fatalln("Error Starting Server at: ", 3000)
	}
}
```

* Now let’s run the app
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736920984238/293f89d4-7edc-4ca8-94ce-205224c96e7e.png align="center")

---

## Login

```go
// controllers/auth_controller.go

func Login(c *fiber.Ctx) error {
	var loginReq types.LoginRequest

	if err := c.BodyParser(&loginReq); err != nil {
		return err
	}

	if err := validate.Struct(&loginReq); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"message": "Validation failed",
			"errors":  err.Error(),
		})
	}
}
```

* This part of the Login remains the same as the Register. We have created a LoginRequest structure inside `types.go`
    

```go
// types/types.go

package types

...

type LoginRequest struct {
	Email 
	Password
}
```

* Now, we need to check if the user exists in the database when the client tries to log in with their credentials.
    
* To do this, we will use the `Where()` function of `DB` to see if the email is in the database. If it exists (we check this by seeing if `user.Id` is not `0`), we proceed with further operations otherwise, we return an error.
    

```go
func Login(c *fiber.Ctx) error {    
     // ...
    
    var user models.User

	database.DB.Where("email = ?", loginReq.Email).First(&user)

	if user.Id == 0 {
		c.Status(fiber.StatusNotFound)
		return c.JSON(fiber.Map{
			"message": "user not found",
		})
	}
}
```

* If the user exists, we compare the incoming password with the one in the database. If they match, the credentials are correct. If they don't match, we return an error saying "incorrect password."
    

```go
func Login(c *fiber.Ctx) error {    
     // ...
     if err := bcrypt.CompareHashAndPassword(user.Password, []byte(loginReq.Password)); err != nil {
		c.Status(fiber.StatusBadRequest)
		return c.JSON(fiber.Map{
			"message": "incorrect password",
		})
	}
}
```

* `bcrypt` has a function called `CompareHashAndPassword` that checks the incoming password against the one stored in the database.
    
* After that, we just send a JSON response with a success message and the data.
    

```go
func Login(c *fiber.Ctx) error {    
     // ...
	return c.JSON(fiber.Map{
		"message": "success",
		"data":    loginReq,
	})
}
```

* Let’s check the Login handler by running the project.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736921033382/81a139f4-7fca-448a-b056-24cbff67bc86.png align="center")

---

## Logout

* The logout feature is very simple. On the backend, there's not much to do. The frontend should handle the logout process.
    
* So, when this route is accessed, we just return a success message.
    

```go
func Logout(c *fiber.Ctx) error {

	return c.JSON(fiber.Map{
		"message": "Logged out successfully",
	})
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736920106216/9ba3f9b1-803b-443b-9f44-a5246f4ca2cd.png align="center")

* One problem right now here is, that even if you aren’t logged in, you can still log out. And this issue we will resolve below by using JWT.
    

---

# JSON Tags

* If you see the response of Login and Register…
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736921232447/a2098f0a-477a-4ca1-9a66-216cb61b47d5.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736921246555/a67e94b8-e1eb-4fe4-84f9-9bdafd0cb9d2.png align="center")

* We notice that the field names are not in lowercase. To convert these fields into a JSON-friendly format, we use what are called **Tags** in Go.
    
* Tags specify how struct fields should be encoded into or decoded from JSON. So, even if we have a struct field like `Name`, we can encode and decode it as `name` using an annotation.
    
* To achieve this, we need to make changes inside `types.go` and `user.go`.
    

```go
// user.go

package models

type User struct {
	Id       uint   `json:"id"`
	Name     string `json:"name"`
	Email    string `json:"email" gorm:"unique"`
	Password []byte `json:"-"`
}
```

* Here, we made the email a unique key in the database. This means the same email address can't be used again.
    
* Another thing to note is that we added `-` in front of `Password` because we don't want this field to appear in the response for security reasons.
    
* Now, let's modify `types.go`.
    

```go
package types

type RegisterRequest struct {
	Name     string `json:"name"`
	Email    string `json:"email"`
	Password string `json:"password"`
}

type LoginRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}
```

* Now if you run the project and see the response, it should be fixed
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736921680567/9a617c39-56bc-4048-9d37-20db3a10fe3f.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736921709635/ff22bb8a-a347-46ee-8d21-e8985780cdfd.png align="center")

---

# JWT Authentication

* Let’s first understand what JWT is before we dive into code.
    

## What is JWT?

* JWT (JSON Web Token) is a way to safely send data between the client and server.
    
* It is a token made up of three main parts:
    
    * **Header:** This includes information like the signing method and type of token.
        
    * **Payload:** This has Claims, which are details about the user data, such as user ID and expiration time.
        
    * **Signature:** This is made by signing the header and payload with a secret or private key. It checks if the token is real.
        

## Flow of JWT Authentication

* The client sends credentials to the server.
    
* If the credentials are valid, the server generates a JWT containing the user's information and signs it.
    
* The server sends the JWT to the client, typically as part of the HTTP response.
    
* The client stores the token (e.g., in local storage or an HTTP-only cookie).
    
* The client includes the JWT in the `Authorization` header (ex, `Bearer <token>`).
    
* The server validates the token’s signature and checks its claims (e.g., expiration time, roles).
    
* If valid, the server processes the request; otherwise, it rejects it.
    

## Implement JWT Authentication

* To use JWT in our app, let’s first add the package
    

```go
go get github.com/dgrijalva/jwt-go/v4
```

* In our app, when a user logs in successfully, we create a JWT token. After checking the password, we start by creating a claim to build the token.
    

```go
if err := bcrypt.CompareHashAndPassword(...); err != nil {//...}

claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
		Issuer: strconv.Itoa(int(user.Id)),
		ExpiresAt: &jwt.Time{
			Time: time.Now().Add(time.Hour * 24),
		},
	})
```

* `jwt` provides a function called `NewWithClaims` to create a Claim. It takes two parameters:
    
    * **Signing Method:** We can choose from several secure hashing algorithms, but we are using HS256.
        
    * **Claim:** We include the claims we mentioned earlier, like user ID and expiry time, using the `jwt.StandardClaims` interface. We set the user ID in `Issuer` as a string and `ExpiresAt` with a time value, set to 24 hours. This means the token will expire after 24 hours.
        
    * Now that we have the claims ready, we will create a token.
        

```go
token, err := claims.SignedString([]byte(os.Getenv("JWT_SECRET")))
	if err != nil {
		c.Status(fiber.StatusInternalServerError)
		return c.JSON(fiber.Map{
			"message": "could not log in",
		})
	}
```

* As you can see claims have a method named `SignedString` which we use to sign the JWT secret. We typically store the secret inside `.env` file.
    

```go
// .env

// ... other keys
JWT_SECRET=secret
```

* Use a strong secret key for better security. I have used `secret` for testing purposes.
    
* And after everything’s done we get the token.
    
* If you remember this line:
    

“ A JWT token is simply a mix of signed claims and secret/private keys. I hope this explanation makes it clear.”

* And as mentioned in the **Flow of JWT part** we send this token to the client so that they can send it back to the backend to access secure/private routes
    

```go
// auth_controller.go

func Login(c *fiber.Ctx) error {

// ...
	
return c.JSON(fiber.Map{
		"message": "success",
		"token":   token,
		"data":    loginReq,
	})

}
```

* Okay so, now that we created the JWT token. Whenever a user tries to access private routes, we need to first validate the user, whether he is logged in or not. To do that we create a middleware
    

---

# Middlewares

* In Go, we create middleware that acts as a layer between the HTTP request and the handler that processes it.
    
* Middleware is simply a function that takes an HTTP handler and returns an HTTP handler. In our app, we need middleware to ensure the user accessing the route is authorized to access private routes before entering.
    
* To implement it, go to `middleware/middlewares.go` and paste the code below.
    

```go
package middleware

import (
	"os"

	jwtware "github.com/gofiber/contrib/jwt"
	"github.com/gofiber/fiber/v2"
)

func Protected() fiber.Handler {
	return jwtware.New(jwtware.Config{
		SigningKey: jwtware.SigningKey{
			JWTAlg: "HS256",
			Key:    []byte(os.Getenv("JWT_SECRET")),
		},
		TokenLookup:  "header:Authorization",
		AuthScheme:   "Bearer",
		ErrorHandler: jwtError,
	})
}

func jwtError(c *fiber.Ctx, err error) error {
	return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
		"message": "Unauthorized",
	})
}
```

* Starting with imports, we are using `"github.com/gofiber/contrib/jwt"` packages and gave aliases as `jwtware`.
    
* Here we have created a function `Protected()` that returns an HTTP handler. Inside this we are returning `jwtware.New()` handler which is used  to create a new middleware instance for validating JWT
    
* Here we pass different keys:
    
    * SigningKey: This specifies the algorithm and key for verifying JWT’s signature.
        
    * TokenLookup: Defines where the middleware should look for JWT
        
    * AuthScheme: It defines which scheme to use in `Authorization`
        
    * ErrorHandler: It is a custom function to handle errors. So whenever an unauthorized user access any routes which require authorization, this throws an error message: “Unauthorized “
        
* In our case, we need two routes: Log in and Register as Public routes and Logout as Private route. How do we do that?
    

```go
// routes.go

func SetupRoutes(app *fiber.App) {
	// Public routes
	app.Post("/api/register", controllers.Register)
	app.Post("/api/login", controllers.Login)

	app.Use(middleware.Protected())

	// Protected routes
	app.Post("/api/logout", controllers.Logout)
}
```

* As you can see, to use any middleware, fiber provides a very handy method called `Use()` . So what will happen now is whatever routes defined after `app.Use(middleware.Protected())` this will require a JWT Token to get access.
    
* Let’s see it in action.
    

1. Open Postman
    
2. Try to logout directly
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736929382431/16d32da0-e63f-4720-b158-736f19cf49d8.png align="center")

As you can see, Now it is throwing an error saying “Unauthorized“.

3. Now login with valid credentials
    
4. ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736929416766/a3ac0569-311d-4ac7-a5f3-df92654c88b2.png align="center")
    
    Copy the token and paste it inside the Authorization Bearer field and then try to logout.
    
5. ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736929513628/c7f57d57-c17e-4f5a-aa5d-cdd106e002c9.png align="center")
    

![a group of minions are standing next to each other and one of them is saying yay .](https://media1.tenor.com/m/9PItHYqz9gwAAAAd/yay-moinyin.gif align="center")

---

# Code

%[https://github.com/red-star25/jwt-auth-go] 

---

# Conclusion

* In this article, we've explored how to implement secure user authentication in a Go application using JWTs and the Fiber framework.
    
* We saw how to set up a project structure, connect to a MySQL database with GORM, and define essential routes and middleware.
    
* We've built handlers for managing user registration, login, and protected routes. This approach not only enhances the security of your application but also provides a scalable framework for future development.
    
* I hope you liked this article and learned something from it. If you did consider leaving a like and feedback in the comment section.
    
* See you in the next one, until then….
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711803084752/aa266313-a315-41a8-9538-8ff4370577fb.gif?auto=format,compress&gif-q=60&format=webm&auto=format,compress&gif-q=60&format=webm&auto=format,compress&gif-q=60&format=webm&auto=format,compress&gif-q=60&format=webm align="center")

* Connect with me on [**LinkedIn**](https://www.linkedin.com/in/dhruv-nakum-4b1054176/)**,** [**Github**](https://github.com/red-star25)**, and** [**Twitter**](https://twitter.com/dhruv_nakum)**.**
